├── .fantomasignore ├── global.json ├── docsSrc ├── content │ ├── logo.png │ ├── fsdocs-custom.css │ ├── fsdocs-light.css │ ├── navbar-fixed-left.css │ ├── fsdocs-dark.css │ ├── theme-toggle.js │ ├── logo.svg │ └── fsdocs-main.css ├── _menu-item_template.html ├── How_Tos │ ├── Doing_Another_Thing.md │ └── Doing_A_Thing.md ├── _menu_template.html ├── Tutorials │ └── Getting_Started.md ├── index.md ├── Explanations │ └── Background.md └── _template.html ├── .vscode ├── settings.json └── extensions.json ├── tests ├── Cosmos.Tests │ ├── Attributes.fs │ ├── Tests.fs │ └── FSharp.Azure.Cosmos.Tests.fsproj └── Directory.Build.props ├── FSharp.Azure.Cosmos.slnf ├── .git-blame-ignore-revs ├── FSharp.Azure.Cosmos.slnx ├── .github ├── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md ├── ISSUE_TEMPLATE.md ├── workflows │ ├── publish_release.yml │ ├── build.yml │ ├── fsdocs-gh-pages.yml │ └── publish_ci.yml ├── PULL_REQUEST_TEMPLATE.md └── copilot-instructions.md ├── .devcontainer ├── Dockerfile └── devcontainer.json ├── src ├── Directory.Build.props └── Cosmos │ ├── AssemblyInfo.fs │ ├── IterationExtensions.fs │ ├── UniqueKey.fs │ ├── CosmosResponse.fs │ ├── TaskSeq.fs │ ├── FSharp.Azure.Cosmos.fsproj │ ├── ReadMany.fs │ ├── Create.fs │ ├── Delete.fs │ ├── Read.fs │ ├── Patch.fs │ ├── Cosmos.fs │ ├── Replace.fs │ └── Upsert.fs ├── .gitattributes ├── .config └── dotnet-tools.json ├── LICENSE ├── CHANGELOG.md ├── Directory.Packages.props ├── Directory.Build.props ├── Directory.Build.targets ├── README.md ├── .gitignore └── .editorconfig /.fantomasignore: -------------------------------------------------------------------------------- 1 | # Ignore AssemblyInfo files 2 | AssemblyInfo.fs 3 | -------------------------------------------------------------------------------- /global.json: -------------------------------------------------------------------------------- 1 | { 2 | "sdk": { 3 | "version": "9.0.300", 4 | "rollForward": "latestMinor" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /docsSrc/content/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fsprojects/FSharp.Azure.Cosmos/HEAD/docsSrc/content/logo.png -------------------------------------------------------------------------------- /docsSrc/_menu-item_template.html: -------------------------------------------------------------------------------- 1 |
  • {{fsdocs-menu-item-content}}
  • -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "FSharp.fsacRuntime":"netcore", 3 | "FSharp.enableAnalyzers": false, 4 | "FSharp.analyzersPath": [ 5 | "./packages/analyzers" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /docsSrc/How_Tos/Doing_Another_Thing.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: How To do a second thing 3 | category: How To Guides 4 | categoryindex: 2 5 | index: 2 6 | --- 7 | 8 | # How To do a second thing 9 | 10 | -------------------------------------------------------------------------------- /tests/Cosmos.Tests/Attributes.fs: -------------------------------------------------------------------------------- 1 | namespace FSharp.Azure.Cosmos 2 | 3 | open Microsoft.VisualStudio.TestTools.UnitTesting 4 | 5 | [] 6 | 7 | do () 8 | -------------------------------------------------------------------------------- /FSharp.Azure.Cosmos.slnf: -------------------------------------------------------------------------------- 1 | { 2 | "solution": { 3 | "path": "FSharp.Azure.Cosmos.slnx", 4 | "projects": [ 5 | "src\\Cosmos\\FSharp.Azure.Cosmos.fsproj", 6 | "tests\\Cosmos.Tests\\FSharp.Azure.Cosmos.Tests.fsproj" 7 | ] 8 | } 9 | } -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "ionide.ionide-paket", 4 | "ionide.ionide-fsharp", 5 | "ionide.ionide-fake", 6 | "ms-dotnettools.csharp", 7 | "editorConfig.editorConfig" 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /tests/Cosmos.Tests/Tests.fs: -------------------------------------------------------------------------------- 1 | namespace Tests 2 | 3 | open System 4 | open Microsoft.VisualStudio.TestTools.UnitTesting 5 | 6 | [] 7 | type TestClass () = 8 | 9 | [] 10 | member this.TestMethodPassing () = Assert.IsTrue (true) 11 | -------------------------------------------------------------------------------- /tests/Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | false 5 | true 6 | 7 | 8 | -------------------------------------------------------------------------------- /docsSrc/_menu_template.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.git-blame-ignore-revs: -------------------------------------------------------------------------------- 1 | # This file contains a list of git hashes of revisions to be ignored by git 2 | # These revisions are considered "unimportant" in 3 | # that they are unlikely to be what you are interested in when blaming. 4 | # Like formatting with Fantomas 5 | # https://docs.github.com/en/repositories/working-with-files/using-files/viewing-a-file#ignore-commits-in-the-blame-view 6 | # Add formatting commits here 7 | -------------------------------------------------------------------------------- /docsSrc/content/fsdocs-custom.css: -------------------------------------------------------------------------------- 1 | .fsharp-icon-logo { 2 | width: 25px; 3 | margin-top: -2px; 4 | -webkit-filter: grayscale(100%) brightness(1) invert(1); /* Safari 6.0 - 9.0 */ 5 | filter: grayscale(100%) brightness(1) invert(1); 6 | } 7 | 8 | 9 | body .navbar .dropdown-menu .active .bi { 10 | display: block !important; 11 | } 12 | 13 | nav.navbar .dropdown-item img.fsharp-icon-logo { 14 | margin-right: 0px; 15 | } 16 | -------------------------------------------------------------------------------- /docsSrc/How_Tos/Doing_A_Thing.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: How To do a first thing 3 | category: How To Guides 4 | categoryindex: 2 5 | index: 1 6 | --- 7 | 8 | # How To do a first thing 9 | 10 | The best way to use IObservable is to use one .Subscribe()-method which will take function parameters (which will be "injected" to the right place). 11 | 12 | Use Rx (or R3) when you need async events to communicate with each other, e.g.: 13 | - Events, WebServices, Threads, Timers, AutoComplete, Drag & Drop, ... 14 | 15 | -------------------------------------------------------------------------------- /FSharp.Azure.Cosmos.slnx: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | 5 | --- 6 | 7 | **Is your feature request related to a problem? Please describe.** 8 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 9 | 10 | **Describe the solution you'd like** 11 | A clear and concise description of what you want to happen. 12 | 13 | **Describe alternatives you've considered** 14 | A clear and concise description of any alternative solutions or features you've considered. 15 | 16 | **Additional context** 17 | Add any other context or screenshots about the feature request here. 18 | -------------------------------------------------------------------------------- /.devcontainer/Dockerfile: -------------------------------------------------------------------------------- 1 | # [Choice] Debian version (use bullseye on local arm64/Apple Silicon): bookworm, bullseye, buster 2 | ARG VARIANT="bookworm" 3 | FROM buildpack-deps:${VARIANT}-curl 4 | 5 | 6 | ENV \ 7 | # Enable detection of running in a container 8 | DOTNET_RUNNING_IN_CONTAINER=true \ 9 | DOTNET_ROOT=/usr/share/dotnet/ \ 10 | DOTNET_NOLOGO=true \ 11 | DOTNET_CLI_TELEMETRY_OPTOUT=false\ 12 | DOTNET_USE_POLLING_FILE_WATCHER=true 13 | 14 | 15 | # [Optional] Uncomment this section to install additional OS packages. 16 | # RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ 17 | # && apt-get -y install --no-install-recommends 18 | -------------------------------------------------------------------------------- /src/Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | true 5 | true 6 | false 7 | 8 | 9 | true 10 | true 11 | $(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files 2 | * text=auto 3 | 4 | # Custom for Visual Studio 5 | *.cs diff=csharp text=auto eol=lf 6 | *.vb diff=csharp text=auto eol=lf 7 | *.fs diff=csharp text=auto eol=lf 8 | *.fsi diff=csharp text=auto eol=lf 9 | *.fsx diff=csharp text=auto eol=lf 10 | *.sln text eol=crlf merge=union 11 | *.csproj merge=union 12 | *.vbproj merge=union 13 | *.fsproj merge=union 14 | *.dbproj merge=union 15 | *.sh text eol=lf 16 | 17 | # Standard to msysgit 18 | *.doc diff=astextplain 19 | *.DOC diff=astextplain 20 | *.docx diff=astextplain 21 | *.DOCX diff=astextplain 22 | *.dot diff=astextplain 23 | *.DOT diff=astextplain 24 | *.pdf diff=astextplain 25 | *.PDF diff=astextplain 26 | *.rtf diff=astextplain 27 | *.RTF diff=astextplain 28 | -------------------------------------------------------------------------------- /.config/dotnet-tools.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1, 3 | "isRoot": true, 4 | "tools": { 5 | "dotnet-reportgenerator-globaltool": { 6 | "version": "5.4.7", 7 | "commands": [ 8 | "reportgenerator" 9 | ], 10 | "rollForward": false 11 | }, 12 | "fsharp-analyzers": { 13 | "version": "0.31.0", 14 | "commands": [ 15 | "fsharp-analyzers" 16 | ], 17 | "rollForward": false 18 | }, 19 | "fantomas": { 20 | "version": "7.0.1", 21 | "commands": [ 22 | "fantomas" 23 | ], 24 | "rollForward": false 25 | }, 26 | "fsdocs-tool": { 27 | "version": "20.0.1", 28 | "commands": [ 29 | "fsdocs" 30 | ], 31 | "rollForward": false 32 | } 33 | } 34 | } -------------------------------------------------------------------------------- /tests/Cosmos.Tests/FSharp.Azure.Cosmos.Tests.fsproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | $(AssemblyBaseName).Tests 6 | 7 | Exe 8 | true 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Description 2 | 3 | Please insert a description of your problem or question. 4 | 5 | ## Error messages, screenshots 6 | 7 | Please add any error logs or screenshots if available. 8 | 9 | ## Failing test, failing GitHub repo, or reproduction steps 10 | 11 | Please add either a failing test, a GitHub repo of the problem or detailed reproduction steps. 12 | 13 | ## Expected Behavior 14 | 15 | Please define what you would expect the behavior to be like. 16 | 17 | ## Known workarounds 18 | 19 | Please provide a description of any known workarounds. 20 | 21 | ## Other information 22 | 23 | * Operating System: 24 | - [ ] windows [insert version here] 25 | - [ ] macOs [insert version] 26 | - [ ] linux [insert flavor/version here] 27 | * Platform 28 | - [ ] dotnet core 29 | - [ ] dotnet full 30 | - [ ] mono 31 | * Branch or release version: 32 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | 5 | --- 6 | 7 | **Describe the bug** 8 | A clear and concise description of what the bug is. 9 | 10 | **To Reproduce** 11 | Steps to reproduce the behavior: 12 | 1. Go to '...' 13 | 2. Click on '....' 14 | 3. Scroll down to '....' 15 | 4. See error 16 | 17 | **Expected behavior** 18 | A clear and concise description of what you expected to happen. 19 | 20 | **Screenshots** 21 | If applicable, add screenshots to help explain your problem. 22 | 23 | **Desktop (please complete the following information):** 24 | - OS: [e.g. iOS] 25 | - Browser [e.g. chrome, safari] 26 | - Version [e.g. 22] 27 | 28 | **Smartphone (please complete the following information):** 29 | - Device: [e.g. iPhone6] 30 | - OS: [e.g. iOS8.1] 31 | - Browser [e.g. stock browser, safari] 32 | - Version [e.g. 22] 33 | 34 | **Additional context** 35 | Add any other context about the problem here. 36 | -------------------------------------------------------------------------------- /.github/workflows/publish_release.yml: -------------------------------------------------------------------------------- 1 | name: Publish to NuGet 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'releases/*' 7 | 8 | env: 9 | CONFIGURATION: Release 10 | jobs: 11 | build: 12 | # Sets permissions of the GITHUB_TOKEN to allow release creating 13 | permissions: 14 | contents: write 15 | environment: 16 | name: nuget 17 | runs-on: ubuntu-latest 18 | steps: 19 | - uses: actions/checkout@v3 20 | - name: Setup necessary dotnet SDKs 21 | uses: actions/setup-dotnet@v3 22 | with: 23 | global-json-file: global.json 24 | dotnet-version: | 25 | 9.x 26 | 8.x 27 | 28 | - name: Publish to NuGet 29 | env: 30 | NUGET_TOKEN: ${{ secrets.NUGET_TOKEN }} 31 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 32 | FAKE_DETAILED_ERRORS: true 33 | ENABLE_COVERAGE: false # AltCover doesn't work with Release builds, reports lower coverage than actual 34 | run: | 35 | chmod +x ./build.sh 36 | ./build.sh Publish 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Andrii Chebukin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/Cosmos/AssemblyInfo.fs: -------------------------------------------------------------------------------- 1 | // Auto-Generated by FAKE; do not edit 2 | namespace System 3 | open System.Reflection 4 | 5 | [] 6 | [] 7 | [] 8 | [] 9 | [] 10 | [] 11 | [] 12 | [] 13 | do () 14 | 15 | module internal AssemblyVersionInformation = 16 | let [] AssemblyTitle = "FSharp.Azure.Cosmos" 17 | let [] AssemblyProduct = "FSharp.Azure.Cosmos" 18 | let [] AssemblyVersion = "1.0.1" 19 | let [] AssemblyMetadata_ReleaseDate = "2025-08-08T00:00:00.0000000+04:00" 20 | let [] AssemblyFileVersion = "1.0.1" 21 | let [] AssemblyInformationalVersion = "1.0.1" 22 | let [] AssemblyMetadata_ReleaseChannel = "release" 23 | let [] AssemblyMetadata_GitHash = "403c778b1892e1686188a30f1bb8e89372d68951" 24 | -------------------------------------------------------------------------------- /src/Cosmos/IterationExtensions.fs: -------------------------------------------------------------------------------- 1 | namespace Microsoft.Azure.Cosmos 2 | 3 | open System.Runtime.CompilerServices 4 | open System.Runtime.InteropServices 5 | open System.Threading 6 | open Microsoft.Azure.Cosmos 7 | open FSharp.Control 8 | 9 | [] 10 | module FeedIteratorExtensions = 11 | 12 | // See https://github.com/Azure/azure-cosmos-dotnet-v3/issues/903 13 | type FeedIterator<'T> with 14 | 15 | /// Converts the iterator to an async sequence of items. 16 | member iterator.AsAsyncEnumerable<'T> ([] cancellationToken : CancellationToken) = taskSeq { 17 | while iterator.HasMoreResults do 18 | let! page = iterator.ReadNextAsync (cancellationToken) 19 | 20 | for item in page do 21 | cancellationToken.ThrowIfCancellationRequested () 22 | yield item 23 | } 24 | 25 | open System.Linq 26 | open Microsoft.Azure.Cosmos 27 | open Microsoft.Azure.Cosmos.Linq 28 | 29 | [] 30 | module QueryableExtensions = 31 | 32 | type IQueryable<'T> with 33 | 34 | member inline query.AsAsyncEnumerable<'T> ([] cancellationToken : CancellationToken) = 35 | query.ToFeedIterator().AsAsyncEnumerable<'T> (cancellationToken) 36 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Proposed Changes 2 | 3 | Describe the big picture of your changes here to communicate to the maintainers why we should accept this pull request. If it fixes a bug or resolves a feature request, be sure to link to that issue. 4 | 5 | ## Types of changes 6 | 7 | What types of changes does your code introduce to FSharp.Azure.Cosmos? 8 | _Put an `x` in the boxes that apply_ 9 | 10 | - [ ] Bugfix (non-breaking change which fixes an issue) 11 | - [ ] New feature (non-breaking change which adds functionality) 12 | - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) 13 | 14 | 15 | ## Checklist 16 | 17 | _Put an `x` in the boxes that apply. You can also fill these out after creating the PR. If you're unsure about any of them, don't hesitate to ask. We're here to help! This is simply a reminder of what we are going to look for before merging your code._ 18 | 19 | - [ ] Build and tests pass locally 20 | - [ ] I have added tests that prove my fix is effective or that my feature works (if appropriate) 21 | - [ ] I have added necessary documentation (if appropriate) 22 | 23 | ## Further comments 24 | 25 | If this is a relatively large or complex change, kick off the discussion by explaining why you chose the solution you did and what alternatives you considered, etc... 26 | -------------------------------------------------------------------------------- /src/Cosmos/UniqueKey.fs: -------------------------------------------------------------------------------- 1 | [] 2 | module FSharp.Azure.Cosmos.UniqueKey 3 | 4 | open Microsoft.Azure.Cosmos 5 | 6 | type UniqueKeyPolicyBuilder () = 7 | 8 | member _.Zero () = UniqueKeyPolicy () 9 | 10 | member builder.Yield (key : UniqueKey) = builder.Key (builder.Zero (), key) 11 | 12 | member builder.Yield _ = builder.Zero () 13 | 14 | [] 15 | member _.Key (policy : UniqueKeyPolicy, key : UniqueKey) = 16 | policy.UniqueKeys.Add key 17 | policy 18 | 19 | [] 20 | member _.Keys (policy : UniqueKeyPolicy, keys : UniqueKey seq) = 21 | let policyKeys = policy.UniqueKeys 22 | keys |> Seq.iter policyKeys.Add 23 | policy 24 | 25 | let uniqueKeyPolicy = UniqueKeyPolicyBuilder () 26 | 27 | type UniqueKeyBuilder () = 28 | 29 | member _.Zero () = UniqueKey () 30 | 31 | member builder.Yield (path : string) = builder.Path (builder.Zero (), path) 32 | 33 | member builder.Yield _ = builder.Zero () 34 | 35 | [] 36 | member _.Path (key : UniqueKey, path : string) = 37 | key.Paths.Add path 38 | key 39 | 40 | [] 41 | member _.Paths (key : UniqueKey, paths : string seq) = 42 | let keyPaths = key.Paths 43 | paths |> Seq.iter keyPaths.Add 44 | key 45 | 46 | let uniqueKey = UniqueKeyBuilder () 47 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## [1.0.1] - 2025-08-08 9 | 10 | ### Fixed 11 | Count query for `CountAsync` container extension method 12 | 13 | ## [1.0.0] - 2025-05-22 14 | 15 | First release 16 | 17 | ### Added response discriminated unions for each Cosmos DB operation 18 | They allow to handle all the relevant status codes which can be considered as errors instead of exceptions. 19 | 20 | ### Added computation expressions for all Cosmos DB operations 21 | * Read 22 | * ReadMany 23 | * Create 24 | * Replace 25 | * Upsert 26 | * Delete 27 | * Patch 28 | 29 | ### Added computation expressions for unique key definition 30 | 31 | ### Added extension methods to execute operations defined with computation expressions 32 | 33 | ### Added extension methods to perform queries on Cosmos DB 34 | * Create `IAsyncEnumerable` (`TaskSeq`) from a `FeedIterator`/`IQueryable` 35 | * Provide `CancellationToken` to `TaskSeq` using `CancellableTaskSeq` module 36 | [Unreleased]: https://github.com/fsprojects/FSharp.Azure.Cosmos/compare/releases/1.0.1...HEAD 37 | [1.0.1]: https://github.com/fsprojects/FSharp.Azure.Cosmos/compare/releases/1.0.0...releases/1.0.1 38 | [1.0.0]: https://github.com/fsprojects/FSharp.Azure.Cosmos/releases/tag/releases/1.0.0 39 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build main 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | 11 | jobs: 12 | build: 13 | strategy: 14 | matrix: 15 | configuration: [Debug, Release] 16 | os: [ubuntu-latest, windows-latest, macOS-latest] 17 | runs-on: ${{ matrix.os }} 18 | 19 | steps: 20 | - uses: actions/checkout@v3 21 | - name: Setup necessary dotnet SDKs 22 | uses: actions/setup-dotnet@v3 23 | with: 24 | global-json-file: global.json 25 | dotnet-version: | 26 | 9.x 27 | 8.x 28 | 29 | - name: Build via Bash 30 | if: runner.os != 'Windows' 31 | run: | 32 | chmod +x ./build.sh 33 | ./build.sh 34 | env: 35 | CI: true 36 | CONFIGURATION: ${{ matrix.configuration }} 37 | ENABLE_COVERAGE: true 38 | - name: Build via Windows 39 | if: runner.os == 'Windows' 40 | run: ./build.cmd 41 | env: 42 | CI: true 43 | CONFIGURATION: ${{ matrix.configuration }} 44 | ENABLE_COVERAGE: true 45 | # Builds the project in a dev container 46 | build-devcontainer: 47 | runs-on: ubuntu-latest 48 | steps: 49 | 50 | - uses: actions/checkout@v3 51 | 52 | - name: Build and run dev container task 53 | uses: devcontainers/ci@v0.3 54 | with: 55 | runCmd: | 56 | chmod +x ./build.sh 57 | ./build.sh 58 | -------------------------------------------------------------------------------- /src/Cosmos/CosmosResponse.fs: -------------------------------------------------------------------------------- 1 | namespace FSharp.Azure.Cosmos 2 | 3 | open System 4 | open System.Net 5 | open Microsoft.Azure.Cosmos 6 | 7 | /// 8 | /// Represents the response from a Cosmos DB operation. 9 | /// 10 | type CosmosResponse<'T> = { 11 | HttpStatusCode : HttpStatusCode 12 | Headers : Headers 13 | Result : 'T 14 | Diagnostics : CosmosDiagnostics 15 | Exception : Exception voption 16 | } with 17 | 18 | member this.RequestCharge = this.Headers.RequestCharge 19 | member this.ActivityId = this.Headers.ActivityId 20 | member this.ETag = this.Headers.ETag 21 | 22 | module CosmosResponse = 23 | 24 | let fromItemResponse (successFn : 'T -> 'Result) (response : ItemResponse<'T>) = { 25 | HttpStatusCode = response.StatusCode 26 | Headers = response.Headers 27 | Result = successFn response.Resource 28 | Diagnostics = response.Diagnostics 29 | Exception = ValueNone 30 | } 31 | 32 | let fromFeedResponse (successFn : FeedResponse<'T> -> 'Result) (response : FeedResponse<'T>) = { 33 | HttpStatusCode = response.StatusCode 34 | Headers = response.Headers 35 | Result = successFn response 36 | Diagnostics = response.Diagnostics 37 | Exception = ValueNone 38 | } 39 | 40 | let fromException resultFn (ex : CosmosException) = { 41 | HttpStatusCode = ex.StatusCode 42 | Headers = ex.Headers 43 | Result = resultFn ex 44 | Diagnostics = ex.Diagnostics 45 | Exception = ValueSome ex 46 | } 47 | 48 | let toException<'T> (response : CosmosResponse<'T>) = response.Exception.Value 49 | -------------------------------------------------------------------------------- /.github/copilot-instructions.md: -------------------------------------------------------------------------------- 1 | We prefer the latest F# 9 features over the old syntax 2 | 3 | Prefer `voption` over `option` 4 | 5 | Prefer `task` CE over `async` CE 6 | 7 | This is how you define a non-default F# class constructor: 8 | ```fsharp 9 | type DerivedClass = 10 | inherit BaseClass 11 | 12 | new (``arguments here``) as ``created object`` 13 | = 14 | // create any objects used in the base class constructor 15 | let fieldValue = "" 16 | { 17 | inherit 18 | BaseClass (``arguments here``) 19 | } 20 | then 21 | ``created object``.otherField <- fieldValue 22 | 23 | [] 24 | val mutable otherField : FieldType 25 | ``` 26 | 27 | Always prefer F# class initializers over property assignment! **You absolutely must use F# class initializers instead of property assignment**! 28 | 29 | Class declaration: 30 | ``` F# 31 | type MyClass (someConstructorParam : string) = 32 | member ReadOnlyProperty = someConstructorParam 33 | 34 | member val MutableProperty1 = "" with get, set 35 | member val MutableProperty2 = "" with get, set 36 | ``` 37 | 38 | Wrong: 39 | ``` F# 40 | let myClass = MyClass("some value") 41 | myClass.MutableProperty1 <- "new value" 42 | myClass.MutableProperty2 <- "new value" 43 | ``` 44 | 45 | Right: 46 | ``` F# 47 | let myClass = 48 | MyClass( 49 | // constructor parameters go first without names 50 | "some value", 51 | // then mutable properties go next with names 52 | MutableProperty1 = "new value", 53 | MutableProperty2 = 54 | // operations must be placed into parentheses 55 | (5 |> string) 56 | ) 57 | ``` 58 | -------------------------------------------------------------------------------- /.github/workflows/fsdocs-gh-pages.yml: -------------------------------------------------------------------------------- 1 | name: Deploy Docs 2 | 3 | on: 4 | # Runs on pushes targeting the default branch 5 | push: 6 | branches: ["main"] 7 | 8 | # Allows you to run this workflow manually from the Actions tab 9 | workflow_dispatch: 10 | 11 | # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages 12 | permissions: 13 | contents: read 14 | pages: write 15 | id-token: write 16 | 17 | # Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued. 18 | # However, do NOT cancel in-progress runs as we want to allow these production deployments to complete. 19 | concurrency: 20 | group: "pages" 21 | cancel-in-progress: false 22 | 23 | jobs: 24 | # Build job 25 | build: 26 | runs-on: ubuntu-latest 27 | steps: 28 | - name: Checkout 29 | uses: actions/checkout@v4 30 | 31 | - name: Setup Pages 32 | uses: actions/configure-pages@v4 33 | 34 | - name: Setup necessary dotnet SDKs 35 | uses: actions/setup-dotnet@v4 36 | with: 37 | global-json-file: global.json 38 | dotnet-version: | 39 | 9.x 40 | 41 | - name: Build Docs 42 | run: | 43 | chmod +x ./build.sh 44 | ./build.sh builddocs 45 | 46 | - name: Upload artifact 47 | uses: actions/upload-pages-artifact@v3 48 | with: 49 | path: docs/ 50 | 51 | # Deployment job 52 | deploy: 53 | environment: 54 | name: github-pages 55 | url: ${{ steps.deployment.outputs.page_url }} 56 | runs-on: ubuntu-latest 57 | needs: build 58 | steps: 59 | - name: Deploy to GitHub Pages 60 | id: deployment 61 | uses: actions/deploy-pages@v4 62 | -------------------------------------------------------------------------------- /src/Cosmos/TaskSeq.fs: -------------------------------------------------------------------------------- 1 | namespace Microsoft.Azure.Cosmos 2 | 3 | open System.Linq 4 | open System.Threading 5 | open Microsoft.Azure.Cosmos 6 | open Microsoft.Azure.Cosmos.Linq 7 | 8 | module TaskSeq = 9 | 10 | /// 11 | /// Executes Cosmos DB query and asynchronously iterates Cosmos DB . 12 | /// 13 | /// Cosmos DB feed iterator 14 | let ofFeedIterator<'T> (iterator : FeedIterator<'T>) = iterator.AsAsyncEnumerable<'T> () 15 | 16 | /// 17 | /// Creates Cosmos DB from 18 | /// and asynchronously iterates it. 19 | /// 20 | /// Cosmos DB queryable 21 | let ofCosmosDbQueryable<'T> (query : IQueryable<'T>) = query.ToFeedIterator().AsAsyncEnumerable<'T> () 22 | 23 | module CancellableTaskSeq = 24 | 25 | /// 26 | /// Executes Cosmos DB query and asynchronously iterates Cosmos DB . 27 | /// 28 | /// Cancellation token 29 | /// Cosmos DB feed iterator 30 | let ofFeedIterator<'T> (cancellationToken : CancellationToken) (iterator : FeedIterator<'T>) = 31 | iterator.AsAsyncEnumerable<'T> (cancellationToken) 32 | 33 | /// 34 | /// Creates Cosmos DB from 35 | /// and asynchronously iterates it. 36 | /// 37 | /// Cancellation token 38 | /// Cosmos DB queryable 39 | let ofCosmosDbQueryable<'T> (cancellationToken : CancellationToken) (query : IQueryable<'T>) = 40 | query.ToFeedIterator().AsAsyncEnumerable<'T> (cancellationToken) 41 | -------------------------------------------------------------------------------- /docsSrc/Tutorials/Getting_Started.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Getting Started 3 | category: Tutorials 4 | categoryindex: 1 5 | index: 1 6 | --- 7 | 8 | # Getting Started 9 | 10 | ## Installation 11 | 12 | First, add the NuGet package to your project: 13 | ``` 14 | dotnet add package FSharp.Azure.Cosmos 15 | ``` 16 | ## Basic Setup 17 | 18 | Here's a minimal example to get started: 19 | ``` F# 20 | open Microsoft.Azure.Cosmos 21 | open FSharp.Azure.Cosmos 22 | ``` 23 | 24 | ``` F# 25 | // Create the client 26 | let client = CosmosClient(connectionString = "your_connection_string") 27 | 28 | // Get database and container 29 | let database = client.GetDatabase("your_database") 30 | let container = database.GetContainer("your_container") 31 | 32 | // Define a simple record type 33 | type Person = { 34 | TenantId : string 35 | Id: string 36 | Name: string 37 | Age: int 38 | } 39 | 40 | // Create an item using computation expression 41 | let createPerson = task { 42 | let person = { TenantId = "Customer1"; Id = "1"; Name = "John"; Age = 30 } 43 | let operation = create { 44 | item person 45 | partitionKey person.TenantId 46 | } 47 | match! container.ExecuteAsync operation with 48 | | Created item -> printfn "Created: %A" item 49 | | Conflict -> printfn "Item already exists" 50 | | _ -> () 51 | } 52 | 53 | // Query items using TaskSeq 54 | let queryPeople = task { 55 | let query = QueryDefinition "SELECT * FROM c WHERE c.age > 25" 56 | let! results = 57 | container.GetItemQueryIterator(query) 58 | |> TaskSeq.ofFeedIterator 59 | |> TaskSeq.toArrayAsync 60 | 61 | printfn "Found people: %A" results 62 | } 63 | ``` 64 | ## Next Steps 65 | 66 | - Check out the [How-To Guides](../How_Tos/Doing_A_Thing.html) for common scenarios 67 | - Read the [Background](../Explanations/Background.html) for deeper understanding 68 | - Browse the [API Reference](../reference/index.html) for detailed documentation 69 | -------------------------------------------------------------------------------- /docsSrc/content/fsdocs-light.css: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css2?family=Hind+Vadodara&family=Roboto+Mono&display=swap'); 2 | /*-------------------------------------------------------------------------- 3 | Formatting for page & standard document content 4 | /*--------------------------------------------------------------------------*/ 5 | 6 | :root { 7 | --fsdocs-text-color:#262626; 8 | --fsdocs-pre-border-color: #d8d8d8; 9 | --fsdocs-pre-border-color-top: #e3e3e3; 10 | --fsdocs-pre-background-color: #f3f4f7; 11 | --fsdocs-pre-color: #8e0e2b; 12 | --fsdocs-table-pre-background-color: #fff7ed; 13 | --fsdocs-table-pre-color: #837b79; 14 | 15 | --fsdocs-code-strings-color: #dd1144; 16 | --fsdocs-code-printf-color: #E0C57F; 17 | --fsdocs-code-escaped-color: #EA8675; 18 | --fsdocs-code-identifiers-color: var(--fsdocs-text-color); 19 | --fsdocs-code-module-color: #009999; 20 | --fsdocs-code-reference-color: #4974D1; 21 | --fsdocs-code-value-color: #43AEC6; 22 | --fsdocs-code-interface-color: #43AEC6; 23 | --fsdocs-code-typearg-color: #43AEC6; 24 | --fsdocs-code-disposable-color: #43AEC6; 25 | --fsdocs-code-property-color: #43AEC6; 26 | --fsdocs-code-punctuation-color: #43AEC6; 27 | --fsdocs-code-punctuation2-color: #var(--fsdocs-text-color); 28 | --fsdocs-code-function-color: #e1e1e1; 29 | --fsdocs-code-function2-color: #990000; 30 | --fsdocs-code-activepattern-color: #4ec9b0; 31 | --fsdocs-code-unioncase-color: #4ec9b0; 32 | --fsdocs-code-enumeration-color: #4ec9b0; 33 | --fsdocs-code-keywords-color: #b68015; 34 | --fsdocs-code-comment-color: #808080; 35 | --fsdocs-code-operators-color: #af75c1; 36 | --fsdocs-code-numbers-color: #009999; 37 | --fsdocs-code-linenumbers-color: #80b0b0; 38 | --fsdocs-code-mutable-color: #d1d1d1; 39 | --fsdocs-code-inactive-color: #808080; 40 | --fsdocs-code-preprocessor-color: #af75c1; 41 | --fsdocs-code-fsioutput-color: #808080; 42 | --fsdocs-code-tooltip-color: #d1d1d1; 43 | } 44 | -------------------------------------------------------------------------------- /src/Cosmos/FSharp.Azure.Cosmos.fsproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | $(AssemblyBaseName) 6 | true 7 | true 8 | True 9 | 10 | 11 | 12 | FSharp.Azure.Cosmos 13 | FSharp.Azure.Cosmos 14 | F# API for using Microsoft Azure Cosmos DB service via NoSQL API 15 | Provides extension methods for the FeedIterator and computation expressions to build operations 16 | 17 | 18 | 19 | true 20 | true 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | all 45 | runtime; build; native; contentfiles; analyzers; buildtransitive 46 | 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /docsSrc/content/navbar-fixed-left.css: -------------------------------------------------------------------------------- 1 | /* CSS for Bootstrap 5 Fixed Left Sidebar Navigation */ 2 | 3 | 4 | 5 | @media (min-width: 992px){ 6 | 7 | body { 8 | padding-left: 300px; 9 | padding-right: 60px; 10 | } 11 | 12 | #fsdocs-logo { 13 | width:140px; 14 | height:140px; 15 | margin:10px 0px 0px 0px; 16 | border-style:none; 17 | } 18 | 19 | 20 | nav.navbar { 21 | position: fixed; 22 | left: 0; 23 | width: 300px; 24 | bottom: 0; 25 | top: 0; 26 | overflow-y: auto; 27 | overflow-x: hidden; 28 | display: block; 29 | border-right: 1px solid #cecece; 30 | } 31 | 32 | nav.navbar>.container { 33 | flex-direction: column; 34 | padding: 0; 35 | } 36 | 37 | nav.navbar .navbar-nav { 38 | flex-direction: column; 39 | } 40 | nav.navbar .navbar-collapse { 41 | width: 100%; 42 | } 43 | 44 | nav.navbar .navbar-nav { 45 | width: 100%; 46 | } 47 | 48 | nav.navbar .navbar-nav .dropdown-menu { 49 | position: static; 50 | display: block; 51 | } 52 | 53 | nav.navbar .dropdown { 54 | margin-bottom: 5px; 55 | font-size: 14px; 56 | } 57 | 58 | nav.navbar .dropdown-item { 59 | white-space: normal; 60 | font-size: 14px; 61 | vertical-align: middle; 62 | } 63 | 64 | nav.navbar .dropdown-item img { 65 | margin-right: 5px; 66 | } 67 | 68 | nav.navbar .dropdown-toggle { 69 | cursor: default; 70 | } 71 | 72 | nav.navbar .dropdown-menu { 73 | border-radius: 0; 74 | border-left: 0; 75 | border-right: 0; 76 | } 77 | 78 | nav.navbar .dropdown-toggle:not(#bd-theme)::after { 79 | display: none; 80 | } 81 | 82 | .dropdown-menu[data-bs-popper] { 83 | top: auto; 84 | left: auto; 85 | margin-top: auto; 86 | } 87 | 88 | .nav-link:focus, .nav-link:hover { 89 | color: auto; 90 | } 91 | } -------------------------------------------------------------------------------- /Directory.Packages.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /docsSrc/content/fsdocs-dark.css: -------------------------------------------------------------------------------- 1 | @import url('https://raw.githubusercontent.com/tonsky/FiraCode/fixed/distr/fira_code.css'); 2 | @import url('https://fonts.googleapis.com/css2?family=Hind+Vadodara&family=Roboto+Mono&display=swap'); 3 | /*-------------------------------------------------------------------------- 4 | Formatting for page & standard document content 5 | /*--------------------------------------------------------------------------*/ 6 | 7 | :root { 8 | --fsdocs-text-color:#d1d1d1; 9 | --fsdocs-pre-border-color: #000000; 10 | --fsdocs-pre-border-color-top: #070707; 11 | --fsdocs-pre-background-color: #1E1E1E; 12 | --fsdocs-pre-color: #e2e2e2; 13 | --fsdocs-table-pre-background-color: #1d1d1d; 14 | --fsdocs-table-pre-color: #c9c9c9; 15 | 16 | --fsdocs-code-strings-color: #ea9a75; 17 | --fsdocs-code-printf-color: #E0C57F; 18 | --fsdocs-code-escaped-color: #EA8675; 19 | --fsdocs-code-identifiers-color: var(--fsdocs-text-color); 20 | --fsdocs-code-module-color: #43AEC6; 21 | --fsdocs-code-reference-color: #6a8dd8; 22 | --fsdocs-code-value-color: #43AEC6; 23 | --fsdocs-code-interface-color: #43AEC6; 24 | --fsdocs-code-typearg-color: #43AEC6; 25 | --fsdocs-code-disposable-color: #2f798a; 26 | --fsdocs-code-property-color: #43AEC6; 27 | --fsdocs-code-punctuation-color: #43AEC6; 28 | --fsdocs-code-punctuation2-color: #e1e1e1; 29 | --fsdocs-code-function-color: #e1e1e1; 30 | --fsdocs-code-function2-color: #43AEC6; 31 | --fsdocs-code-activepattern-color: #4ec9b0; 32 | --fsdocs-code-unioncase-color: #4ec9b0; 33 | --fsdocs-code-enumeration-color: #4ec9b0; 34 | --fsdocs-code-keywords-color: #2248c4; 35 | --fsdocs-code-comment-color: #329215; 36 | --fsdocs-code-operators-color: #af75c1; 37 | --fsdocs-code-numbers-color: #96C71D; 38 | --fsdocs-code-linenumbers-color: #80b0b0; 39 | --fsdocs-code-mutable-color: #997f0c; 40 | --fsdocs-code-inactive-color: #808080; 41 | --fsdocs-code-preprocessor-color: #af75c1; 42 | --fsdocs-code-fsioutput-color: #808080; 43 | --fsdocs-code-tooltip-color: #d1d1d1; 44 | } 45 | 46 | 47 | .fsdocs-source-link img { 48 | -webkit-filter: grayscale(100%) brightness(1) invert(1); /* Safari 6.0 - 9.0 */ 49 | filter: grayscale(100%) brightness(1) invert(1); 50 | } -------------------------------------------------------------------------------- /.github/workflows/publish_ci.yml: -------------------------------------------------------------------------------- 1 | name: Publish to GitHub 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | env: 9 | CONFIGURATION: Release 10 | 11 | jobs: 12 | build: 13 | # Sets permissions of the GITHUB_TOKEN to allow release creating 14 | permissions: 15 | packages: write 16 | environment: 17 | name: nuget 18 | runs-on: ubuntu-latest 19 | 20 | steps: 21 | - uses: actions/checkout@v3 22 | 23 | - name: Setup necessary dotnet SDKs 24 | uses: actions/setup-dotnet@v3 25 | with: 26 | global-json-file: global.json 27 | dotnet-version: | 28 | 9.x 29 | 8.x 30 | 31 | - name: Add the GitHub source 32 | run: dotnet nuget add source --name "github.com" "https://nuget.pkg.github.com/fsprojects/index.json" 33 | 34 | - name: Ensure NuGet package source mapping 35 | shell: pwsh 36 | run: | 37 | $nugetConfigPath = "$HOME/.nuget/NuGet/NuGet.Config" 38 | [xml]$nugetConfig = Get-Content $nugetConfigPath 39 | 40 | $packageSourceMapping = $nugetConfig.configuration.packageSourceMapping 41 | if ($packageSourceMapping -ne $null) { 42 | $packageSourceMapping.RemoveAll() 43 | } else { 44 | $packageSourceMapping = $nugetConfig.CreateElement("packageSourceMapping") 45 | $nugetConfig.configuration.AppendChild($packageSourceMapping) 46 | } 47 | 48 | $nugetSource = $nugetConfig.CreateElement("packageSource") 49 | $nugetSource.SetAttribute("key", "nuget.org") 50 | $nugetPattern = $nugetConfig.CreateElement("package") 51 | $nugetPattern.SetAttribute("pattern", "*") 52 | $nugetSource.AppendChild($nugetPattern) 53 | $packageSourceMapping.AppendChild($nugetSource) 54 | 55 | $nugetConfig.Save($nugetConfigPath) 56 | 57 | - name: Publish to GitHub 58 | env: 59 | NUGET_TOKEN: ${{ secrets.NUGET_TOKEN }} 60 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 61 | FAKE_DETAILED_ERRORS: true 62 | ENABLE_COVERAGE: false # AltCover doesn't work with Release builds, reports lower coverage than actual 63 | run: | 64 | chmod +x ./build.sh 65 | ./build.sh "PublishToGitHub" 66 | -------------------------------------------------------------------------------- /Directory.Build.props: -------------------------------------------------------------------------------- 1 | 5 | 6 | 7 | 8 | F#;FSharp;Cosmos;CosmosDB;Cosmos DB;Cosmos SQL;Core API 9 | https://github.com/fsprojects/FSharp.Azure.Cosmos 10 | false 11 | LICENSE 12 | README.md 13 | logo.png 14 | 15 | git 16 | fsprojects, XperiAndri, dim-37, mariianazarova 17 | https://github.com/fsprojects/FSharp.Azure.Cosmos 18 | 19 | true 20 | 21 | true 22 | snupkg 23 | true 24 | 25 | 26 | 27 | FSharp.Azure.Cosmos 28 | 9.0 29 | enable 30 | true 31 | false 32 | true 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 49 | 50 | -------------------------------------------------------------------------------- /docsSrc/content/theme-toggle.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Color mode toggler for Bootstrap's docs (https://getbootstrap.com/) 3 | * Copyright 2011-2022 The Bootstrap Authors 4 | * Licensed under the Creative Commons Attribution 3.0 Unported License. 5 | */ 6 | 7 | (() => { 8 | 'use strict' 9 | 10 | const storedTheme = localStorage.getItem('theme') 11 | 12 | const getPreferredTheme = () => { 13 | if (storedTheme) { 14 | return storedTheme 15 | } 16 | 17 | return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light' 18 | } 19 | 20 | const setTheme = function (theme) { 21 | const fsdocsTheme = document.getElementById("fsdocs-theme") 22 | const re = /fsdocs-.*.css/ 23 | if (theme === 'auto' && window.matchMedia('(prefers-color-scheme: dark)').matches) { 24 | document.documentElement.setAttribute('data-bs-theme', 'dark') 25 | fsdocsTheme.setAttribute("href", fsdocsTheme.getAttribute("href").replace(re,"fsdocs-dark.css")) 26 | 27 | } else { 28 | document.documentElement.setAttribute('data-bs-theme', theme) 29 | 30 | fsdocsTheme.setAttribute("href", fsdocsTheme.getAttribute("href").replace(re,`fsdocs-${theme}.css`)) 31 | } 32 | } 33 | 34 | setTheme(getPreferredTheme()) 35 | 36 | const showActiveTheme = theme => { 37 | const activeThemeIcon = document.getElementById('theme-icon-active') 38 | const btnToActive = document.querySelector(`[data-bs-theme-value="${theme}"]`) 39 | const svgOfActiveBtn = btnToActive.querySelector('i').getAttribute('class') 40 | 41 | document.querySelectorAll('[data-bs-theme-value]').forEach(element => { 42 | element.classList.remove('active') 43 | }) 44 | 45 | btnToActive.classList.add('active') 46 | activeThemeIcon.setAttribute('class', svgOfActiveBtn) 47 | } 48 | 49 | window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => { 50 | if (storedTheme !== 'light' || storedTheme !== 'dark') { 51 | setTheme(getPreferredTheme()) 52 | } 53 | }) 54 | 55 | window.addEventListener('DOMContentLoaded', () => { 56 | showActiveTheme(getPreferredTheme()) 57 | 58 | document.querySelectorAll('[data-bs-theme-value]') 59 | .forEach(toggle => { 60 | toggle.addEventListener('click', () => { 61 | const theme = toggle.getAttribute('data-bs-theme-value') 62 | localStorage.setItem('theme', theme) 63 | setTheme(theme) 64 | showActiveTheme(theme) 65 | }) 66 | }) 67 | }) 68 | })() -------------------------------------------------------------------------------- /docsSrc/index.md: -------------------------------------------------------------------------------- 1 | # FSharp.Azure.Cosmos 2 | 3 | F# idiomatic wrapper for [Azure Cosmos DB SDK](https://github.com/Azure/azure-cosmos-dotnet-v3) that provides strongly-typed responses and computation expressions for all Cosmos DB operations. 4 | 5 | ## Key Features 6 | 7 | - **Type-Safe Responses**: All Cosmos DB operations return discriminated unions for proper error handling 8 | - **F# Computation Expressions**: Fluent syntax for database operations 9 | 10 | --- 11 | 12 |
    13 |
    14 |
    15 |
    16 | The FSharp.Azure.Cosmos library can be installed from NuGet: 17 |
    PM> Install-Package FSharp.Azure.Cosmos
    18 |
    19 |
    20 |
    21 |
    22 | 23 | --- 24 | 25 |
    26 |
    27 |
    28 |
    29 |
    Tutorials
    30 |

    Step-by-step guide to get started with FSharp.Azure.Cosmos.

    31 |
    32 | 35 |
    36 |
    37 |
    38 |
    39 |
    40 |
    How-To Guides
    41 |

    Guides you through the steps involved in addressing key problems and use-cases.

    42 |
    43 | 46 |
    47 |
    48 |
    49 |
    50 |
    51 |
    Explanations
    52 |

    Discusses key topics and concepts at a fairly high level and provide useful background information and explanation..

    53 |
    54 | 57 |
    58 |
    59 |
    60 |
    61 |
    62 |
    Api Reference
    63 |

    Contain technical reference for APIs.

    64 |
    65 | 68 |
    69 |
    70 |
    71 | -------------------------------------------------------------------------------- /docsSrc/Explanations/Background.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Background 3 | category: Explanations 4 | categoryindex: 3 5 | index: 1 6 | --- 7 | 8 | # Background 9 | 10 | ## Cosmos DB and F# Integration 11 | 12 | Azure Cosmos DB is Microsoft's globally distributed, multi-model database service. While the .NET SDK provides excellent functionality, using it directly from F# can lead to code that doesn't feel idiomatic to the language. 13 | Also `RequestOptions` pattern is not the best option is Cosmos DB operation features discoverability, where F# computation expressions shine and simplify operation definitions. 14 | 15 | FSharp.Azure.Cosmos addresses this by providing F#-first abstractions over the Cosmos DB SDK, focusing on: 16 | 17 | 1. **Type-Safe Responses**: Using discriminated unions to handle response status codes as values instead of exceptions 18 | 2. **F# Computation Expressions**: Natural syntax for database operations like read, write, update, and delete 19 | 3. **Resource Management**: Better control over database resources through F# idioms 20 | 21 | ## Response Handling 22 | 23 | Traditional .NET exception handling can be verbose and error-prone: 24 | ``` F# 25 | try 26 | let! response = container.CreateItemAsync(item) 27 | // Handle success 28 | with 29 | | :? CosmosException as ex when ex.StatusCode = HttpStatusCode.Conflict -> 30 | // Handle conflict 31 | | :? CosmosException as ex when ex.StatusCode = HttpStatusCode.TooManyRequests -> 32 | // Handle rate limiting 33 | ``` 34 | FSharp.Azure.Cosmos transforms this into a more F#-like pattern: 35 | ``` F# 36 | let! response = container.ExecuteAsync operation 37 | match response with 38 | | Created item -> // Handle success 39 | | Conflict -> // Handle conflict 40 | | TooManyRequests -> // Handle rate limiting 41 | ``` 42 | ## Computation Expressions 43 | 44 | The library provides computation expressions for all major operations: 45 | ``` F# 46 | let createOperation = create { 47 | item person 48 | partitionKey person.TenantId 49 | consistencyLevel ConsistencyLevel.Eventual 50 | preTrigger "preTrigger" 51 | postTrigger "postTrigger" 52 | } 53 | ``` 54 | This approach: 55 | - Simplifies request options definition 56 | - Allows to define an operation template and reuse it 57 | 58 | ## Query Extensions 59 | 60 | Modern applications often need to handle large result sets efficiently. The library provides extensions for working with `FeedIterator` and `IQueryable` results using `TaskSeq`-based iteration 61 | ``` F# 62 | let! people = 63 | container 64 | .GetItemLinqQueryable() 65 | .Where(fun p -> p.Name = name && p.TenantId = tenantId) 66 | |> CancellableTaskSeq.ofAsyncEnumerable cancellationToken 67 | |> TaskSeq.toListAsync 68 | ``` 69 | This approach ensures efficient query execution and resource management in F# while maintaining idiomatic practices. 70 | -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dotnet", 3 | // Set the build context one level higher so we can grab metadata like global.json 4 | "context": "..", 5 | "dockerFile": "Dockerfile", 6 | "forwardPorts": [ 7 | 0 8 | ], 9 | "features": { 10 | // https://github.com/devcontainers/features/blob/main/src/common-utils/README.md 11 | "ghcr.io/devcontainers/features/common-utils:2": { 12 | "installZsh": true, 13 | "installOhMyZshConfig": true, 14 | "configureZshAsDefaultShell": true, 15 | "username": "vscode", 16 | "userUid": "1000", 17 | "userGid": "1000", 18 | "upgradePackages": true 19 | }, 20 | // https://github.com/devcontainers/features/blob/main/src/github-cli/README.md 21 | "ghcr.io/devcontainers/features/github-cli:1": {}, 22 | // https://github.com/devcontainers-contrib/features/blob/main/src/starship/README.md 23 | "ghcr.io/devcontainers-contrib/features/starship:1": {}, 24 | // https://github.com/devcontainers/features/blob/main/src/dotnet/README.md 25 | "ghcr.io/devcontainers/features/dotnet:2": { 26 | "version": "9.0", 27 | "additionalVersions": "8.0" 28 | } 29 | }, 30 | "overrideFeatureInstallOrder": [ 31 | "ghcr.io/devcontainers/features/common-utils", 32 | "ghcr.io/devcontainers/features/github-cli", 33 | "ghcr.io/devcontainers-contrib/features/starship", 34 | "ghcr.io/devcontainers/features/dotnet" 35 | ], 36 | "customizations": { 37 | "vscode": { 38 | // Add the IDs of extensions you want installed when the container is created. 39 | "extensions": [ 40 | "ms-dotnettools.csharp", 41 | "Ionide.Ionide-fsharp", 42 | "tintoy.msbuild-project-tools", 43 | "ionide.ionide-paket", 44 | "usernamehw.errorlens", 45 | "alefragnani.Bookmarks", 46 | "oderwat.indent-rainbow", 47 | "vscode-icons-team.vscode-icons", 48 | "EditorConfig.EditorConfig", 49 | "ms-azuretools.vscode-docker", 50 | "GitHub.vscode-pull-request-github", 51 | "github.vscode-github-actions" 52 | ], 53 | "settings": { 54 | "terminal.integrated.defaultProfile.linux": "zsh", 55 | "csharp.suppressDotnetInstallWarning": true 56 | } 57 | } 58 | }, 59 | "remoteUser": "vscode", 60 | "containerUser": "vscode", 61 | "containerEnv": { 62 | // Expose the local environment variable to the container 63 | // They are used for releasing and publishing from the container 64 | "GITHUB_TOKEN": "${localEnv:GITHUB_TOKEN}" 65 | }, 66 | "onCreateCommand": { 67 | "enable-starship": "echo 'eval \"$(starship init zsh)\"' >> ~/.zshrc" 68 | }, 69 | "postAttachCommand": { 70 | "restore": "dotnet tool restore && dotnet restore" 71 | }, 72 | "waitFor": "updateContentCommand" 73 | } 74 | -------------------------------------------------------------------------------- /Directory.Build.targets: -------------------------------------------------------------------------------- 1 | 5 | 6 | 7 | 8 | <_BuildProjBaseIntermediateOutputPath>$(MSBuildThisFileDirectory)build/obj/ 9 | <_DotnetToolManifestFile>$(MSBuildThisFileDirectory).config/dotnet-tools.json 10 | <_DotnetToolRestoreOutputFile>$(_BuildProjBaseIntermediateOutputPath)/dotnet-tool-restore-$(NETCoreSdkVersion)-$(OS) 11 | <_DotnetFantomasOutputFile>$(BaseIntermediateOutputPath)dotnet-fantomas-msbuild-$(NETCoreSdkVersion)-$(OS) 12 | 13 | 14 | 15 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /docsSrc/content/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 23 | 26 | 27 | 28 | 29 | 30 | 31 | 33 | 34 | 35 | 36 | 38 | 39 | 40 | 41 | 42 | 43 | 45 | 48 | 49 | 50 | 51 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /src/Cosmos/ReadMany.fs: -------------------------------------------------------------------------------- 1 | [] 2 | module FSharp.Azure.Cosmos.ReadMany 3 | 4 | open System 5 | open System.Collections.Immutable 6 | open Microsoft.Azure.Cosmos 7 | 8 | [] 9 | type ReadManyOperation<'T> = { 10 | Items : ValueTuple ImmutableArray 11 | RequestOptions : ReadManyRequestOptions | null 12 | } 13 | 14 | type ReadManyBuilder<'T> () = 15 | member _.Yield _ = { Items = ImmutableArray.Empty; RequestOptions = null } : ReadManyOperation<'T> 16 | 17 | /// Sets the item being created 18 | [] 19 | member _.Item (state : ReadManyOperation<_>, id, partitionKey : PartitionKey) = 20 | let items = state.Items.Add (id, partitionKey) 21 | { state with Items = items } 22 | 23 | /// Sets the item being created 24 | [] 25 | member builder.Item (state : ReadManyOperation<_>, id, partitionKey : string) = 26 | builder.Item (state, id, PartitionKey partitionKey) 27 | 28 | /// Sets the items being created 29 | [] 30 | member _.Items (state : ReadManyOperation<_>, items : ValueTuple seq) = 31 | let items = state.Items.AddRange items 32 | { state with Items = items } 33 | 34 | /// Sets the items being created 35 | [] 36 | member builder.Items (state : ReadManyOperation<_>, items : ValueTuple seq) = 37 | builder.Items ( 38 | state, 39 | items 40 | |> Seq.map (fun struct (id, partitionKey) -> struct (id, PartitionKey partitionKey)) 41 | ) 42 | 43 | /// Sets the request options 44 | [] 45 | member _.RequestOptions (state : ReadManyOperation<_>, options : ReadManyRequestOptions) = { 46 | state with 47 | RequestOptions = options 48 | } 49 | 50 | /// Sets the eTag to 51 | [] 52 | member _.ETag (state : ReadManyOperation<_>, eTag : string) = 53 | match state.RequestOptions with 54 | | null -> 55 | let options = ReadManyRequestOptions (IfNoneMatchEtag = eTag) 56 | { state with RequestOptions = options } 57 | | options -> 58 | options.IfNoneMatchEtag <- eTag 59 | state 60 | 61 | // ------------------------------------------- Request options ------------------------------------------- 62 | /// Sets the operation 63 | [] 64 | member _.ConsistencyLevel (state : ReadManyOperation<_>, consistencyLevel : ConsistencyLevel Nullable) = 65 | match state.RequestOptions with 66 | | null -> 67 | let options = ReadManyRequestOptions (ConsistencyLevel = consistencyLevel) 68 | { state with RequestOptions = options } 69 | | options -> 70 | options.ConsistencyLevel <- consistencyLevel 71 | state 72 | 73 | /// Sets the session token 74 | [] 75 | member _.SessionToken (state : ReadManyOperation<_>, sessionToken : string) = 76 | match state.RequestOptions with 77 | | null -> 78 | let options = ReadManyRequestOptions (SessionToken = sessionToken) 79 | { state with RequestOptions = options } 80 | | options -> 81 | options.SessionToken <- sessionToken 82 | state 83 | 84 | let readMany<'T> = ReadManyBuilder<'T> () 85 | 86 | // https://docs.microsoft.com/en-us/rest/api/cosmos-db/http-status-codes-for-cosmosdb 87 | 88 | /// Represents the result of a read operation. 89 | type ReadManyResult<'t> = 90 | | Ok of 't // 200 91 | | NotModified // 204 92 | | IncompatibleConsistencyLevel of ResponseBody : string // 400 93 | | NotFound of ResponseBody : string // 404 94 | 95 | open System.Net 96 | 97 | module CosmosException = 98 | 99 | let toReadResult badRequestCtor notFoundResultCtor (ex : CosmosException) = 100 | match ex.StatusCode with 101 | | HttpStatusCode.BadRequest -> badRequestCtor ex.ResponseBody 102 | | HttpStatusCode.NotFound -> notFoundResultCtor ex.ResponseBody 103 | | _ -> raise ex 104 | 105 | open System.Runtime.InteropServices 106 | open System.Threading 107 | open System.Threading.Tasks 108 | open CosmosException 109 | 110 | type Microsoft.Azure.Cosmos.Container with 111 | 112 | /// 113 | /// Executes a read many operation and returns . 114 | /// 115 | /// Read many operation 116 | /// Cancellation token 117 | member container.PlainExecuteAsync<'T> 118 | (operation : ReadManyOperation<'T>, [] cancellationToken : CancellationToken) 119 | = 120 | container.ReadManyItemsAsync<'T> (operation.Items, operation.RequestOptions, cancellationToken = cancellationToken) 121 | 122 | /// 123 | /// Executes a read many operation, transforms success or failure, and returns . 124 | /// 125 | /// Read operation 126 | /// Result transform if success 127 | /// Error transform if failure 128 | /// Cancellation token 129 | member container.ExecuteAsync<'T, 'Result> 130 | (operation : ReadManyOperation<'T>, success, failure, [] cancellationToken : CancellationToken) 131 | : Task> 132 | = 133 | task { 134 | try 135 | let! result = container.PlainExecuteAsync (operation, cancellationToken) 136 | return CosmosResponse.fromFeedResponse (success) result 137 | with HandleException ex -> 138 | return CosmosResponse.fromException (failure) ex 139 | } 140 | 141 | /// 142 | /// Executes a read many operation and returns . 143 | /// 144 | /// Read operation 145 | /// Cancellation token 146 | member container.ExecuteAsync<'T> (operation : ReadManyOperation<'T>, [] cancellationToken : CancellationToken) = 147 | let successFn result : ReadManyResult> = 148 | if Object.Equals (result, Unchecked.defaultof<'T>) then 149 | ReadManyResult.NotModified 150 | else 151 | ReadManyResult.Ok result 152 | 153 | container.ExecuteAsync<'T, ReadManyResult>> ( 154 | operation, 155 | successFn, 156 | toReadResult ReadManyResult.IncompatibleConsistencyLevel ReadManyResult.NotFound, 157 | cancellationToken 158 | ) 159 | -------------------------------------------------------------------------------- /src/Cosmos/Create.fs: -------------------------------------------------------------------------------- 1 | [] 2 | module FSharp.Azure.Cosmos.Create 3 | 4 | open System 5 | open Microsoft.Azure.Cosmos 6 | 7 | [] 8 | type CreateOperation<'T> = { 9 | Item : 'T 10 | PartitionKey : PartitionKey voption 11 | RequestOptions : ItemRequestOptions 12 | } 13 | 14 | type CreateBuilder<'T> (enableContentResponseOnWrite : bool) = 15 | member _.Yield _ = 16 | { 17 | Item = Unchecked.defaultof<_> 18 | PartitionKey = ValueNone 19 | RequestOptions = ItemRequestOptions (EnableContentResponseOnWrite = enableContentResponseOnWrite) 20 | } 21 | : CreateOperation<'T> 22 | 23 | /// Sets the item being created 24 | [] 25 | member _.Item (state : CreateOperation<_>, item) = { state with Item = item } 26 | 27 | /// Sets the partition key 28 | [] 29 | member _.PartitionKey (state : CreateOperation<_>, partitionKey : PartitionKey) = { 30 | state with 31 | PartitionKey = ValueSome partitionKey 32 | } 33 | 34 | /// Sets the partition key 35 | [] 36 | member _.PartitionKey (state : CreateOperation<_>, partitionKey : string) = { 37 | state with 38 | PartitionKey = ValueSome (PartitionKey partitionKey) 39 | } 40 | 41 | /// Sets the request options 42 | [] 43 | member _.RequestOptions (state : CreateOperation<_>, options : ItemRequestOptions) = 44 | options.EnableContentResponseOnWrite <- enableContentResponseOnWrite 45 | { state with RequestOptions = options } 46 | 47 | // ------------------------------------------- Request options ------------------------------------------- 48 | /// Sets the operation 49 | [] 50 | member _.ConsistencyLevel (state : CreateOperation<_>, consistencyLevel : ConsistencyLevel Nullable) = 51 | state.RequestOptions.ConsistencyLevel <- consistencyLevel 52 | state 53 | 54 | /// Sets if the response should include the content of the item after the operation 55 | [] 56 | member _.EnableContentResponseOnWrite (state : CreateOperation<_>, enableContentResponseOnWrite : bool) = 57 | state.RequestOptions.EnableContentResponseOnWrite <- enableContentResponseOnWrite 58 | state 59 | 60 | /// Sets the indexing directive 61 | [] 62 | member _.IndexingDirective (state : CreateOperation<_>, indexingDirective : IndexingDirective Nullable) = 63 | state.RequestOptions.IndexingDirective <- indexingDirective 64 | state 65 | 66 | /// Adds a trigger to be invoked before the operation 67 | [] 68 | member _.PreTrigger (state : CreateOperation<_>, trigger : string) = 69 | state.RequestOptions.AddPreTrigger trigger 70 | state 71 | 72 | /// Adds triggers to be invoked before the operation 73 | [] 74 | member _.PreTriggers (state : CreateOperation<_>, triggers : seq) = 75 | state.RequestOptions.AddPreTriggers triggers 76 | state 77 | 78 | /// Adds a trigger to be invoked after the operation 79 | [] 80 | member _.PostTrigger (state : CreateOperation<_>, trigger : string) = 81 | state.RequestOptions.AddPostTrigger trigger 82 | state 83 | 84 | /// Adds triggers to be invoked after the operation 85 | [] 86 | member _.PostTriggers (state : CreateOperation<_>, triggers : seq) = 87 | state.RequestOptions.AddPostTriggers triggers 88 | state 89 | 90 | /// Sets the session token 91 | [] 92 | member _.SessionToken (state : CreateOperation<_>, sessionToken : string) = 93 | state.RequestOptions.SessionToken <- sessionToken 94 | state 95 | 96 | let create<'T> = CreateBuilder<'T> (false) 97 | let createAndRead<'T> = CreateBuilder<'T> (true) 98 | 99 | // https://docs.microsoft.com/en-us/rest/api/cosmos-db/http-status-codes-for-cosmosdb 100 | 101 | /// Represents the result of a create operation. 102 | type CreateResult<'T> = 103 | | Ok of 'T // 201 104 | | BadRequest of ResponseBody : string // 400 105 | /// Forbidden 106 | | PartitionStorageLimitReached of ResponseBody : string // 403 107 | /// Conflict 108 | | IdAlreadyExists of ResponseBody : string // 409 109 | | EntityTooLarge of ResponseBody : string // 413 110 | | TooManyRequests of ResponseBody : string * RetryAfter : TimeSpan voption // 429 111 | 112 | open System.Net 113 | 114 | module CosmosException = 115 | 116 | let toCreateResult (ex : CosmosException) = 117 | match ex.StatusCode with 118 | | HttpStatusCode.BadRequest -> CreateResult.BadRequest ex.ResponseBody 119 | | HttpStatusCode.Forbidden -> CreateResult.PartitionStorageLimitReached ex.ResponseBody 120 | | HttpStatusCode.Conflict -> CreateResult.IdAlreadyExists ex.ResponseBody 121 | | HttpStatusCode.RequestEntityTooLarge -> CreateResult.EntityTooLarge ex.ResponseBody 122 | | HttpStatusCode.TooManyRequests -> 123 | CreateResult.TooManyRequests (ex.ResponseBody, ex.RetryAfter |> ValueOption.ofNullable) 124 | | _ -> raise ex 125 | 126 | open System.Runtime.InteropServices 127 | open System.Threading 128 | open System.Threading.Tasks 129 | open CosmosException 130 | 131 | type Microsoft.Azure.Cosmos.Container with 132 | 133 | /// 134 | /// Executes a create operation and returns . 135 | /// 136 | /// Create operation. 137 | /// Cancellation token. 138 | member container.PlainExecuteAsync<'T> (operation : CreateOperation<'T>, [] cancellationToken : CancellationToken) = 139 | container.CreateItemAsync<'T> ( 140 | operation.Item, 141 | operation.PartitionKey |> ValueOption.toNullable, 142 | operation.RequestOptions, 143 | cancellationToken = cancellationToken 144 | ) 145 | 146 | /// 147 | /// Executes a create operation and returns . 148 | /// 149 | /// Create operation. 150 | /// Cancellation token. 151 | member container.ExecuteAsync<'T> 152 | (operation : CreateOperation<'T>, [] cancellationToken : CancellationToken) 153 | : Task>> 154 | = 155 | task { 156 | try 157 | let! response = container.PlainExecuteAsync (operation, cancellationToken) 158 | return CosmosResponse.fromItemResponse CreateResult.Ok response 159 | with HandleException ex -> 160 | return CosmosResponse.fromException toCreateResult ex 161 | } 162 | -------------------------------------------------------------------------------- /src/Cosmos/Delete.fs: -------------------------------------------------------------------------------- 1 | [] 2 | module FSharp.Azure.Cosmos.Delete 3 | 4 | open Microsoft.Azure.Cosmos 5 | 6 | [] 7 | type DeleteOperation = { 8 | Id : string 9 | PartitionKey : PartitionKey 10 | RequestOptions : ItemRequestOptions voption 11 | } 12 | 13 | open System 14 | 15 | type DeleteBuilder () = 16 | 17 | member _.Yield _ = 18 | { 19 | Id = String.Empty 20 | PartitionKey = PartitionKey.None 21 | RequestOptions = ValueNone 22 | } 23 | : DeleteOperation 24 | 25 | /// Sets the item being creeated 26 | [] 27 | member _.Id (state : DeleteOperation, id) = { state with Id = id } 28 | 29 | /// Sets the partition key 30 | [] 31 | member _.PartitionKey (state : DeleteOperation, partitionKey : PartitionKey) = { state with PartitionKey = partitionKey } 32 | 33 | /// Sets the partition key 34 | [] 35 | member _.PartitionKey (state : DeleteOperation, partitionKey : string) = { 36 | state with 37 | PartitionKey = PartitionKey partitionKey 38 | } 39 | 40 | /// Sets the request options 41 | [] 42 | member _.RequestOptions (state : DeleteOperation, options : ItemRequestOptions) = { 43 | state with 44 | RequestOptions = ValueSome options 45 | } 46 | 47 | /// Sets the eTag to 48 | [] 49 | member _.ETag (state : DeleteOperation, eTag : string) = 50 | match state.RequestOptions with 51 | | ValueSome requestOptions -> 52 | requestOptions.IfNoneMatchEtag <- eTag 53 | state 54 | | ValueNone -> 55 | let options = ItemRequestOptions (IfNoneMatchEtag = eTag) 56 | { state with RequestOptions = ValueSome options } 57 | 58 | // ------------------------------------------- Request options ------------------------------------------- 59 | /// Sets the operation 60 | [] 61 | member _.ConsistencyLevel (state : DeleteOperation, consistencyLevel : ConsistencyLevel Nullable) = 62 | match state.RequestOptions with 63 | | ValueSome requestOptions -> 64 | requestOptions.ConsistencyLevel <- consistencyLevel 65 | state 66 | | ValueNone -> 67 | let options = ItemRequestOptions (ConsistencyLevel = consistencyLevel) 68 | { state with RequestOptions = ValueSome options } 69 | 70 | /// Sets if the response should include the content of the item after the operation 71 | [] 72 | member _.EnableContentResponseOnWrite (state : DeleteOperation, enableContentResponseOnWrite : bool) = 73 | match state.RequestOptions with 74 | | ValueSome requestOptions -> 75 | requestOptions.EnableContentResponseOnWrite <- enableContentResponseOnWrite 76 | state 77 | | ValueNone -> 78 | let options = ItemRequestOptions (EnableContentResponseOnWrite = enableContentResponseOnWrite) 79 | { state with RequestOptions = ValueSome options } 80 | 81 | /// Sets the indexing directive 82 | [] 83 | member _.IndexingDirective (state : DeleteOperation, indexingDirective : IndexingDirective Nullable) = 84 | match state.RequestOptions with 85 | | ValueSome requestOptions -> 86 | requestOptions.IndexingDirective <- indexingDirective 87 | state 88 | | ValueNone -> 89 | let options = ItemRequestOptions (IndexingDirective = indexingDirective) 90 | { state with RequestOptions = ValueSome options } 91 | 92 | /// Adds a trigger to be invoked before the operation 93 | [] 94 | member _.PreTrigger (state : DeleteOperation, trigger : string) = 95 | match state.RequestOptions with 96 | | ValueSome requestOptions -> 97 | requestOptions.AddPreTrigger trigger 98 | state 99 | | ValueNone -> 100 | let options = ItemRequestOptions () 101 | options.AddPreTrigger trigger 102 | { state with RequestOptions = ValueSome options } 103 | 104 | /// Adds triggers to be invoked before the operation 105 | [] 106 | member _.PreTriggers (state : DeleteOperation, triggers : seq) = 107 | match state.RequestOptions with 108 | | ValueSome requestOptions -> 109 | requestOptions.AddPreTriggers triggers 110 | state 111 | | ValueNone -> 112 | let options = ItemRequestOptions () 113 | options.AddPreTriggers triggers 114 | { state with RequestOptions = ValueSome options } 115 | 116 | /// Adds a trigger to be invoked after the operation 117 | [] 118 | member _.PostTrigger (state : DeleteOperation, trigger : string) = 119 | match state.RequestOptions with 120 | | ValueSome requestOptions -> 121 | requestOptions.AddPostTrigger trigger 122 | state 123 | | ValueNone -> 124 | let options = ItemRequestOptions () 125 | options.AddPostTrigger trigger 126 | { state with RequestOptions = ValueSome options } 127 | 128 | /// Adds triggers to be invoked after the operation 129 | [] 130 | member _.PostTriggers (state : DeleteOperation, triggers : seq) = 131 | match state.RequestOptions with 132 | | ValueSome requestOptions -> 133 | requestOptions.AddPostTriggers triggers 134 | state 135 | | ValueNone -> 136 | let options = ItemRequestOptions () 137 | options.AddPostTriggers triggers 138 | { state with RequestOptions = ValueSome options } 139 | 140 | /// Sets the session token 141 | [] 142 | member _.SessionToken (state : DeleteOperation, sessionToken : string) = 143 | match state.RequestOptions with 144 | | ValueSome requestOptions -> 145 | requestOptions.SessionToken <- sessionToken 146 | state 147 | | ValueNone -> 148 | let options = ItemRequestOptions (SessionToken = sessionToken) 149 | { state with RequestOptions = ValueSome options } 150 | 151 | let delete = DeleteBuilder () 152 | 153 | // https://docs.microsoft.com/en-us/rest/api/cosmos-db/http-status-codes-for-cosmosdb 154 | 155 | /// Represents the result of a delete operation. 156 | type DeleteResult<'t> = 157 | | Ok of 't // 200 158 | | NotFound of ResponseBody : string // 404 159 | 160 | open System.Net 161 | 162 | module CosmosException = 163 | 164 | let toDeleteResult (ex : CosmosException) = 165 | match ex.StatusCode with 166 | | HttpStatusCode.NotFound -> DeleteResult.NotFound ex.ResponseBody 167 | | _ -> raise ex 168 | 169 | open System.Runtime.InteropServices 170 | open System.Threading 171 | open System.Threading.Tasks 172 | open CosmosException 173 | 174 | type Microsoft.Azure.Cosmos.Container with 175 | 176 | /// 177 | /// Executes a delete operation 178 | /// 179 | /// Delete operation. 180 | /// Cancellation token. 181 | member container.PlainExecuteAsync (operation : DeleteOperation, [] cancellationToken : CancellationToken) = 182 | container.DeleteItemAsync ( 183 | operation.Id, 184 | operation.PartitionKey, 185 | operation.RequestOptions |> ValueOption.toObj, 186 | cancellationToken = cancellationToken 187 | ) 188 | 189 | /// 190 | /// Executes a delete operation and returns . 191 | /// 192 | /// Delete operation. 193 | /// Cancellation token. 194 | member container.ExecuteAsync (operation : DeleteOperation, [] cancellationToken : CancellationToken) = task { 195 | try 196 | let! response = container.PlainExecuteAsync (operation, cancellationToken) 197 | return CosmosResponse.fromItemResponse DeleteResult.Ok response 198 | with HandleException ex -> 199 | return CosmosResponse.fromException toDeleteResult ex 200 | } 201 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # FSharp.Azure.Cosmos [![NuGet Status](https://img.shields.io/nuget/v/FSharp.Azure.Cosmos.svg?style=flat)](https://www.nuget.org/packages/FSharp.Azure.Cosmos/) [![GitHub Actions](https://github.com/fsprojects/FSharp.Azure.Cosmos/workflows/Build%20main/badge.svg)](https://github.com/fsprojects/FSharp.Azure.Cosmos/actions?query=branch%3Amain) 2 | 3 | An F# idiomatic wrapper for [Azure Cosmos DB SDK](https://github.com/Azure/azure-cosmos-dotnet-v3) that provides strongly-typed response handling and computation expressions for all Cosmos DB operations. 4 | 5 | ## Features 6 | 7 | ### F# Idiomatic Operations 8 | - Responds with discriminated unions for each Cosmos DB operation to handle status codes as values instead of exceptions 9 | - F# computation expressions for all Cosmos DB operations: 10 | - Read 11 | - ReadMany 12 | - Create 13 | - Replace 14 | - Upsert 15 | - Delete 16 | - Patch 17 | - Unique key definition through computation expressions 18 | - Extension methods for executing operations defined with computation expressions 19 | 20 | ### Modern Query Support 21 | - Query extensions that create `IAsyncEnumerable` (`TaskSeq`) from `FeedIterator`/`IQueryable` 22 | - `CancellableTaskSeq` module 23 | 24 | [Documentation](https://fsprojects.github.io/FSharp.Azure.Cosmos/) 25 | 26 | --- 27 | 28 | ## Builds 29 | 30 | GitHub Actions | 31 | :---: | 32 | [![GitHub Actions](https://github.com/fsprojects/FSharp.Azure.Cosmos/workflows/Build%20main/badge.svg)](https://github.com/fsprojects/FSharp.Azure.Cosmos/actions?query=branch%3Amain) | 33 | 34 | ## NuGet 35 | 36 | Package | Stable | Prerelease 37 | --- | --- | --- 38 | FSharp.Azure.Cosmos | [![NuGet Badge](https://img.shields.io/nuget/v/FSharp.Azure.Cosmos.svg)](https://www.nuget.org/packages/FSharp.Azure.Cosmos/) | [![NuGet Badge](https://img.shields.io/nuget/vpre/FSharp.Azure.Cosmos.svg)](https://www.nuget.org/packages/FSharp.Azure.Cosmos/) 39 | 40 | --- 41 | 42 | ### Developing 43 | 44 | Make sure the following **requirements** are installed on your system: 45 | 46 | - [dotnet SDK](https://www.microsoft.com/net/download/core) 9.0 or higher (uses F# 9) 47 | 48 | or 49 | 50 | - [VSCode Dev Container](https://code.visualstudio.com/docs/remote/containers) 51 | 52 | 53 | --- 54 | 55 | ### Environment Variables 56 | 57 | - `CONFIGURATION` will set the [configuration](https://docs.microsoft.com/en-us/dotnet/core/tools/dotnet-build?tabs=netcore2x#options) of the dotnet commands. If not set, it will default to Release. 58 | - `CONFIGURATION=Debug ./build.sh` will result in `-c` additions to commands such as in `dotnet build -c Debug` 59 | - `ENABLE_COVERAGE` Will enable running code coverage metrics. AltCover can have [severe performance degradation](https://github.com/SteveGilham/altcover/issues/57) so code coverage evaluation are disabled by default to speed up the feedback loop. 60 | - `ENABLE_COVERAGE=1 ./build.sh` will enable code coverage evaluation 61 | 62 | 63 | --- 64 | 65 | ### Building 66 | > build.cmd // on windows 67 | 68 | > ./build.sh // on unix 69 | --- 70 | 71 | ### Build Targets 72 | 73 | - `Clean` - Cleans artifact and temp directories. 74 | - `DotnetRestore` - Runs [dotnet restore](https://docs.microsoft.com/en-us/dotnet/core/tools/dotnet-restore?tabs=netcore2x) on the [solution file](https://docs.microsoft.com/en-us/visualstudio/extensibility/internals/solution-dot-sln-file?view=vs-2019). 75 | - [`DotnetBuild`](#Building) - Runs [dotnet build](https://docs.microsoft.com/en-us/dotnet/core/tools/dotnet-build?tabs=netcore2x) on the [solution file](https://docs.microsoft.com/en-us/visualstudio/extensibility/internals/solution-dot-sln-file?view=vs-2019). 76 | - `FSharpAnalyzers` - Runs [BinaryDefense.FSharp.Analyzers](https://github.com/BinaryDefense/BinaryDefense.FSharp.Analyzers). 77 | - `DotnetTest` - Runs [dotnet test](https://docs.microsoft.com/en-us/dotnet/core/tools/dotnet-test?tabs=netcore21) on the [solution file](https://docs.microsoft.com/en-us/visualstudio/extensibility/internals/solution-dot-sln-file?view=vs-2019). 78 | - `GenerateCoverageReport` - Code coverage is run during `DotnetTest` and this generates a report via [ReportGenerator](https://github.com/danielpalme/ReportGenerator). 79 | - `ShowCoverageReport` - Shows the report generated in `GenerateCoverageReport`. 80 | - `WatchTests` - Runs [dotnet watch](https://docs.microsoft.com/en-us/aspnet/core/tutorials/dotnet-watch?view=aspnetcore-3.0) with the test projects. Useful for rapid feedback loops. 81 | - `GenerateAssemblyInfo` - Generates [AssemblyInfo](https://docs.microsoft.com/en-us/dotnet/api/microsoft.visualbasic.applicationservices.assemblyinfo?view=netframework-4.8) for libraries. 82 | - `DotnetPack` - Runs [dotnet pack](https://docs.microsoft.com/en-us/dotnet/core/tools/dotnet-pack). This includes running [Source Link](https://github.com/dotnet/sourcelink). 83 | - `SourceLinkTest` - Runs a Source Link test tool to verify Source Links were properly generated. 84 | - `PublishToNuGet` - Publishes the NuGet packages generated in `DotnetPack` to NuGet via [paket push](https://fsprojects.github.io/Paket/paket-push.html). Runs only from `Github Actions`. 85 | - `GitRelease` - Creates a commit message with the [Release Notes](https://fake.build/apidocs/v5/fake-core-releasenotes.html) and a git tag via the version in the `Release Notes`. 86 | - `GitHubRelease` - Publishes a [GitHub Release](https://help.github.com/en/articles/creating-releases) with the Release Notes and any NuGet packages. Runs only from `Github Actions`. 87 | - `FormatCode` - Runs [Fantomas](https://github.com/fsprojects/fantomas) on the solution file. 88 | - `CheckFormatCode` - Runs [Fantomas --check](https://fsprojects.github.io/fantomas/docs/end-users/FormattingCheck.html) on the solution file. 89 | - `BuildDocs` - Generates [Documentation](https://fsprojects.github.io/FSharp.Formatting) from `docsSrc` and the [XML Documentation Comments](https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/xmldoc/) from your libraries in `src`. 90 | - `WatchDocs` - Generates documentation and starts a webserver locally. It will rebuild and hot reload if it detects any changes made to `docsSrc` files, or libraries in `src`. 91 | 92 | --- 93 | 94 | 95 | ### Releasing 96 | 97 | - [Start a git repo with a remote](https://help.github.com/articles/adding-an-existing-project-to-github-using-the-command-line/) 98 | git init 99 | git add . 100 | git commit -m "Scaffold" 101 | git branch -M main 102 | git remote add origin https://github.com/fsprojects/FSharp.Azure.Cosmos.git 103 | git push -u origin main 104 | - [Create an Environment](https://docs.github.com/en/actions/deployment/targeting-different-environments/using-environments-for-deployment#creating-an-environment) on your repository named `nuget`. 105 | - [Create a NuGet API key](https://learn.microsoft.com/en-us/nuget/nuget-org/publish-a-package#create-an-api-key) 106 | - Add your `NUGET_TOKEN` to the [Environment Secrets](https://docs.github.com/en/actions/deployment/targeting-different-environments/using-environments-for-deployment#environment-secrets) of your newly created environment. 107 | - Then update the `CHANGELOG.md` with an "Unreleased" section containing release notes for this version, in [KeepAChangelog](https://keepachangelog.com/en/1.1.0/) format. 108 | 109 | NOTE: Its highly recommend to add a link to the Pull Request next to the release note that it affects. The reason for this is when the `RELEASE` target is run, it will add these new notes into the body of git commit. GitHub will notice the links and will update the Pull Request with what commit referenced it saying ["added a commit that referenced this pull request"](https://github.com/TheAngryByrd/MiniScaffold/pull/179#ref-commit-837ad59). Since the build script automates the commit message, it will say "Bump Version to x.y.z". The benefit of this is when users goto a Pull Request, it will be clear when and which version those code changes released. Also when reading the `CHANGELOG`, if someone is curious about how or why those changes were made, they can easily discover the work and discussions. 110 | 111 | ### Releasing Documentation 112 | 113 | - Set Source for "Build and deployment" on [GitHub Pages](https://github.com/fsprojects/FSharp.Azure.Cosmos/settings/pages) to `GitHub Actions`. 114 | - Documentation is auto-deployed via [GitHub Action](https://github.com/fsprojects/FSharp.Azure.Cosmos/blob/main/.github/workflows/fsdocs-gh-pages.yml) to [Your GitHub Page](https://fsprojects.github.io/FSharp.Azure.Cosmos/) 115 | 116 | # Maintainer(s) 117 | 118 | - [@xperiandri](https://github.com/xperiandri) 119 | 120 | The default maintainer account for projects under "fsprojects" is [@fsprojectsgit](https://github.com/fsprojectsgit) - F# Community Project Incubation Space (repo management) 121 | -------------------------------------------------------------------------------- /.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/main/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.rsuser 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | 16 | # Mono auto generated files 17 | mono_crash.* 18 | 19 | # Build results 20 | [Dd]ebug/ 21 | [Dd]ebugPublic/ 22 | [Rr]elease/ 23 | [Rr]eleases/ 24 | x64/ 25 | x86/ 26 | [Ww][Ii][Nn]32/ 27 | [Aa][Rr][Mm]/ 28 | [Aa][Rr][Mm]64/ 29 | bld/ 30 | [Bb]in/ 31 | [Oo]bj/ 32 | [Ll]og/ 33 | [Ll]ogs/ 34 | 35 | # Visual Studio 2015/2017 cache/options directory 36 | .vs/ 37 | # Uncomment if you have tasks that create the project's static files in wwwroot 38 | #wwwroot/ 39 | 40 | # Visual Studio 2017 auto generated files 41 | Generated\ Files/ 42 | 43 | # MSTest test Results 44 | [Tt]est[Rr]esult*/ 45 | [Bb]uild[Ll]og.* 46 | 47 | # NUnit 48 | *.VisualState.xml 49 | TestResult.xml 50 | nunit-*.xml 51 | 52 | # Build Results of an ATL Project 53 | [Dd]ebugPS/ 54 | [Rr]eleasePS/ 55 | dlldata.c 56 | 57 | # Benchmark Results 58 | BenchmarkDotNet.Artifacts/ 59 | 60 | # .NET Core 61 | project.lock.json 62 | project.fragment.lock.json 63 | artifacts/ 64 | 65 | # ASP.NET Scaffolding 66 | ScaffoldingReadMe.txt 67 | 68 | # StyleCop 69 | StyleCopReport.xml 70 | 71 | # Files built by Visual Studio 72 | *_i.c 73 | *_p.c 74 | *_h.h 75 | *.ilk 76 | *.meta 77 | *.obj 78 | *.iobj 79 | *.pch 80 | *.pdb 81 | *.ipdb 82 | *.pgc 83 | *.pgd 84 | *.rsp 85 | *.sbr 86 | *.tlb 87 | *.tli 88 | *.tlh 89 | *.tmp 90 | *.tmp_proj 91 | *_wpftmp.csproj 92 | *.log 93 | *.tlog 94 | *.vspscc 95 | *.vssscc 96 | .builds 97 | *.pidb 98 | *.svclog 99 | *.scc 100 | 101 | # Chutzpah Test files 102 | _Chutzpah* 103 | 104 | # Visual C++ cache files 105 | ipch/ 106 | *.aps 107 | *.ncb 108 | *.opendb 109 | *.opensdf 110 | *.sdf 111 | *.cachefile 112 | *.VC.db 113 | *.VC.VC.opendb 114 | 115 | # Visual Studio profiler 116 | *.psess 117 | *.vsp 118 | *.vspx 119 | *.sap 120 | 121 | # Visual Studio Trace Files 122 | *.e2e 123 | 124 | # TFS 2012 Local Workspace 125 | $tf/ 126 | 127 | # Guidance Automation Toolkit 128 | *.gpState 129 | 130 | # ReSharper is a .NET coding add-in 131 | _ReSharper*/ 132 | *.[Rr]e[Ss]harper 133 | *.DotSettings.user 134 | 135 | # JustCode is a .NET coding add-in 136 | .JustCode 137 | 138 | # TeamCity is a build add-in 139 | _TeamCity* 140 | 141 | # DotCover is a Code Coverage Tool 142 | *.dotCover 143 | 144 | # AxoCover is a Code Coverage Tool 145 | .axoCover/* 146 | !.axoCover/settings.json 147 | 148 | # Coverlet is a free, cross platform Code Coverage Tool 149 | coverage*.json 150 | coverage*.xml 151 | coverage*.info 152 | 153 | # Visual Studio code coverage results 154 | *.coverage 155 | *.coveragexml 156 | 157 | # NCrunch 158 | _NCrunch_* 159 | .*crunch*.local.xml 160 | nCrunchTemp_* 161 | 162 | # MightyMoose 163 | *.mm.* 164 | AutoTest.Net/ 165 | 166 | # Web workbench (sass) 167 | .sass-cache/ 168 | 169 | # Installshield output folder 170 | [Ee]xpress/ 171 | 172 | # DocProject is a documentation generator add-in 173 | DocProject/buildhelp/ 174 | DocProject/Help/*.HxT 175 | DocProject/Help/*.HxC 176 | DocProject/Help/*.hhc 177 | DocProject/Help/*.hhk 178 | DocProject/Help/*.hhp 179 | DocProject/Help/Html2 180 | DocProject/Help/html 181 | 182 | # Click-Once directory 183 | publish/ 184 | 185 | # Publish Web Output 186 | *.[Pp]ublish.xml 187 | *.azurePubxml 188 | # Note: Comment the next line if you want to checkin your web deploy settings, 189 | # but database connection strings (with potential passwords) will be unencrypted 190 | *.pubxml 191 | *.publishproj 192 | 193 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 194 | # checkin your Azure Web App publish settings, but sensitive information contained 195 | # in these scripts will be unencrypted 196 | PublishScripts/ 197 | 198 | # NuGet Packages 199 | *.nupkg 200 | # NuGet Symbol Packages 201 | *.snupkg 202 | # The packages folder can be ignored because of Package Restore 203 | **/[Pp]ackages/* 204 | # except build/, which is used as an MSBuild target. 205 | !**/[Pp]ackages/build/ 206 | # Uncomment if necessary however generally it will be regenerated when needed 207 | #!**/[Pp]ackages/repositories.config 208 | # NuGet v3's project.json files produces more ignorable files 209 | *.nuget.props 210 | *.nuget.targets 211 | 212 | # Microsoft Azure Build Output 213 | csx/ 214 | *.build.csdef 215 | 216 | # Microsoft Azure Emulator 217 | ecf/ 218 | rcf/ 219 | 220 | # Windows Store app package directories and files 221 | AppPackages/ 222 | BundleArtifacts/ 223 | Package.StoreAssociation.xml 224 | _pkginfo.txt 225 | *.appx 226 | *.appxbundle 227 | *.appxupload 228 | 229 | # Visual Studio cache files 230 | # files ending in .cache can be ignored 231 | *.[Cc]ache 232 | # but keep track of directories ending in .cache 233 | !?*.[Cc]ache/ 234 | 235 | # Others 236 | ClientBin/ 237 | ~$* 238 | *~ 239 | *.dbmdl 240 | *.dbproj.schemaview 241 | *.jfm 242 | *.pfx 243 | *.publishsettings 244 | orleans.codegen.cs 245 | 246 | # Including strong name files can present a security risk 247 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 248 | #*.snk 249 | 250 | # Since there are multiple workflows, uncomment next line to ignore bower_components 251 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 252 | #bower_components/ 253 | 254 | # RIA/Silverlight projects 255 | Generated_Code/ 256 | 257 | # Backup & report files from converting an old project file 258 | # to a newer Visual Studio version. Backup files are not needed, 259 | # because we have git ;-) 260 | _UpgradeReport_Files/ 261 | Backup*/ 262 | UpgradeLog*.XML 263 | UpgradeLog*.htm 264 | ServiceFabricBackup/ 265 | *.rptproj.bak 266 | 267 | # SQL Server files 268 | *.mdf 269 | *.ldf 270 | *.ndf 271 | 272 | # Business Intelligence projects 273 | *.rdl.data 274 | *.bim.layout 275 | *.bim_*.settings 276 | *.rptproj.rsuser 277 | *- [Bb]ackup.rdl 278 | *- [Bb]ackup ([0-9]).rdl 279 | *- [Bb]ackup ([0-9][0-9]).rdl 280 | 281 | # Microsoft Fakes 282 | FakesAssemblies/ 283 | 284 | # GhostDoc plugin setting file 285 | *.GhostDoc.xml 286 | 287 | # Node.js Tools for Visual Studio 288 | .ntvs_analysis.dat 289 | node_modules/ 290 | 291 | # Visual Studio 6 build log 292 | *.plg 293 | 294 | # Visual Studio 6 workspace options file 295 | *.opt 296 | 297 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 298 | *.vbw 299 | 300 | # Visual Studio 6 auto-generated project file (contains which files were open etc.) 301 | *.vbp 302 | 303 | # Visual Studio 6 workspace and project file (working project files containing files to include in project) 304 | *.dsw 305 | *.dsp 306 | 307 | # Visual Studio 6 technical files 308 | *.ncb 309 | *.aps 310 | 311 | # Visual Studio LightSwitch build output 312 | **/*.HTMLClient/GeneratedArtifacts 313 | **/*.DesktopClient/GeneratedArtifacts 314 | **/*.DesktopClient/ModelManifest.xml 315 | **/*.Server/GeneratedArtifacts 316 | **/*.Server/ModelManifest.xml 317 | _Pvt_Extensions 318 | 319 | # Paket dependency manager 320 | .paket/paket.exe 321 | paket-files/ 322 | 323 | # FAKE - F# Make 324 | .fake/ 325 | 326 | # CodeRush personal settings 327 | .cr/personal 328 | 329 | # Python Tools for Visual Studio (PTVS) 330 | __pycache__/ 331 | *.pyc 332 | 333 | # Cake - Uncomment if you are using it 334 | # tools/** 335 | # !tools/packages.config 336 | 337 | # Tabs Studio 338 | *.tss 339 | 340 | # Telerik's JustMock configuration file 341 | *.jmconfig 342 | 343 | # BizTalk build output 344 | *.btp.cs 345 | *.btm.cs 346 | *.odx.cs 347 | *.xsd.cs 348 | 349 | # OpenCover UI analysis results 350 | OpenCover/ 351 | 352 | # Azure Stream Analytics local run output 353 | ASALocalRun/ 354 | 355 | # MSBuild Binary and Structured Log 356 | *.binlog 357 | 358 | # NVidia Nsight GPU debugger configuration file 359 | *.nvuser 360 | 361 | # MFractors (Xamarin productivity tool) working folder 362 | .mfractor/ 363 | 364 | # Local History for Visual Studio 365 | .localhistory/ 366 | 367 | # Visual Studio History (VSHistory) files 368 | .vshistory/ 369 | 370 | # BeatPulse healthcheck temp database 371 | healthchecksdb 372 | 373 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 374 | MigrationBackup/ 375 | 376 | # Ionide (cross platform F# VS Code tools) working folder 377 | .ionide/ 378 | 379 | # Fody - auto-generated XML schema 380 | FodyWeavers.xsd 381 | 382 | # VS Code files for those working on multiple tools 383 | .vscode/* 384 | !.vscode/settings.json 385 | !.vscode/tasks.json 386 | !.vscode/launch.json 387 | !.vscode/extensions.json 388 | *.code-workspace 389 | 390 | # Local History for Visual Studio Code 391 | .history/ 392 | 393 | # Windows Installer files from build outputs 394 | *.cab 395 | *.msi 396 | *.msix 397 | *.msm 398 | *.msp 399 | 400 | # JetBrains Rider 401 | *.sln.iml 402 | 403 | # fsdocs generated 404 | tmp/ 405 | temp/ 406 | .fsdocs 407 | docs/ 408 | -------------------------------------------------------------------------------- /src/Cosmos/Read.fs: -------------------------------------------------------------------------------- 1 | [] 2 | module FSharp.Azure.Cosmos.Read 3 | 4 | open Microsoft.Azure.Cosmos 5 | 6 | [] 7 | type ReadOperation<'T> = { 8 | Id : string 9 | PartitionKey : PartitionKey 10 | RequestOptions : ItemRequestOptions | null 11 | } 12 | 13 | open System 14 | 15 | type ReadBuilder<'T> () = 16 | member _.Yield _ = 17 | { Id = String.Empty; PartitionKey = PartitionKey.None; RequestOptions = null } : ReadOperation<'T> 18 | 19 | /// Sets the item being created 20 | [] 21 | member _.Id (state : ReadOperation<_>, id) = { state with Id = id } 22 | 23 | /// Sets the partition key 24 | [] 25 | member _.PartitionKey (state : ReadOperation<_>, partitionKey : PartitionKey) = { state with PartitionKey = partitionKey } 26 | 27 | /// Sets the partition key 28 | [] 29 | member _.PartitionKey (state : ReadOperation<_>, partitionKey : string) = { 30 | state with 31 | PartitionKey = PartitionKey partitionKey 32 | } 33 | 34 | /// Sets the request options 35 | [] 36 | member _.RequestOptions (state : ReadOperation<_>, options : ItemRequestOptions) = { state with RequestOptions = options } 37 | 38 | /// Sets the eTag to 39 | [] 40 | member _.ETag (state : ReadOperation<_>, eTag : string) = 41 | match state.RequestOptions with 42 | | null -> 43 | let options = ItemRequestOptions (IfNoneMatchEtag = eTag) 44 | { state with RequestOptions = options } 45 | | options -> 46 | options.IfNoneMatchEtag <- eTag 47 | state 48 | 49 | // ------------------------------------------- Request options ------------------------------------------- 50 | /// Sets the operation 51 | [] 52 | member _.ConsistencyLevel (state : ReadOperation<_>, consistencyLevel : ConsistencyLevel Nullable) = 53 | match state.RequestOptions with 54 | | null -> 55 | let options = ItemRequestOptions (ConsistencyLevel = consistencyLevel) 56 | { state with RequestOptions = options } 57 | | options -> 58 | options.ConsistencyLevel <- consistencyLevel 59 | state 60 | 61 | /// Sets the indexing directive 62 | [] 63 | member _.IndexingDirective (state : ReadOperation<_>, indexingDirective : IndexingDirective Nullable) = 64 | match state.RequestOptions with 65 | | null -> 66 | let options = ItemRequestOptions (IndexingDirective = indexingDirective) 67 | { state with RequestOptions = options } 68 | | options -> 69 | options.IndexingDirective <- indexingDirective 70 | state 71 | 72 | /// Sets the session token 73 | [] 74 | member _.SessionToken (state : ReadOperation<_>, sessionToken : string) = 75 | match state.RequestOptions with 76 | | null -> 77 | let options = ItemRequestOptions (SessionToken = sessionToken) 78 | { state with RequestOptions = options } 79 | | options -> 80 | options.SessionToken <- sessionToken 81 | state 82 | 83 | let read<'T> = ReadBuilder<'T> () 84 | 85 | // https://docs.microsoft.com/en-us/rest/api/cosmos-db/http-status-codes-for-cosmosdb 86 | 87 | /// Represents the result of a read operation. 88 | type ReadResult<'t> = 89 | | Ok of 't // 200 90 | | NotModified // 204 91 | | IncompatibleConsistencyLevel of ResponseBody : string // 400 92 | | NotFound of ResponseBody : string // 404 93 | 94 | open System.Net 95 | 96 | module CosmosException = 97 | 98 | let toReadResult badRequestCtor notFoundResultCtor (ex : CosmosException) = 99 | match ex.StatusCode with 100 | | HttpStatusCode.BadRequest -> badRequestCtor ex.ResponseBody 101 | | HttpStatusCode.NotFound -> notFoundResultCtor ex.ResponseBody 102 | | _ -> raise ex 103 | 104 | open System.Runtime.InteropServices 105 | open System.Threading 106 | open System.Threading.Tasks 107 | open CosmosException 108 | 109 | type Microsoft.Azure.Cosmos.Container with 110 | 111 | /// 112 | /// Executes a read operation and returns . 113 | /// 114 | /// Read operation 115 | /// Cancellation token 116 | member container.PlainExecuteAsync<'T> (operation : ReadOperation<'T>, [] cancellationToken : CancellationToken) = 117 | container.ReadItemAsync<'T> ( 118 | operation.Id, 119 | operation.PartitionKey, 120 | operation.RequestOptions, 121 | cancellationToken = cancellationToken 122 | ) 123 | 124 | /// 125 | /// Executes a read operation, transforms success or failure, and returns . 126 | /// 127 | /// Read operation 128 | /// Result transform if success 129 | /// Error transform if failure 130 | /// Cancellation token 131 | member container.ExecuteAsync<'T, 'Result> 132 | (operation : ReadOperation<'T>, success, failure, [] cancellationToken : CancellationToken) 133 | : Task> 134 | = 135 | task { 136 | try 137 | let! result = container.PlainExecuteAsync (operation, cancellationToken) 138 | return CosmosResponse.fromItemResponse (success) result 139 | with HandleException ex -> 140 | return CosmosResponse.fromException (failure) ex 141 | } 142 | 143 | /// 144 | /// Executes a read operation and returns . 145 | /// 146 | /// Read operation 147 | /// Cancellation token 148 | member container.ExecuteAsync<'T> (operation : ReadOperation<'T>, [] cancellationToken : CancellationToken) = 149 | let successFn result : ReadResult<'T> = 150 | if Object.Equals (result, Unchecked.defaultof<'T>) then 151 | ReadResult.NotModified 152 | else 153 | ReadResult.Ok result 154 | 155 | container.ExecuteAsync<'T, ReadResult<'T>> ( 156 | operation, 157 | successFn, 158 | toReadResult ReadResult.IncompatibleConsistencyLevel ReadResult.NotFound, 159 | cancellationToken 160 | ) 161 | 162 | /// 163 | /// Executes a read operation and returns . 164 | /// 165 | /// Read operation 166 | /// Cancellation token 167 | member container.ExecuteAsyncOption<'T> (operation : ReadOperation<'T>, [] cancellationToken : CancellationToken) = 168 | container.ExecuteAsync<'T, 'T option> ( 169 | operation, 170 | Some, 171 | toReadResult (fun message -> raise (invalidOp message)) (fun _ -> None), 172 | cancellationToken 173 | ) 174 | 175 | /// 176 | /// Executes a read operation and returns . 177 | /// 178 | /// Read operation 179 | /// Cancellation token 180 | member container.ExecuteAsyncValueOption<'T> 181 | (operation : ReadOperation<'T>, [] cancellationToken : CancellationToken) 182 | = 183 | container.ExecuteAsync<'T, 'T voption> ( 184 | operation, 185 | ValueSome, 186 | toReadResult (fun message -> raise (invalidOp message)) (fun _ -> ValueNone), 187 | cancellationToken 188 | ) 189 | 190 | open System.Runtime.CompilerServices 191 | 192 | [] 193 | type FeedIteratorExtensions private () = 194 | 195 | static let seqToReadResult (items : 'T seq) = 196 | match items |> Seq.tryHead with 197 | | Some item -> Ok item 198 | | None -> NotFound "Item not found" 199 | 200 | [] 201 | static member FirstAsync (iterator : FeedIterator<'T>, [] cancellationToken : CancellationToken) = task { 202 | try 203 | let! page = iterator.ReadNextAsync cancellationToken 204 | return CosmosResponse.fromFeedResponse seqToReadResult page 205 | with HandleException ex -> 206 | return CosmosResponse.fromException (toReadResult ReadResult.IncompatibleConsistencyLevel ReadResult.NotFound) ex 207 | } 208 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: 2 | http://EditorConfig.org 3 | 4 | # top-most EditorConfig file 5 | root = true 6 | 7 | ############################### 8 | # Core EditorConfig Options # 9 | ############################### 10 | # All files 11 | [*] # Do not apply to all files not to break something 12 | guidelines = 120 dashed, 130 13 | # Either crlf | lf, default is system-dependent (when not specified at all) 14 | # end_of_line=crlf 15 | # Remove whitespace at the end of any line 16 | 17 | # Visual Studio Solution Files 18 | [*.sln] 19 | indent_style = tab 20 | 21 | # Code files 22 | [*.{cs,csx,fs,fsi,fsx}] 23 | trim_trailing_whitespace = true 24 | insert_final_newline = true 25 | indent_style = space # default=space 26 | indent_size = 4 # default=4 27 | charset = utf-8 28 | 29 | # Project files and app specific XML files 30 | [*.{csproj,fsproj,shproj,sfproj,projitems,props,xaml,targets,ruleset,config,nuspec,resx,vsixmanifest,vsct}] 31 | trim_trailing_whitespace = true 32 | insert_final_newline = true 33 | indent_style = space 34 | indent_size = 2 35 | 36 | # XML configuration files 37 | [{app.config,nuget.config,packages.config,web.config}] 38 | trim_trailing_whitespace = true 39 | insert_final_newline = true 40 | indent_style = space 41 | indent_size = 2 42 | 43 | # XML files 44 | [*.xml] 45 | trim_trailing_whitespace = false # do not trim as it affects CData 46 | insert_final_newline = true 47 | indent_style = space 48 | indent_size = 2 49 | 50 | # JSON and YAML files 51 | [*.{json,yml,yaml}] 52 | trim_trailing_whitespace = true 53 | insert_final_newline = true 54 | indent_style = space 55 | indent_size = 2 56 | 57 | # Proto files 58 | [*.proto] 59 | trim_trailing_whitespace = true 60 | insert_final_newline = true 61 | indent_style = space 62 | indent_size = 4 63 | 64 | # Markdown Files 65 | [*.{md,mdx}] 66 | trim_trailing_whitespace = false 67 | 68 | # Bash Files 69 | [*.{sh}] 70 | end_of_line = lf 71 | 72 | # Batch Files 73 | [*.{cmd,bat}] 74 | end_of_line = crlf 75 | 76 | # Powershell Files 77 | [*.{ps1, psm1}] 78 | end_of_line = crlf 79 | 80 | # Paket files 81 | [paket.*] 82 | trim_trailing_whitespace = true 83 | indent_size = 2 84 | 85 | [*.paket.references] 86 | trim_trailing_whitespace = true 87 | indent_size = 2 88 | 89 | ############################### 90 | # F# Coding Conventions # 91 | ############################### 92 | # https://fsprojects.github.io/fantomas/docs/end-users/Configuration.html 93 | 94 | # filetypes that need to be formatted by Fantomas: 95 | [*.{fs,fsi,fsx}] 96 | 97 | # files to be ignored for Fantomas may go into this file, if present: 98 | # .fantomasignore 99 | 100 | # indentation size, default=4 101 | indent_size=4 102 | 103 | # line length before it gets broken down into multiple lines 104 | # default 120 105 | max_line_length=130 106 | 107 | # Either crlf | lf, default is system-dependent (when not specified at all) 108 | # end_of_line=crlf 109 | 110 | # Whether end-of-file has a newline, default=true 111 | insert_final_newline=true 112 | 113 | # false: someLineOfCode 114 | # true: someLineOfCode; 115 | # default false 116 | fsharp_semicolon_at_end_of_line=false 117 | 118 | # false: f(1,2) 119 | # true: f(1, 2) 120 | # default true 121 | fsharp_space_before_parameter=true 122 | 123 | # false: Option.map(fun x -> x) 124 | # true: Option.map (fun x -> x) 125 | # default true 126 | fsharp_space_before_lowercase_invocation=true 127 | 128 | # false: x.ToString() 129 | # true: x.ToString () 130 | # default false 131 | fsharp_space_before_uppercase_invocation=true 132 | 133 | # false: new Ship(withBeans) 134 | # true: new Ship (withBeans) 135 | # default false 136 | fsharp_space_before_class_constructor=true 137 | 138 | # false: __.member Foo(x) = x 139 | # true: __.member Foo (x) = x 140 | # default false 141 | fsharp_space_before_member=true 142 | 143 | # false: type Point = { x: int; y: int } 144 | # true: type Point = { x : int; y : int } 145 | # default false 146 | fsharp_space_before_colon=true 147 | 148 | # false: (a,b,c) 149 | # true: (a, b, c) 150 | # default true 151 | fsharp_space_after_comma=true 152 | 153 | # false: [a; b; 42] 154 | # true: [a ; b ; 42] 155 | # default false 156 | fsharp_space_before_semicolon=false 157 | 158 | # false: [a;b;42] 159 | # true: [a; b; 42] 160 | # default true 161 | fsharp_space_after_semicolon=true 162 | 163 | # false: let a = [1;2;3] 164 | # true: let a = [ 1;2;3 ] 165 | # default true 166 | fsharp_space_around_delimiter=true 167 | 168 | # breaks an if-then-else in smaller parts if it is on one line 169 | # default 40 170 | fsharp_max_if_then_else_short_width=60 171 | 172 | # breaks an infix operator expression if it is on one line 173 | # infix: a + b + c 174 | # default 50 175 | fsharp_max_infix_operator_expression=60 176 | 177 | # breaks a single-line record declaration 178 | # i.e. if this gets too wide: { X = 10; Y = 12 } 179 | # default 40 180 | fsharp_max_record_width=80 181 | 182 | # breaks a record into one item per line if items exceed this number 183 | # i.e. if set to 1, this will be on three lines: { X = 10; Y = 12 } 184 | # requires fsharp_record_multiline_formatter=number_of_items to take effect 185 | # default 1 186 | fsharp_max_record_number_of_items=1 187 | 188 | # whether to use line-length (by counting chars) or items (by counting fields) 189 | # for the record settings above 190 | # either number_of_items or character_width 191 | # default character_width 192 | fsharp_record_multiline_formatter=character_width 193 | 194 | # breaks a single line array or list if it exceeds this size 195 | # default 40 196 | fsharp_max_array_or_list_width=100 197 | 198 | # breaks an array or list into one item per line if items exceeds this number 199 | # i.e. if set to 1, this will be shown on three lines [1; 2; 3] 200 | # requires fsharp_array_or_list_multiline_formatter=number_of_items to take effect 201 | # default 1 202 | fsharp_max_array_or_list_number_of_items=1 203 | 204 | # whether to use line-length (by counting chars) or items (by counting fields) 205 | # for the list and array settings above 206 | # either number_of_items or character_width 207 | # default character_width 208 | fsharp_array_or_list_multiline_formatter=character_width 209 | 210 | # maximum with of a value binding, does not include keyword "let" 211 | # default 80 212 | fsharp_max_value_binding_width=100 213 | 214 | # maximum width for function and member binding (rh-side) 215 | # default 40 216 | fsharp_max_function_binding_width=80 217 | 218 | # maximum width for expressions like X.DoY().GetZ(10).Help() 219 | # default 50 220 | fsharp_max_dot_get_expression_width=80 221 | 222 | # whether open/close brackets go on the same column 223 | # cramped: type Range = 224 | # { From: float 225 | # To: float } 226 | # aligned: type Range = 227 | # { 228 | # From: float 229 | # To: float 230 | # } 231 | # stroustrup: type Range = { 232 | # From: float 233 | # To: float 234 | # } 235 | # default cramped 236 | fsharp_multiline_bracket_style=stroustrup 237 | 238 | # whether to move the beginning of compuitation expression to the new line 239 | # true: let x = 240 | # computation { 241 | # ... 242 | # } 243 | # false: let x = computation { 244 | # .. 245 | # } 246 | fsharp_newline_before_multiline_computation_expression=false 247 | 248 | # whether a newline should be placed before members 249 | # false: type Range = 250 | # { From: float } 251 | # member this.Length = this.To - this.From 252 | # true: type Range = 253 | # { From: float } 254 | # 255 | # member this.Length = this.To - this.From 256 | # default false 257 | fsharp_newline_between_type_definition_and_members=true 258 | 259 | # if a function sign exceeds max_line_length, then: 260 | # false: do not place the equal-sign on a single line 261 | # true: place the equal-sign on a single line 262 | # default false 263 | fsharp_align_function_signature_to_indentation=false 264 | 265 | # see docs: https://github.com/fsprojects/fantomas/blob/master/docs/Documentation.md#fsharp_alternative_long_member_definitions 266 | # default false 267 | fsharp_alternative_long_member_definitions=true 268 | 269 | # places closing paren in lambda on a newline in multiline lambdas 270 | # default false 271 | fsharp_multi_line_lambda_closing_newline=true 272 | 273 | # allows the 'else'-branch to be aligned at same level as 'else' if the ret type allows it 274 | # false: match x with 275 | # | null -> () 276 | # | _ -> () 277 | # true: match x with 278 | # | null -> () 279 | # | _ -> 280 | # () 281 | # default false 282 | fsharp_keep_indent_in_branch=true 283 | 284 | # whether a bar is placed before DU 285 | # false: type MyDU = Short of int 286 | # true: type MyDU = | Short of int 287 | # default false 288 | fsharp_bar_before_discriminated_union_declaration=false 289 | 290 | # multiline, nested expressions must be surrounded by blank lines 291 | # default true 292 | fsharp_blank_lines_around_nested_multiline_expressions=false 293 | 294 | # set maximal number of consecutive blank lines to keep from original source 295 | # it doesn't change number of new blank lines generated by Fantomas 296 | fsharp_keep_max_number_of_blank_lines=2 297 | -------------------------------------------------------------------------------- /src/Cosmos/Patch.fs: -------------------------------------------------------------------------------- 1 | [] 2 | module FSharp.Azure.Cosmos.Patch 3 | 4 | open System.Collections.Immutable 5 | open System.Linq 6 | open Microsoft.Azure.Cosmos 7 | 8 | [] 9 | type PatchOperation<'T> = { 10 | Operations : PatchOperation list 11 | Id : string 12 | PartitionKey : PartitionKey 13 | RequestOptions : PatchItemRequestOptions 14 | } 15 | 16 | open System 17 | 18 | type PatchBuilder<'T> (enableContentResponseOnWrite : bool) = 19 | member _.Yield _ = 20 | { 21 | Operations = [] 22 | Id = String.Empty 23 | PartitionKey = PartitionKey.None 24 | RequestOptions = PatchItemRequestOptions (EnableContentResponseOnWrite = enableContentResponseOnWrite) 25 | } 26 | : PatchOperation<'T> 27 | 28 | /// Adds a 29 | [] 30 | member _.Operation (state : PatchOperation<'T>, operation) = { state with Operations = operation :: state.Operations } 31 | 32 | /// Adds the 33 | [] 34 | member _.Operations (state : PatchOperation<'T>, operations) = { state with Operations = state.Operations @ operations } 35 | 36 | /// Sets the Id of an item being patched 37 | [] 38 | member _.Id (state : PatchOperation<'T>, id) = { state with Id = id } 39 | 40 | /// Sets the partition key 41 | [] 42 | member _.PartitionKey (state : PatchOperation<'T>, partitionKey : PartitionKey) = { state with PartitionKey = partitionKey } 43 | 44 | /// Sets the partition key 45 | [] 46 | member _.PartitionKey (state : PatchOperation<'T>, partitionKey : string) = { 47 | state with 48 | PartitionKey = (PartitionKey partitionKey) 49 | } 50 | 51 | /// Sets the request options 52 | [] 53 | member _.RequestOptions (state : PatchOperation<'T>, options : PatchItemRequestOptions) = 54 | options.EnableContentResponseOnWrite <- state.RequestOptions.EnableContentResponseOnWrite 55 | { state with RequestOptions = options } 56 | 57 | /// Sets the eTag to 58 | [] 59 | member _.ETag (state : PatchOperation<'T>, eTag : string) = 60 | state.RequestOptions.IfMatchEtag <- eTag 61 | state 62 | 63 | // ------------------------------------------- Patch request options ------------------------------------------- 64 | /// Sets the filter predicate 65 | [] 66 | member _.FilterPredicate (state : PatchOperation<'T>, filterPredicate : string) = 67 | state.RequestOptions.FilterPredicate <- filterPredicate 68 | state 69 | 70 | // ------------------------------------------- Request options ------------------------------------------- 71 | /// Sets the operation 72 | [] 73 | member _.ConsistencyLevel (state : PatchOperation<_>, consistencyLevel : ConsistencyLevel Nullable) = 74 | state.RequestOptions.ConsistencyLevel <- consistencyLevel 75 | state 76 | 77 | /// Sets if the response should include the content of the item after the operation 78 | [] 79 | member _.EnableContentResponseOnWrite (state : PatchOperation<_>, enableContentResponseOnWrite : bool) = 80 | state.RequestOptions.EnableContentResponseOnWrite <- enableContentResponseOnWrite 81 | state 82 | 83 | /// Sets the indexing directive 84 | [] 85 | member _.IndexingDirective (state : PatchOperation<_>, indexingDirective : IndexingDirective Nullable) = 86 | state.RequestOptions.IndexingDirective <- indexingDirective 87 | state 88 | 89 | /// Adds a trigger to be invoked before the operation 90 | [] 91 | member _.PreTrigger (state : PatchOperation<_>, trigger : string) = 92 | state.RequestOptions.AddPreTrigger trigger 93 | state 94 | 95 | /// Adds triggers to be invoked before the operation 96 | [] 97 | member _.PreTriggers (state : PatchOperation<_>, triggers : seq) = 98 | state.RequestOptions.AddPreTriggers triggers 99 | state 100 | 101 | /// Adds a trigger to be invoked after the operation 102 | [] 103 | member _.PostTrigger (state : PatchOperation<_>, trigger : string) = 104 | state.RequestOptions.AddPostTrigger trigger 105 | state 106 | 107 | /// Adds triggers to be invoked after the operation 108 | [] 109 | member _.PostTriggers (state : PatchOperation<_>, triggers : seq) = 110 | state.RequestOptions.AddPostTriggers triggers 111 | state 112 | 113 | /// Sets the session token 114 | [] 115 | member _.SessionToken (state : PatchOperation<_>, sessionToken : string) = 116 | state.RequestOptions.SessionToken <- sessionToken 117 | state 118 | 119 | let patch<'T> = PatchBuilder<'T> (false) 120 | let patchAndRead<'T> = PatchBuilder<'T> (true) 121 | 122 | // https://docs.microsoft.com/en-us/rest/api/cosmos-db/http-status-codes-for-cosmosdb 123 | 124 | /// Represents the result of a patch operation. 125 | type PatchResult<'t> = 126 | | Ok of 't // 200 127 | | BadRequest of ResponseBody : string // 400 128 | | NotFound of ResponseBody : string // 404 129 | /// Precondition failed 130 | | ModifiedBefore of ResponseBody : string // 412 - need re-do 131 | | TooManyRequests of ResponseBody : string * RetryAfter : TimeSpan voption // 429 132 | 133 | open System.Net 134 | 135 | module CosmosException = 136 | 137 | let toPatchResult (ex : CosmosException) = 138 | match ex.StatusCode with 139 | | HttpStatusCode.BadRequest -> PatchResult.BadRequest ex.ResponseBody 140 | | HttpStatusCode.NotFound -> PatchResult.NotFound ex.ResponseBody 141 | | HttpStatusCode.PreconditionFailed -> PatchResult.ModifiedBefore ex.ResponseBody 142 | | HttpStatusCode.TooManyRequests -> PatchResult.TooManyRequests (ex.ResponseBody, ex.RetryAfter |> ValueOption.ofNullable) 143 | | _ -> raise ex 144 | 145 | open System.Runtime.InteropServices 146 | open System.Threading 147 | open System.Threading.Tasks 148 | open CosmosException 149 | 150 | type Microsoft.Azure.Cosmos.Container with 151 | 152 | /// 153 | /// Executes a patch operation and returns . 154 | /// 155 | /// Patch operation. 156 | /// Cancellation token. 157 | member container.PlainExecuteAsync<'T> (operation : PatchOperation<'T>, [] cancellationToken : CancellationToken) = 158 | container.PatchItemAsync<'T> ( 159 | operation.Id, 160 | operation.PartitionKey, 161 | operation.Operations.ToImmutableList (), 162 | operation.RequestOptions, 163 | cancellationToken = cancellationToken 164 | ) 165 | 166 | /// 167 | /// Executes a patch operation, transforms success or failure, and returns . 168 | /// 169 | /// Patch operation 170 | /// Result transform if success 171 | /// Error transform if failure 172 | /// Cancellation token 173 | member container.ExecuteOverwriteAsync<'T, 'Result> 174 | (operation : PatchOperation<'T>, success, failure, [] cancellationToken : CancellationToken) 175 | : Task> 176 | = 177 | task { 178 | try 179 | let! response = container.PlainExecuteAsync<'T> (operation, cancellationToken) 180 | return CosmosResponse.fromItemResponse success response 181 | with HandleException ex -> 182 | return CosmosResponse.fromException failure ex 183 | } 184 | 185 | /// 186 | /// Executes a patch operation safely and returns . 187 | /// Requires ETag to be set in . 188 | /// 189 | /// Patch operation. 190 | /// Cancellation token. 191 | member container.ExecuteAsync<'T> (operation : PatchOperation<'T>, [] cancellationToken : CancellationToken) = 192 | if String.IsNullOrEmpty operation.RequestOptions.IfMatchEtag then 193 | invalidArg "eTag" "Safe patch requires ETag" 194 | 195 | container.ExecuteOverwriteAsync (operation, PatchResult.Ok, toPatchResult, cancellationToken) 196 | 197 | /// 198 | /// Executes a patch operation and returns . 199 | /// 200 | /// Patch operation. 201 | /// Cancellation token. 202 | member container.ExecuteOverwriteAsync<'T> 203 | (operation : PatchOperation<'T>, [] cancellationToken : CancellationToken) 204 | = 205 | container.ExecuteOverwriteAsync (operation, PatchResult.Ok, toPatchResult, cancellationToken) 206 | -------------------------------------------------------------------------------- /docsSrc/_template.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | {{fsdocs-page-title}} 7 | 8 | 9 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 20 | 21 | 22 | 23 | 25 | 26 | {{fsdocs-watch-script}} 27 | 28 | 29 | 30 | 125 | 126 | 136 |
    137 | {{fsdocs-content}} 138 | {{fsdocs-tooltips}} 139 |
    140 | 141 | 142 | 144 | 147 | 148 | 149 | 151 | 152 | 155 | 156 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | -------------------------------------------------------------------------------- /src/Cosmos/Cosmos.fs: -------------------------------------------------------------------------------- 1 | namespace FSharp.Azure.Cosmos 2 | 3 | open System 4 | open System.Net 5 | open System.Runtime.InteropServices 6 | open System.Threading 7 | open System.Threading.Tasks 8 | open FSharp.Control 9 | open Microsoft.Azure.Cosmos 10 | 11 | module internal RequestOptions = 12 | 13 | let internal createOrUpdate setter requestOptions = 14 | let options = 15 | match requestOptions with 16 | | ValueSome options -> options 17 | | ValueNone -> ItemRequestOptions () 18 | setter options 19 | options 20 | 21 | 22 | [] 23 | module Operations = 24 | 25 | let internal canHandleStatusCode statusCode = 26 | match statusCode with 27 | | HttpStatusCode.BadRequest 28 | | HttpStatusCode.NotFound 29 | | HttpStatusCode.Conflict 30 | | HttpStatusCode.PreconditionFailed 31 | | HttpStatusCode.RequestEntityTooLarge 32 | | HttpStatusCode.TooManyRequests -> true 33 | | _ -> false 34 | 35 | let internal unwrapCosmosException (ex : Exception) = 36 | match ex with 37 | | :? CosmosException as ex -> Some ex 38 | | :? AggregateException as ex -> 39 | match ex.InnerException with 40 | | :? CosmosException as cex -> Some cex 41 | | _ -> None 42 | | _ -> None 43 | 44 | let internal handleException (ex : Exception) = 45 | let cosmosException = unwrapCosmosException ex 46 | match cosmosException with 47 | | Some ex when canHandleStatusCode ex.StatusCode -> Some ex 48 | | _ -> None 49 | 50 | let (|CosmosException|_|) (ex : Exception) = unwrapCosmosException ex 51 | 52 | let (|HandleException|_|) (ex : Exception) = handleException ex 53 | 54 | let internal retryUpdate toErrorResult executeConcurrentlyAsync maxRetryCount currentAttemptCount (e : CosmosException) = 55 | match e.StatusCode with 56 | | HttpStatusCode.PreconditionFailed when currentAttemptCount >= maxRetryCount -> 57 | CosmosResponse.fromException toErrorResult e |> async.Return 58 | | HttpStatusCode.PreconditionFailed -> executeConcurrentlyAsync maxRetryCount (currentAttemptCount + 1) 59 | | _ -> CosmosResponse.fromException toErrorResult e |> async.Return 60 | 61 | let internal getRequestOptionsWithMaxItemCount1 requestOptions = 62 | requestOptions 63 | |> ValueOption.ofObj 64 | |> ValueOption.defaultWith QueryRequestOptions 65 | |> fun o -> 66 | o.MaxItemCount <- 1 67 | o 68 | 69 | type ItemRequestOptions with 70 | 71 | member options.AddPreTrigger (trigger : string) = 72 | options.PreTriggers <- [| 73 | if not <| isNull options.PreTriggers then 74 | yield! options.PreTriggers 75 | yield trigger 76 | |] 77 | 78 | member options.AddPreTriggers (triggers : string seq) = 79 | if obj.ReferenceEquals (triggers, null) then 80 | raise (ArgumentNullException (nameof triggers)) 81 | options.PreTriggers <- [| 82 | if not <| isNull options.PreTriggers then 83 | yield! options.PreTriggers 84 | yield! triggers 85 | |] 86 | 87 | member options.AddPostTrigger (trigger : string) = 88 | options.PostTriggers <- [| 89 | if not <| isNull options.PostTriggers then 90 | yield! options.PostTriggers 91 | yield trigger 92 | |] 93 | 94 | member options.AddPostTriggers (triggers : string seq) = 95 | if obj.ReferenceEquals (triggers, null) then 96 | raise (ArgumentNullException (nameof triggers)) 97 | options.PostTriggers <- [| yield! options.PostTriggers; yield! triggers |] 98 | 99 | let internal countQuery = QueryDefinition ("SELECT VALUE COUNT(1) FROM c") 100 | let internal existsQuery = QueryDefinition ("SELECT VALUE COUNT(1) FROM item WHERE item.id = @Id") 101 | let internal getExistsQuery id = existsQuery.WithParameter ("@Id", id) 102 | 103 | type Microsoft.Azure.Cosmos.Container with 104 | 105 | /// 106 | /// Counts the number of items in the container with specified . 107 | /// 108 | /// Request options 109 | /// Cancellation token 110 | member container.CountAsync (requestOptions : QueryRequestOptions, [] cancellationToken : CancellationToken) = 111 | container.GetItemQueryIterator (countQuery, requestOptions = getRequestOptionsWithMaxItemCount1 requestOptions) 112 | |> CancellableTaskSeq.ofFeedIterator cancellationToken 113 | |> TaskSeq.tryHead 114 | |> Task.map (Option.defaultValue 0) 115 | 116 | /// 117 | /// Counts the number of items in the container partition with specified key. 118 | /// 119 | /// If no partition key is provided, the count will be for the entire container. 120 | /// 121 | /// 122 | /// Partition key 123 | /// Cancellation token 124 | member container.CountAsync (partitionKey, [] cancellationToken : CancellationToken) = 125 | container.CountAsync (QueryRequestOptions (PartitionKey = partitionKey), cancellationToken) 126 | 127 | /// 128 | /// Counts the number of items in the container partition with specified key. 129 | /// 130 | /// If no partition key is provided, the count will be for the entire container. 131 | /// 132 | /// 133 | /// Partition key 134 | /// Cancellation token 135 | member container.CountAsync (partitionKey : string, [] cancellationToken : CancellationToken) = 136 | if String.IsNullOrEmpty partitionKey then 137 | container.CountAsync (PartitionKey.None, cancellationToken = cancellationToken) 138 | else 139 | container.CountAsync (PartitionKey partitionKey, cancellationToken) 140 | 141 | /// 142 | /// Counts the number of items in the container with specified . 143 | /// 144 | /// Request options 145 | /// Cancellation token 146 | member container.LongCountAsync 147 | (requestOptions : QueryRequestOptions, [] cancellationToken : CancellationToken) 148 | = 149 | container.GetItemQueryIterator (countQuery, requestOptions = getRequestOptionsWithMaxItemCount1 requestOptions) 150 | |> CancellableTaskSeq.ofFeedIterator cancellationToken 151 | |> TaskSeq.tryHead 152 | |> Task.map (Option.defaultValue 0) 153 | 154 | /// 155 | /// Counts the number of items in the container partition with specified key. 156 | /// 157 | /// If no partition key is provided, the count will be for the entire container. 158 | /// 159 | /// 160 | /// Partition key 161 | /// Cancellation token 162 | member container.LongCountAsync (partitionKey, [] cancellationToken : CancellationToken) = 163 | container.LongCountAsync (QueryRequestOptions (PartitionKey = partitionKey), cancellationToken) 164 | 165 | /// 166 | /// Counts the number of items in the container partition with specified key. 167 | /// 168 | /// If no partition key is provided, the count will be for the entire container. 169 | /// 170 | /// 171 | /// Partition key 172 | /// Cancellation token 173 | member container.LongCountAsync (partitionKey : string, [] cancellationToken : CancellationToken) = 174 | container.LongCountAsync (PartitionKey partitionKey, cancellationToken) 175 | 176 | /// 177 | /// Checks if an item with specified Id exists in the container. 178 | /// 179 | /// Item Id 180 | /// Request options 181 | /// Cancellation token 182 | member container.ExistsAsync 183 | (id : string, [] requestOptions : QueryRequestOptions, [] cancellationToken : CancellationToken) 184 | = 185 | task { 186 | let query = getExistsQuery id 187 | let! count = 188 | container.GetItemQueryIterator ( 189 | query, 190 | requestOptions = getRequestOptionsWithMaxItemCount1 requestOptions 191 | ) 192 | |> CancellableTaskSeq.ofFeedIterator cancellationToken 193 | |> TaskSeq.tryHead 194 | |> Task.map (Option.defaultValue 0) 195 | return count = 1 196 | } 197 | 198 | /// 199 | /// Checks if an item with specified Id exists in the container partition with specified key. 200 | /// 201 | /// Item Id 202 | /// Partition key 203 | /// Cancellation token 204 | member container.ExistsAsync 205 | (id : string, partitionKey : PartitionKey, [] cancellationToken : CancellationToken) 206 | = 207 | container.ExistsAsync (id, QueryRequestOptions (PartitionKey = partitionKey), cancellationToken) 208 | 209 | /// 210 | /// Checks if an item with specified Id exists in the container partition with specified key. 211 | /// 212 | /// Item Id 213 | /// Partition key 214 | /// Cancellation token 215 | member container.IsNotDeletedAsync 216 | deletedFieldName 217 | (id : string, [] requiestOptions : QueryRequestOptions, [] cancellationToken : CancellationToken) 218 | = 219 | task { 220 | let query = 221 | QueryDefinition( 222 | $"SELECT VALUE COUNT(1) \ 223 | FROM item \ 224 | WHERE item.id = @Id AND IS_NULL(item.{deletedFieldName})" 225 | ) 226 | .WithParameter ("@Id", id) 227 | let! count = 228 | container.GetItemQueryIterator ( 229 | query, 230 | requestOptions = getRequestOptionsWithMaxItemCount1 requiestOptions 231 | ) 232 | |> CancellableTaskSeq.ofFeedIterator cancellationToken 233 | |> TaskSeq.tryHead 234 | |> Task.map (Option.defaultValue 0) 235 | return count = 1 236 | } 237 | -------------------------------------------------------------------------------- /docsSrc/content/fsdocs-main.css: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css2?family=Hind+Vadodara&family=Roboto+Mono&display=swap'); 2 | /*-------------------------------------------------------------------------- 3 | Formatting for page & standard document content 4 | /*--------------------------------------------------------------------------*/ 5 | 6 | body { 7 | font-family: 'Hind Vadodara', sans-serif; 8 | /* padding-top: 0px; 9 | padding-bottom: 40px; 10 | */ 11 | } 12 | 13 | blockquote { 14 | margin: 0 1em 0 0.25em; 15 | margin-top: 0px; 16 | margin-right: 1em; 17 | margin-bottom: 0px; 18 | margin-left: 0.25em; 19 | padding: 0 .75em 0 1em; 20 | border-left: 1px solid #777; 21 | border-right: 0px solid #777; 22 | } 23 | 24 | /* Format the heading - nicer spacing etc. */ 25 | .masthead { 26 | overflow: hidden; 27 | } 28 | 29 | .masthead .muted a { 30 | text-decoration: none; 31 | color: #999999; 32 | } 33 | 34 | .masthead ul, .masthead li { 35 | margin-bottom: 0px; 36 | } 37 | 38 | .masthead .nav li { 39 | margin-top: 15px; 40 | font-size: 110%; 41 | } 42 | 43 | .masthead h3 { 44 | margin-top: 15px; 45 | margin-bottom: 5px; 46 | font-size: 170%; 47 | } 48 | 49 | /*-------------------------------------------------------------------------- 50 | Formatting fsdocs-content 51 | /*--------------------------------------------------------------------------*/ 52 | 53 | /* Change font sizes for headings etc. */ 54 | #fsdocs-content h1 { 55 | margin: 30px 0px 15px 0px; 56 | /* font-weight: 400; */ 57 | font-size: 2rem; 58 | letter-spacing: 1.78px; 59 | line-height: 2.5rem; 60 | font-weight: 400; 61 | } 62 | 63 | #fsdocs-content h2 { 64 | font-size: 1.6rem; 65 | margin: 20px 0px 10px 0px; 66 | font-weight: 400; 67 | } 68 | 69 | #fsdocs-content h3 { 70 | font-size: 1.2rem; 71 | margin: 15px 0px 10px 0px; 72 | font-weight: 400; 73 | } 74 | 75 | #fsdocs-content hr { 76 | margin: 0px 0px 20px 0px; 77 | } 78 | 79 | #fsdocs-content li { 80 | font-size: 1.0rem; 81 | line-height: 1.375rem; 82 | letter-spacing: 0.01px; 83 | font-weight: 500; 84 | margin: 0px 0px 15px 0px; 85 | } 86 | 87 | #fsdocs-content p { 88 | font-size: 1.0rem; 89 | line-height: 1.375rem; 90 | letter-spacing: 0.01px; 91 | font-weight: 500; 92 | color: var(--fsdocs-text-color);; 93 | } 94 | 95 | #fsdocs-content a:not(.btn) { 96 | color: #4974D1; 97 | } 98 | /* remove the default bootstrap bold on dt elements */ 99 | #fsdocs-content dt { 100 | font-weight: normal; 101 | } 102 | 103 | 104 | 105 | /*-------------------------------------------------------------------------- 106 | Formatting tables in fsdocs-content, using learn.microsoft.com tables 107 | /*--------------------------------------------------------------------------*/ 108 | 109 | #fsdocs-content .table { 110 | table-layout: auto; 111 | width: 100%; 112 | font-size: 0.875rem; 113 | } 114 | 115 | #fsdocs-content .table caption { 116 | font-size: 0.8rem; 117 | font-weight: 600; 118 | letter-spacing: 2px; 119 | text-transform: uppercase; 120 | padding: 1.125rem; 121 | border-width: 0 0 1px; 122 | border-style: solid; 123 | border-color: #e3e3e3; 124 | text-align: right; 125 | } 126 | 127 | #fsdocs-content .table td, 128 | #fsdocs-content .table th { 129 | display: table-cell; 130 | word-wrap: break-word; 131 | padding: 0.75rem 1rem 0.75rem 0rem; 132 | line-height: 1.5; 133 | vertical-align: top; 134 | border-top: 1px solid #e3e3e3; 135 | border-right: 0; 136 | border-left: 0; 137 | border-bottom: 0; 138 | border-style: solid; 139 | } 140 | 141 | /* suppress the top line on inner lists such as tables of exceptions */ 142 | #fsdocs-content .table .fsdocs-exception-list td, 143 | #fsdocs-content .table .fsdocs-exception-list th { 144 | border-top: 0 145 | } 146 | 147 | #fsdocs-content .table td p:first-child, 148 | #fsdocs-content .table th p:first-child { 149 | margin-top: 0; 150 | } 151 | 152 | #fsdocs-content .table td.nowrap, 153 | #fsdocs-content .table th.nowrap { 154 | white-space: nowrap; 155 | } 156 | 157 | #fsdocs-content .table td.is-narrow, 158 | #fsdocs-content .table th.is-narrow { 159 | width: 15%; 160 | } 161 | 162 | #fsdocs-content .table th:not([scope='row']) { 163 | border-top: 0; 164 | border-bottom: 1px; 165 | } 166 | 167 | #fsdocs-content .table > caption + thead > tr:first-child > td, 168 | #fsdocs-content .table > colgroup + thead > tr:first-child > td, 169 | #fsdocs-content .table > thead:first-child > tr:first-child > td { 170 | border-top: 0; 171 | } 172 | 173 | #fsdocs-content .table table-striped > tbody > tr:nth-of-type(odd) { 174 | background-color: var(--box-shadow-light); 175 | } 176 | 177 | #fsdocs-content .table.min { 178 | width: unset; 179 | } 180 | 181 | #fsdocs-content .table.is-left-aligned td:first-child, 182 | #fsdocs-content .table.is-left-aligned th:first-child { 183 | padding-left: 0; 184 | } 185 | 186 | #fsdocs-content .table.is-left-aligned td:first-child a, 187 | #fsdocs-content .table.is-left-aligned th:first-child a { 188 | outline-offset: -0.125rem; 189 | } 190 | 191 | @media screen and (max-width: 767px), screen and (min-resolution: 120dpi) and (max-width: 767.9px) { 192 | #fsdocs-content .table.is-stacked-mobile td:nth-child(1) { 193 | display: block; 194 | width: 100%; 195 | padding: 1rem 0; 196 | } 197 | 198 | #fsdocs-content .table.is-stacked-mobile td:not(:nth-child(1)) { 199 | display: block; 200 | border-width: 0; 201 | padding: 0 0 1rem; 202 | } 203 | } 204 | 205 | #fsdocs-content .table.has-inner-borders th, 206 | #fsdocs-content .table.has-inner-borders td { 207 | border-right: 1px solid #e3e3e3; 208 | } 209 | 210 | #fsdocs-content .table.has-inner-borders th:last-child, 211 | #fsdocs-content .table.has-inner-borders td:last-child { 212 | border-right: none; 213 | } 214 | 215 | .fsdocs-entity-list .fsdocs-entity-name { 216 | width: 25%; 217 | font-weight: bold; 218 | } 219 | 220 | .fsdocs-member-list .fsdocs-member-usage { 221 | width: 35%; 222 | } 223 | 224 | /*-------------------------------------------------------------------------- 225 | Formatting xmldoc sections in fsdocs-content 226 | /*--------------------------------------------------------------------------*/ 227 | 228 | .fsdocs-xmldoc, .fsdocs-entity-xmldoc, .fsdocs-member-xmldoc { 229 | font-size: 1.0rem; 230 | line-height: 1.375rem; 231 | letter-spacing: 0.01px; 232 | font-weight: 500; 233 | color: var(--fsdocs-text-color);; 234 | } 235 | 236 | .fsdocs-xmldoc h1 { 237 | font-size: 1.2rem; 238 | margin: 10px 0px 0px 0px; 239 | } 240 | 241 | .fsdocs-xmldoc h2 { 242 | font-size: 1.2rem; 243 | margin: 10px 0px 0px 0px; 244 | } 245 | 246 | .fsdocs-xmldoc h3 { 247 | font-size: 1.1rem; 248 | margin: 10px 0px 0px 0px; 249 | } 250 | 251 | /* #fsdocs-nav .searchbox { 252 | margin-top: 30px; 253 | margin-bottom: 30px; 254 | } */ 255 | 256 | #fsdocs-nav img.logo{ 257 | width:90%; 258 | /* height:140px; */ 259 | /* margin:10px 0px 0px 20px; */ 260 | margin-top:40px; 261 | border-style:none; 262 | } 263 | 264 | #fsdocs-nav input{ 265 | /* margin-left: 20px; */ 266 | margin-right: 20px; 267 | margin-top: 20px; 268 | margin-bottom: 20px; 269 | width: 93%; 270 | -webkit-border-radius: 0; 271 | border-radius: 0; 272 | } 273 | 274 | #fsdocs-nav { 275 | /* margin-left: -5px; */ 276 | /* width: 90%; */ 277 | font-size:0.95rem; 278 | } 279 | 280 | #fsdocs-nav li.nav-header{ 281 | /* margin-left: -5px; */ 282 | /* width: 90%; */ 283 | padding-left: 0; 284 | color: var(--fsdocs-text-color);; 285 | text-transform: none; 286 | font-size:16px; 287 | margin-top: 9px; 288 | font-weight: bold; 289 | } 290 | 291 | #fsdocs-nav a{ 292 | padding-left: 0; 293 | color: #6c6c6d; 294 | /* margin-left: 5px; */ 295 | /* width: 90%; */ 296 | } 297 | 298 | /*-------------------------------------------------------------------------- 299 | Formatting pre and code sections in fsdocs-content (code highlighting is 300 | further below) 301 | /*--------------------------------------------------------------------------*/ 302 | 303 | #fsdocs-content code { 304 | /* font-size: 0.83rem; */ 305 | font: 0.85rem 'Roboto Mono', monospace; 306 | background-color: #f7f7f900; 307 | border: 0px; 308 | padding: 0px; 309 | /* word-wrap: break-word; */ 310 | /* white-space: pre; */ 311 | } 312 | 313 | /* omitted */ 314 | #fsdocs-content span.omitted { 315 | background: #3c4e52; 316 | border-radius: 5px; 317 | color: #808080; 318 | padding: 0px 0px 1px 0px; 319 | } 320 | 321 | #fsdocs-content pre .fssnip code { 322 | font: 0.86rem 'Roboto Mono', monospace; 323 | } 324 | 325 | #fsdocs-content table.pre, 326 | #fsdocs-content pre.fssnip, 327 | #fsdocs-content pre { 328 | line-height: 13pt; 329 | border: 0px solid var(--fsdocs-pre-border-color); 330 | border-top: 0px solid var(--fsdocs-pre-border-color-top); 331 | border-collapse: separate; 332 | white-space: pre; 333 | font: 0.86rem 'Roboto Mono', monospace; 334 | width: 100%; 335 | margin: 10px 0px 20px 0px; 336 | background-color: var(--fsdocs-pre-background-color); 337 | padding: 10px; 338 | border-radius: 5px; 339 | color: var(--fsdocs-pre-color); 340 | max-width: none; 341 | box-sizing: border-box; 342 | } 343 | 344 | #fsdocs-content pre.fssnip code { 345 | font: 0.86rem 'Roboto Mono', monospace; 346 | font-weight: 600; 347 | } 348 | 349 | #fsdocs-content table.pre { 350 | background-color: var(--fsdocs-table-pre-background-color);; 351 | } 352 | 353 | #fsdocs-content table.pre pre { 354 | padding: 0px; 355 | margin: 0px; 356 | border-radius: 0px; 357 | width: 100%; 358 | background-color: var(--fsdocs-table-pre-background-color); 359 | color: var(--fsdocs-table-pre-color); 360 | } 361 | 362 | #fsdocs-content table.pre td { 363 | padding: 0px; 364 | white-space: normal; 365 | margin: 0px; 366 | width: 100%; 367 | } 368 | 369 | #fsdocs-content table.pre td.lines { 370 | width: 30px; 371 | } 372 | 373 | 374 | #fsdocs-content pre { 375 | word-wrap: inherit; 376 | } 377 | 378 | .fsdocs-example-header { 379 | font-size: 1.0rem; 380 | line-height: 1.375rem; 381 | letter-spacing: 0.01px; 382 | font-weight: 700; 383 | color: var(--fsdocs-text-color);; 384 | } 385 | 386 | /*-------------------------------------------------------------------------- 387 | Formatting github source links 388 | /*--------------------------------------------------------------------------*/ 389 | 390 | .fsdocs-source-link { 391 | float: right; 392 | text-decoration: none; 393 | } 394 | 395 | .fsdocs-source-link img { 396 | border-style: none; 397 | margin-left: 10px; 398 | width: auto; 399 | height: 1.4em; 400 | } 401 | 402 | .fsdocs-source-link .hover { 403 | display: none; 404 | } 405 | 406 | .fsdocs-source-link:hover .hover { 407 | display: block; 408 | } 409 | 410 | .fsdocs-source-link .normal { 411 | display: block; 412 | } 413 | 414 | .fsdocs-source-link:hover .normal { 415 | display: none; 416 | } 417 | 418 | /*-------------------------------------------------------------------------- 419 | Formatting logo 420 | /*--------------------------------------------------------------------------*/ 421 | 422 | #fsdocs-logo { 423 | width:40px; 424 | height:40px; 425 | margin:10px 0px 0px 0px; 426 | border-style:none; 427 | } 428 | 429 | /*-------------------------------------------------------------------------- 430 | 431 | /*--------------------------------------------------------------------------*/ 432 | 433 | #fsdocs-content table.pre pre { 434 | padding: 0px; 435 | margin: 0px; 436 | border: none; 437 | } 438 | 439 | /*-------------------------------------------------------------------------- 440 | Remove formatting from links 441 | /*--------------------------------------------------------------------------*/ 442 | 443 | #fsdocs-content h1 a, 444 | #fsdocs-content h1 a:hover, 445 | #fsdocs-content h1 a:focus, 446 | #fsdocs-content h2 a, 447 | #fsdocs-content h2 a:hover, 448 | #fsdocs-content h2 a:focus, 449 | #fsdocs-content h3 a, 450 | #fsdocs-content h3 a:hover, 451 | #fsdocs-content h3 a:focus, 452 | #fsdocs-content h4 a, 453 | #fsdocs-content h4 a:hover, #fsdocs-content 454 | #fsdocs-content h4 a:focus, 455 | #fsdocs-content h5 a, 456 | #fsdocs-content h5 a:hover, 457 | #fsdocs-content h5 a:focus, 458 | #fsdocs-content h6 a, 459 | #fsdocs-content h6 a:hover, 460 | #fsdocs-content h6 a:focus { 461 | color: var(--fsdocs-text-color);; 462 | text-decoration: none; 463 | text-decoration-style: none; 464 | /* outline: none */ 465 | } 466 | 467 | /*-------------------------------------------------------------------------- 468 | Formatting for F# code snippets 469 | /*--------------------------------------------------------------------------*/ 470 | 471 | .fsdocs-param-name, 472 | .fsdocs-return-name, 473 | .fsdocs-param { 474 | font-weight: 900; 475 | font-size: 0.85rem; 476 | font-family: 'Roboto Mono', monospace; 477 | } 478 | /* strings --- and stlyes for other string related formats */ 479 | #fsdocs-content span.s { 480 | color: var(--fsdocs-code-strings-color); 481 | } 482 | /* printf formatters */ 483 | #fsdocs-content span.pf { 484 | color: var(--fsdocs-code-printf-color); 485 | } 486 | /* escaped chars */ 487 | #fsdocs-content span.e { 488 | color: var(--fsdocs-code-escaped-color); 489 | } 490 | 491 | /* identifiers --- and styles for more specific identifier types */ 492 | #fsdocs-content span.id { 493 | color: var(--fsdocs-identifiers-color);; 494 | } 495 | /* module */ 496 | #fsdocs-content span.m { 497 | color:var(--fsdocs-code-module-color); 498 | } 499 | /* reference type */ 500 | #fsdocs-content span.rt { 501 | color: var(--fsdocs-code-reference-color); 502 | } 503 | /* value type */ 504 | #fsdocs-content span.vt { 505 | color: var(--fsdocs-code-value-color); 506 | } 507 | /* interface */ 508 | #fsdocs-content span.if { 509 | color: var(--fsdocs-code-interface-color); 510 | } 511 | /* type argument */ 512 | #fsdocs-content span.ta { 513 | color: var(--fsdocs-code-typearg-color); 514 | } 515 | /* disposable */ 516 | #fsdocs-content span.d { 517 | color: var(--fsdocs-code-disposable-color); 518 | } 519 | /* property */ 520 | #fsdocs-content span.prop { 521 | color: var(--fsdocs-code-property-color); 522 | } 523 | /* punctuation */ 524 | #fsdocs-content span.p { 525 | color: var(--fsdocs-code-punctuation-color); 526 | } 527 | #fsdocs-content span.pn { 528 | color: var(--fsdocs-code-punctuation2-color); 529 | } 530 | /* function */ 531 | #fsdocs-content span.f { 532 | color: var(--fsdocs-code-function-color); 533 | } 534 | #fsdocs-content span.fn { 535 | color: var(--fsdocs-code-function2-color); 536 | } 537 | /* active pattern */ 538 | #fsdocs-content span.pat { 539 | color: var(--fsdocs-code-activepattern-color); 540 | } 541 | /* union case */ 542 | #fsdocs-content span.u { 543 | color: var(--fsdocs-code-unioncase-color); 544 | } 545 | /* enumeration */ 546 | #fsdocs-content span.e { 547 | color: var(--fsdocs-code-enumeration-color); 548 | } 549 | /* keywords */ 550 | #fsdocs-content span.k { 551 | color: var(--fsdocs-code-keywords-color); 552 | /* font-weight: bold; */ 553 | } 554 | /* comment */ 555 | #fsdocs-content span.c { 556 | color: var(--fsdocs-code-comment-color); 557 | font-weight: 400; 558 | font-style: italic; 559 | } 560 | /* operators */ 561 | #fsdocs-content span.o { 562 | color: var(--fsdocs-code-operators-color); 563 | } 564 | /* numbers */ 565 | #fsdocs-content span.n { 566 | color: var(--fsdocs-code-numbers-color); 567 | } 568 | /* line number */ 569 | #fsdocs-content span.l { 570 | color: var(--fsdocs-code-linenumbers-color); 571 | } 572 | /* mutable var or ref cell */ 573 | #fsdocs-content span.v { 574 | color: var(--fsdocs-code-mutable-color); 575 | font-weight: bold; 576 | } 577 | /* inactive code */ 578 | #fsdocs-content span.inactive { 579 | color: var(--fsdocs-code-inactive-color); 580 | } 581 | /* preprocessor */ 582 | #fsdocs-content span.prep { 583 | color: var(--fsdocs-code-preprocessor-color); 584 | } 585 | /* fsi output */ 586 | #fsdocs-content span.fsi { 587 | color: var(--fsdocs-code-fsioutput-color); 588 | } 589 | 590 | /* tool tip */ 591 | div.fsdocs-tip { 592 | background: #475b5f; 593 | border-radius: 4px; 594 | font: 0.85rem 'Roboto Mono', monospace; 595 | padding: 6px 8px 6px 8px; 596 | display: none; 597 | color: var(--fsdocs-code-tooltip-color); 598 | pointer-events: none; 599 | } 600 | 601 | div.fsdocs-tip code { 602 | color: var(--fsdocs-code-tooltip-color); 603 | font: 0.85rem 'Roboto Mono', monospace; 604 | } 605 | -------------------------------------------------------------------------------- /src/Cosmos/Replace.fs: -------------------------------------------------------------------------------- 1 | [] 2 | module FSharp.Azure.Cosmos.Replace 3 | 4 | open Microsoft.Azure.Cosmos 5 | 6 | [] 7 | type ReplaceOperation<'T> = { 8 | Item : 'T 9 | Id : string 10 | PartitionKey : PartitionKey voption 11 | RequestOptions : ItemRequestOptions 12 | } 13 | 14 | [] 15 | type ReplaceConcurrentlyOperation<'T, 'E> = { 16 | Id : string 17 | PartitionKey : PartitionKey voption 18 | RequestOptions : ItemRequestOptions 19 | Update : 'T -> Async> 20 | } 21 | 22 | open System 23 | 24 | type ReplaceBuilder<'T> (enableContentResponseOnWrite : bool) = 25 | member _.Yield _ = 26 | { 27 | Item = Unchecked.defaultof<_> 28 | Id = String.Empty 29 | PartitionKey = ValueNone 30 | RequestOptions = ItemRequestOptions (EnableContentResponseOnWrite = enableContentResponseOnWrite) 31 | } 32 | : ReplaceOperation<'T> 33 | 34 | /// Sets the item being to replace existing with 35 | [] 36 | member _.Item (state : ReplaceOperation<_>, item) = { state with Item = item } 37 | 38 | /// Sets the item being to replace existing with 39 | [] 40 | member _.Id (state : ReplaceOperation<_>, id) = { state with Id = id } 41 | 42 | /// Sets the partition key 43 | [] 44 | member _.PartitionKey (state : ReplaceOperation<_>, partitionKey : PartitionKey) = { 45 | state with 46 | PartitionKey = ValueSome partitionKey 47 | } 48 | 49 | /// Sets the partition key 50 | [] 51 | member _.PartitionKey (state : ReplaceOperation<_>, partitionKey : string) = { 52 | state with 53 | PartitionKey = ValueSome (PartitionKey partitionKey) 54 | } 55 | 56 | /// Sets the request options 57 | [] 58 | member _.RequestOptions (state : ReplaceOperation<_>, options : ItemRequestOptions) = { state with RequestOptions = options } 59 | 60 | /// Sets the eTag to 61 | [] 62 | member _.ETag (state : ReplaceOperation<_>, eTag : string) = 63 | state.RequestOptions.IfMatchEtag <- eTag 64 | state 65 | 66 | // ------------------------------------------- Request options ------------------------------------------- 67 | /// Sets the operation 68 | [] 69 | member _.ConsistencyLevel (state : ReplaceOperation<_>, consistencyLevel : ConsistencyLevel Nullable) = 70 | state.RequestOptions.ConsistencyLevel <- consistencyLevel 71 | state 72 | 73 | /// Sets if the response should include the content of the item after the operation 74 | [] 75 | member _.EnableContentResponseOnWrite (state : ReplaceOperation<_>, enableContentResponseOnWrite : bool) = 76 | state.RequestOptions.EnableContentResponseOnWrite <- enableContentResponseOnWrite 77 | state 78 | 79 | /// Sets the indexing directive 80 | [] 81 | member _.IndexingDirective (state : ReplaceOperation<_>, indexingDirective : IndexingDirective Nullable) = 82 | state.RequestOptions.IndexingDirective <- indexingDirective 83 | state 84 | 85 | /// Adds a trigger to be invoked before the operation 86 | [] 87 | member _.PreTrigger (state : ReplaceOperation<_>, trigger : string) = 88 | state.RequestOptions.AddPreTrigger trigger 89 | state 90 | 91 | /// Adds triggers to be invoked before the operation 92 | [] 93 | member _.PreTriggers (state : ReplaceOperation<_>, triggers : seq) = 94 | state.RequestOptions.AddPreTriggers triggers 95 | state 96 | 97 | /// Adds a trigger to be invoked after the operation 98 | [] 99 | member _.PostTrigger (state : ReplaceOperation<_>, trigger : string) = 100 | state.RequestOptions.AddPostTrigger trigger 101 | state 102 | 103 | /// Adds triggers to be invoked after the operation 104 | [] 105 | member _.PostTriggers (state : ReplaceOperation<_>, triggers : seq) = 106 | state.RequestOptions.AddPostTriggers triggers 107 | state 108 | 109 | /// Sets the session token 110 | [] 111 | member _.SessionToken (state : ReplaceOperation<_>, sessionToken : string) = 112 | state.RequestOptions.SessionToken <- sessionToken 113 | state 114 | 115 | type ReplaceConcurrentlyBuilder<'T, 'E> (enableContentResponseOnWrite : bool) = 116 | member _.Yield _ = 117 | { 118 | Id = String.Empty 119 | PartitionKey = ValueNone 120 | RequestOptions = ItemRequestOptions (EnableContentResponseOnWrite = enableContentResponseOnWrite) 121 | Update = 122 | fun _ -> 123 | raise 124 | <| MissingMethodException ("Update function is not set for concurrent replace operation") 125 | } 126 | : ReplaceConcurrentlyOperation<'T, 'E> 127 | 128 | /// Sets the item being to replace existing with 129 | [] 130 | member _.Id (state : ReplaceConcurrentlyOperation<_, _>, id) = { state with Id = id } 131 | 132 | /// Sets the partition key 133 | [] 134 | member _.PartitionKey (state : ReplaceConcurrentlyOperation<_, _>, partitionKey : PartitionKey) = { 135 | state with 136 | PartitionKey = ValueSome partitionKey 137 | } 138 | 139 | /// Sets the partition key 140 | [] 141 | member _.PartitionKey (state : ReplaceConcurrentlyOperation<_, _>, partitionKey : string) = { 142 | state with 143 | PartitionKey = ValueSome (PartitionKey partitionKey) 144 | } 145 | 146 | /// Sets the request options 147 | [] 148 | member _.RequestOptions (state : ReplaceConcurrentlyOperation<_, _>, options : ItemRequestOptions) = 149 | options.EnableContentResponseOnWrite <- enableContentResponseOnWrite 150 | { state with RequestOptions = options } 151 | 152 | /// Sets the partition key 153 | [] 154 | member _.Update (state : ReplaceConcurrentlyOperation<_, _>, update : 'T -> Async>) = { 155 | state with 156 | Update = update 157 | } 158 | 159 | // ------------------------------------------- Request options ------------------------------------------- 160 | /// Sets the operation 161 | [] 162 | member _.ConsistencyLevel (state : ReplaceConcurrentlyOperation<_, _>, consistencyLevel : ConsistencyLevel Nullable) = 163 | state.RequestOptions.ConsistencyLevel <- consistencyLevel 164 | state 165 | 166 | /// Sets if the response should include the content of the item after the operation 167 | [] 168 | member _.EnableContentResponseOnWrite (state : ReplaceConcurrentlyOperation<_, _>, enableContentResponseOnWrite : bool) = 169 | state.RequestOptions.EnableContentResponseOnWrite <- enableContentResponseOnWrite 170 | state 171 | 172 | /// Sets the indexing directive 173 | [] 174 | member _.IndexingDirective (state : ReplaceConcurrentlyOperation<_, _>, indexingDirective : IndexingDirective Nullable) = 175 | state.RequestOptions.IndexingDirective <- indexingDirective 176 | state 177 | 178 | /// Adds a trigger to be invoked before the operation 179 | [] 180 | member _.PreTrigger (state : ReplaceConcurrentlyOperation<_, _>, trigger : string) = 181 | state.RequestOptions.AddPreTrigger trigger 182 | state 183 | 184 | /// Adds triggers to be invoked before the operation 185 | [] 186 | member _.PreTriggers (state : ReplaceConcurrentlyOperation<_, _>, triggers : seq) = 187 | state.RequestOptions.AddPreTriggers triggers 188 | state 189 | 190 | /// Adds a trigger to be invoked after the operation 191 | [] 192 | member _.PostTrigger (state : ReplaceConcurrentlyOperation<_, _>, trigger : string) = 193 | state.RequestOptions.AddPostTrigger trigger 194 | state 195 | 196 | /// Adds triggers to be invoked after the operation 197 | [] 198 | member _.PostTriggers (state : ReplaceConcurrentlyOperation<_, _>, triggers : seq) = 199 | state.RequestOptions.AddPostTriggers triggers 200 | state 201 | 202 | /// Sets the session token 203 | [] 204 | member _.SessionToken (state : ReplaceConcurrentlyOperation<_, _>, sessionToken : string) = 205 | state.RequestOptions.SessionToken <- sessionToken 206 | state 207 | 208 | let replace<'T> = ReplaceBuilder<'T> (false) 209 | let replaceAndRead<'T> = ReplaceBuilder<'T> (true) 210 | 211 | let replaceConcurrenly<'T, 'E> = ReplaceConcurrentlyBuilder<'T, 'E> (false) 212 | let replaceConcurrenlyAndRead<'T, 'E> = ReplaceConcurrentlyBuilder<'T, 'E> (true) 213 | 214 | // https://docs.microsoft.com/en-us/rest/api/cosmos-db/http-status-codes-for-cosmosdb 215 | 216 | /// Represents the result of a replace operation. 217 | type ReplaceResult<'T> = 218 | | Ok of 'T // 200 219 | | BadRequest of ResponseBody : string // 400 220 | | NotFound of ResponseBody : string // 404 221 | /// Precondition failed 222 | | ModifiedBefore of ResponseBody : string // 412 - need re-do 223 | | EntityTooLarge of ResponseBody : string // 413 224 | | TooManyRequests of ResponseBody : string * RetryAfter : TimeSpan voption // 429 225 | 226 | /// Represents the result of a replace operation. 227 | type ReplaceConcurrentResult<'T, 'E> = 228 | | Ok of 'T // 200 229 | | BadRequest of ResponseBody : string // 400 230 | | NotFound of ResponseBody : string // 404 231 | /// Precondition failed 232 | | ModifiedBefore of ResponseBody : string // 412 - need re-do 233 | | EntityTooLarge of ResponseBody : string // 413 234 | | TooManyRequests of ResponseBody : string * RetryAfter : TimeSpan voption // 429 235 | | CustomError of Error : 'E 236 | 237 | open System.Net 238 | 239 | module CosmosException = 240 | 241 | let toReplaceResult (ex : CosmosException) = 242 | match ex.StatusCode with 243 | | HttpStatusCode.BadRequest -> ReplaceResult.BadRequest ex.ResponseBody 244 | | HttpStatusCode.NotFound -> ReplaceResult.NotFound ex.ResponseBody 245 | | HttpStatusCode.PreconditionFailed -> ReplaceResult.ModifiedBefore ex.ResponseBody 246 | | HttpStatusCode.RequestEntityTooLarge -> ReplaceResult.EntityTooLarge ex.ResponseBody 247 | | HttpStatusCode.TooManyRequests -> 248 | ReplaceResult.TooManyRequests (ex.ResponseBody, ex.RetryAfter |> ValueOption.ofNullable) 249 | | _ -> raise ex 250 | 251 | let toReplaceConcurrentlyErrorResult (ex : CosmosException) = 252 | match ex.StatusCode with 253 | | HttpStatusCode.NotFound -> ReplaceConcurrentResult.NotFound ex.ResponseBody 254 | | HttpStatusCode.BadRequest -> ReplaceConcurrentResult.BadRequest ex.ResponseBody 255 | | HttpStatusCode.PreconditionFailed -> ReplaceConcurrentResult.ModifiedBefore ex.ResponseBody 256 | | HttpStatusCode.RequestEntityTooLarge -> ReplaceConcurrentResult.EntityTooLarge ex.ResponseBody 257 | | HttpStatusCode.TooManyRequests -> 258 | ReplaceConcurrentResult.TooManyRequests (ex.ResponseBody, ex.RetryAfter |> ValueOption.ofNullable) 259 | | _ -> raise ex 260 | 261 | open System.Threading 262 | open System.Threading.Tasks 263 | open CosmosException 264 | 265 | let rec executeConcurrentlyAsync<'value, 'error> 266 | (ct : CancellationToken) 267 | (container : Container) 268 | (operation : ReplaceConcurrentlyOperation<'value, 'error>) 269 | (retryAttempts : int) 270 | : Task>> = 271 | task { 272 | try 273 | let partitionKey = 274 | match operation.PartitionKey with 275 | | ValueSome partitionKey -> partitionKey 276 | | ValueNone -> PartitionKey.None 277 | 278 | let! response = container.ReadItemAsync<'value> (operation.Id, partitionKey, cancellationToken = ct) 279 | let eTag = response.ETag 280 | let! itemUpdateResult = operation.Update response.Resource 281 | 282 | match itemUpdateResult with 283 | | Result.Error e -> return CosmosResponse.fromItemResponse (fun _ -> CustomError e) response 284 | | Result.Ok item -> 285 | let updateOptions = new ItemRequestOptions (IfMatchEtag = eTag) 286 | 287 | let! response = 288 | container.ReplaceItemAsync<'value> ( 289 | item, 290 | operation.Id, 291 | requestOptions = updateOptions, 292 | cancellationToken = ct 293 | ) 294 | 295 | return CosmosResponse.fromItemResponse Ok response 296 | with 297 | | HandleException ex when 298 | ex.StatusCode = HttpStatusCode.PreconditionFailed 299 | && retryAttempts = 1 300 | -> 301 | return CosmosResponse.fromException toReplaceConcurrentlyErrorResult ex 302 | | HandleException ex when ex.StatusCode = HttpStatusCode.PreconditionFailed -> 303 | return! executeConcurrentlyAsync ct container operation (retryAttempts - 1) 304 | | HandleException ex -> return CosmosResponse.fromException toReplaceConcurrentlyErrorResult ex 305 | } 306 | 307 | open System.Runtime.InteropServices 308 | 309 | [] 310 | let DefaultRetryCount = 10 311 | 312 | type Microsoft.Azure.Cosmos.Container with 313 | 314 | /// 315 | /// Executes a replace operation and returns . 316 | /// 317 | /// Replace operation. 318 | /// Cancellation token. 319 | member container.PlainExecuteAsync<'T> 320 | (operation : ReplaceOperation<'T>, [] cancellationToken : CancellationToken) 321 | = 322 | container.ReplaceItemAsync<'T> ( 323 | operation.Item, 324 | operation.Id, 325 | operation.PartitionKey |> ValueOption.toNullable, 326 | operation.RequestOptions, 327 | cancellationToken = cancellationToken 328 | ) 329 | 330 | /// 331 | /// Executes a replace operation, transforms success or failure, and returns . 332 | /// 333 | /// Replace operation 334 | /// Result transform if success 335 | /// Error transform if failure 336 | /// Cancellation token 337 | member container.ExecuteOverwriteAsync<'T, 'Result> 338 | (operation : ReplaceOperation<'T>, success, failure, [] cancellationToken : CancellationToken) 339 | : Task> 340 | = 341 | task { 342 | try 343 | let! response = container.PlainExecuteAsync (operation, cancellationToken) 344 | return CosmosResponse.fromItemResponse success response 345 | with HandleException ex -> 346 | return CosmosResponse.fromException failure ex 347 | } 348 | 349 | /// 350 | /// Executes a replace operation safely and returns . 351 | /// 352 | /// 353 | /// Requires ETag to be set in . 354 | /// 355 | /// Replace operation. 356 | /// Cancellation token. 357 | member container.ExecuteAsync<'T> (operation : ReplaceOperation<'T>, [] cancellationToken : CancellationToken) = 358 | if String.IsNullOrEmpty operation.RequestOptions.IfMatchEtag then 359 | invalidArg "eTag" "Safe replace requires ETag" 360 | 361 | container.ExecuteOverwriteAsync (operation, ReplaceResult.Ok, toReplaceResult, cancellationToken) 362 | 363 | /// 364 | /// Executes a replace operation replacing existing item if it exists and returns . 365 | /// 366 | /// Replace operation. 367 | /// Cancellation token. 368 | member container.ExecuteOverwriteAsync<'T> 369 | (operation : ReplaceOperation<'T>, [] cancellationToken : CancellationToken) 370 | = 371 | container.ExecuteOverwriteAsync (operation, ReplaceResult.Ok, toReplaceResult, cancellationToken) 372 | 373 | /// 374 | /// Executes a replace operation by applying change to item and returns . 375 | /// 376 | /// Replace operation. 377 | /// Max retry count. Default is 10. 378 | /// Cancellation token. 379 | member container.ExecuteConcurrentlyAsync<'T, 'E> 380 | ( 381 | operation : ReplaceConcurrentlyOperation<'T, 'E>, 382 | [] maxRetryCount : int, 383 | [] cancellationToken : CancellationToken 384 | ) 385 | = 386 | executeConcurrentlyAsync<'T, 'E> cancellationToken container operation maxRetryCount 387 | 388 | /// 389 | /// Executes a replace operation by applying change to item and returns . 390 | /// 391 | /// Replace operation. 392 | /// Cancellation token. 393 | member container.ExecuteConcurrentlyAsync<'T, 'E> 394 | (operation : ReplaceConcurrentlyOperation<'T, 'E>, [] cancellationToken : CancellationToken) 395 | = 396 | executeConcurrentlyAsync<'T, 'E> cancellationToken container operation DefaultRetryCount 397 | -------------------------------------------------------------------------------- /src/Cosmos/Upsert.fs: -------------------------------------------------------------------------------- 1 | [] 2 | module FSharp.Azure.Cosmos.Upsert 3 | 4 | open Microsoft.Azure.Cosmos 5 | 6 | [] 7 | type UpsertOperation<'T> = { 8 | Item : 'T 9 | PartitionKey : PartitionKey voption 10 | RequestOptions : ItemRequestOptions 11 | } 12 | 13 | [] 14 | type UpsertConcurrentlyOperation<'T, 'E> = { 15 | Id : string 16 | PartitionKey : PartitionKey voption 17 | RequestOptions : ItemRequestOptions 18 | UpdateOrCreate : 'T option -> Async> 19 | } 20 | 21 | open System 22 | 23 | type UpsertBuilder<'T> (enableContentResponseOnWrite : bool) = 24 | member _.Yield _ = 25 | { 26 | Item = Unchecked.defaultof<_> 27 | PartitionKey = ValueNone 28 | RequestOptions = ItemRequestOptions (EnableContentResponseOnWrite = enableContentResponseOnWrite) 29 | } 30 | : UpsertOperation<'T> 31 | 32 | /// Sets the item being creeated 33 | [] 34 | member _.Item (state : UpsertOperation<_>, item) = { state with Item = item } 35 | 36 | /// Sets the partition key 37 | [] 38 | member _.PartitionKey (state : UpsertOperation<_>, partitionKey : PartitionKey) = { 39 | state with 40 | PartitionKey = ValueSome partitionKey 41 | } 42 | 43 | /// Sets the partition key 44 | [] 45 | member _.PartitionKey (state : UpsertOperation<_>, partitionKey : string) = { 46 | state with 47 | PartitionKey = ValueSome (PartitionKey partitionKey) 48 | } 49 | 50 | /// Sets the request options 51 | [] 52 | member _.RequestOptions (state : UpsertOperation<_>, options : ItemRequestOptions) = 53 | options.EnableContentResponseOnWrite <- enableContentResponseOnWrite 54 | { state with RequestOptions = options } 55 | 56 | /// Sets the eTag to 57 | [] 58 | member _.ETag (state : UpsertOperation<_>, eTag : string) = 59 | state.RequestOptions.IfMatchEtag <- eTag 60 | state 61 | 62 | // ------------------------------------------- Request options ------------------------------------------- 63 | /// Sets the operation 64 | [] 65 | member _.ConsistencyLevel (state : UpsertOperation<_>, consistencyLevel : ConsistencyLevel Nullable) = 66 | state.RequestOptions.ConsistencyLevel <- consistencyLevel 67 | state 68 | 69 | /// Sets if the response should include the content of the item after the operation 70 | [] 71 | member _.EnableContentResponseOnWrite (state : UpsertOperation<_>, enableContentResponseOnWrite : bool) = 72 | state.RequestOptions.EnableContentResponseOnWrite <- enableContentResponseOnWrite 73 | state 74 | 75 | /// Sets the indexing directive 76 | [] 77 | member _.IndexingDirective (state : UpsertOperation<_>, indexingDirective : IndexingDirective Nullable) = 78 | state.RequestOptions.IndexingDirective <- indexingDirective 79 | state 80 | 81 | /// Adds a trigger to be invoked before the operation 82 | [] 83 | member _.PreTrigger (state : UpsertOperation<_>, trigger : string) = 84 | state.RequestOptions.AddPreTrigger trigger 85 | state 86 | 87 | /// Adds triggers to be invoked before the operation 88 | [] 89 | member _.PreTriggers (state : UpsertOperation<_>, triggers : seq) = 90 | state.RequestOptions.AddPreTriggers triggers 91 | state 92 | 93 | /// Adds a trigger to be invoked after the operation 94 | [] 95 | member _.PostTrigger (state : UpsertOperation<_>, trigger : string) = 96 | state.RequestOptions.AddPostTrigger trigger 97 | state 98 | 99 | /// Adds triggers to be invoked after the operation 100 | [] 101 | member _.PostTriggers (state : UpsertOperation<_>, triggers : seq) = 102 | state.RequestOptions.AddPostTriggers triggers 103 | state 104 | 105 | /// Sets the session token 106 | [] 107 | member _.SessionToken (state : UpsertOperation<_>, sessionToken : string) = 108 | state.RequestOptions.SessionToken <- sessionToken 109 | state 110 | 111 | type UpsertConcurrentlyBuilder<'T, 'E> (enableContentResponseOnWrite : bool) = 112 | member _.Yield _ = 113 | { 114 | Id = String.Empty 115 | PartitionKey = ValueNone 116 | RequestOptions = ItemRequestOptions (EnableContentResponseOnWrite = enableContentResponseOnWrite) 117 | UpdateOrCreate = 118 | function 119 | | _ -> 120 | raise 121 | <| MissingMethodException ("Update function is not set for concurrent upsert operation") 122 | } 123 | : UpsertConcurrentlyOperation<'T, 'E> 124 | 125 | /// Sets the item being to upsert existing with 126 | [] 127 | member _.Id (state : UpsertConcurrentlyOperation<_, _>, id) = { state with Id = id } 128 | 129 | /// Sets the partition key 130 | [] 131 | member _.PartitionKey (state : UpsertConcurrentlyOperation<_, _>, partitionKey : PartitionKey) = { 132 | state with 133 | PartitionKey = ValueSome partitionKey 134 | } 135 | 136 | /// Sets the partition key 137 | [] 138 | member _.PartitionKey (state : UpsertConcurrentlyOperation<_, _>, partitionKey : string) = { 139 | state with 140 | PartitionKey = ValueSome (PartitionKey partitionKey) 141 | } 142 | 143 | /// Sets the request options 144 | [] 145 | member _.RequestOptions (state : UpsertConcurrentlyOperation<_, _>, options : ItemRequestOptions) = 146 | options.EnableContentResponseOnWrite <- enableContentResponseOnWrite 147 | { state with RequestOptions = options } 148 | 149 | /// Sets the partition key 150 | [] 151 | member _.UpdateOrCreate (state : UpsertConcurrentlyOperation<_, _>, update : 'T option -> Async>) = { 152 | state with 153 | UpdateOrCreate = update 154 | } 155 | 156 | // ------------------------------------------- Request options ------------------------------------------- 157 | /// Sets the operation 158 | [] 159 | member _.ConsistencyLevel (state : UpsertConcurrentlyOperation<_, _>, consistencyLevel : ConsistencyLevel Nullable) = 160 | state.RequestOptions.ConsistencyLevel <- consistencyLevel 161 | state 162 | 163 | /// Sets if the response should include the content of the item after the operation 164 | [] 165 | member _.EnableContentResponseOnWrite (state : UpsertConcurrentlyOperation<_, _>, enableContentResponseOnWrite : bool) = 166 | state.RequestOptions.EnableContentResponseOnWrite <- enableContentResponseOnWrite 167 | state 168 | 169 | /// Sets the indexing directive 170 | [] 171 | member _.IndexingDirective (state : UpsertConcurrentlyOperation<_, _>, indexingDirective : IndexingDirective Nullable) = 172 | state.RequestOptions.IndexingDirective <- indexingDirective 173 | state 174 | 175 | /// Adds a trigger to be invoked before the operation 176 | [] 177 | member _.PreTrigger (state : UpsertConcurrentlyOperation<_, _>, trigger : string) = 178 | state.RequestOptions.AddPreTrigger trigger 179 | state 180 | 181 | /// Adds triggers to be invoked before the operation 182 | [] 183 | member _.PreTriggers (state : UpsertConcurrentlyOperation<_, _>, triggers : seq) = 184 | state.RequestOptions.AddPreTriggers triggers 185 | state 186 | 187 | /// Adds a trigger to be invoked after the operation 188 | [] 189 | member _.PostTrigger (state : UpsertConcurrentlyOperation<_, _>, trigger : string) = 190 | state.RequestOptions.AddPostTrigger trigger 191 | state 192 | 193 | /// Adds triggers to be invoked after the operation 194 | [] 195 | member _.PostTriggers (state : UpsertConcurrentlyOperation<_, _>, triggers : seq) = 196 | state.RequestOptions.AddPostTriggers triggers 197 | state 198 | 199 | /// Sets the session token 200 | [] 201 | member _.SessionToken (state : UpsertConcurrentlyOperation<_, _>, sessionToken : string) = 202 | state.RequestOptions.SessionToken <- sessionToken 203 | state 204 | 205 | let upsert<'T> = UpsertBuilder<'T> (false) 206 | let upsertAndRead<'T> = UpsertBuilder<'T> (true) 207 | 208 | let upsertConcurrenly<'T, 'E> = UpsertConcurrentlyBuilder<'T, 'E> (false) 209 | let upsertConcurrenlyAndRead<'T, 'E> = UpsertConcurrentlyBuilder<'T, 'E> (true) 210 | 211 | // https://docs.microsoft.com/en-us/rest/api/cosmos-db/http-status-codes-for-cosmosdb 212 | 213 | /// Represents the result of an upsert operation. 214 | type UpsertResult<'t> = 215 | | Ok of 't // 200 216 | | BadRequest of ResponseBody : string // 400 217 | | ModifiedBefore of ResponseBody : string // 412 - need re-do 218 | | EntityTooLarge of ResponseBody : string // 413 219 | | TooManyRequests of ResponseBody : string * RetryAfter : TimeSpan voption // 429 220 | 221 | /// Represents the result of an upsert operation. 222 | type UpsertConcurrentResult<'t, 'E> = 223 | | Ok of 't // 200 224 | | BadRequest of ResponseBody : string // 400 225 | | ModifiedBefore of ResponseBody : string //412 - need re-do 226 | | EntityTooLarge of ResponseBody : string // 413 227 | | TooManyRequests of ResponseBody : string * RetryAfter : TimeSpan voption // 429 228 | | CustomError of Error : 'E 229 | 230 | open System.Net 231 | 232 | module CosmosException = 233 | 234 | let toUpsertResult (ex : CosmosException) = 235 | match ex.StatusCode with 236 | | HttpStatusCode.BadRequest -> UpsertResult.BadRequest ex.ResponseBody 237 | | HttpStatusCode.PreconditionFailed -> UpsertResult.ModifiedBefore ex.ResponseBody 238 | | HttpStatusCode.RequestEntityTooLarge -> UpsertResult.EntityTooLarge ex.ResponseBody 239 | | _ -> raise ex 240 | 241 | let toUpsertConcurrentlyErrorResult (ex : CosmosException) = 242 | match ex.StatusCode with 243 | | HttpStatusCode.BadRequest -> UpsertConcurrentResult.BadRequest ex.ResponseBody 244 | | HttpStatusCode.PreconditionFailed -> UpsertConcurrentResult.ModifiedBefore ex.ResponseBody 245 | | HttpStatusCode.RequestEntityTooLarge -> UpsertConcurrentResult.EntityTooLarge ex.ResponseBody 246 | | HttpStatusCode.TooManyRequests -> 247 | UpsertConcurrentResult.TooManyRequests (ex.ResponseBody, ex.RetryAfter |> ValueOption.ofNullable) 248 | | _ -> raise ex 249 | 250 | open System.Threading 251 | open System.Threading.Tasks 252 | open CosmosException 253 | 254 | let rec executeConcurrentlyAsync<'value, 'error> 255 | (ct : CancellationToken) 256 | (container : Container) 257 | (operation : UpsertConcurrentlyOperation<'value, 'error>) 258 | (retryAttempts : int) 259 | : Task>> = 260 | task { 261 | 262 | let! itemResult, response = task { 263 | let partitionKey = 264 | match operation.PartitionKey with 265 | | ValueSome partitionKey -> partitionKey 266 | | ValueNone -> PartitionKey.None 267 | 268 | try 269 | let! response = container.ReadItemAsync<'value> (operation.Id, partitionKey, cancellationToken = ct) 270 | let! itemResult = operation.UpdateOrCreate (Some response.Resource) 271 | return itemResult, Choice1Of2 response 272 | with HandleException ex when ex.StatusCode = HttpStatusCode.NotFound -> 273 | let! itemResult = operation.UpdateOrCreate None 274 | return itemResult, Choice2Of2 ex 275 | } 276 | 277 | try 278 | match itemResult, response with 279 | | Result.Error e, Choice1Of2 response -> return CosmosResponse.fromItemResponse (fun _ -> CustomError e) response 280 | | Result.Error e, Choice2Of2 ex -> return CosmosResponse.fromException (fun _ -> CustomError e) ex 281 | | Result.Ok item, Choice1Of2 response -> 282 | let updateOptions = ItemRequestOptions (IfMatchEtag = response.ETag) 283 | 284 | let! response = 285 | container.UpsertItemAsync<'value> ( 286 | item, 287 | operation.PartitionKey |> ValueOption.toNullable, 288 | requestOptions = updateOptions, 289 | cancellationToken = ct 290 | ) 291 | 292 | return CosmosResponse.fromItemResponse Ok response 293 | | Result.Ok item, Choice2Of2 ex -> 294 | let! response = 295 | container.UpsertItemAsync<'value> ( 296 | item, 297 | operation.PartitionKey |> ValueOption.toNullable, 298 | cancellationToken = ct 299 | ) 300 | 301 | return CosmosResponse.fromItemResponse Ok response 302 | with 303 | | HandleException ex when 304 | ex.StatusCode = HttpStatusCode.PreconditionFailed 305 | && retryAttempts = 1 306 | -> 307 | return CosmosResponse.fromException toUpsertConcurrentlyErrorResult ex 308 | | HandleException ex when ex.StatusCode = HttpStatusCode.PreconditionFailed -> 309 | return! executeConcurrentlyAsync ct container operation (retryAttempts - 1) 310 | | HandleException ex -> return CosmosResponse.fromException toUpsertConcurrentlyErrorResult ex 311 | } 312 | 313 | open System.Runtime.InteropServices 314 | 315 | [] 316 | let DefaultMaxRetryCount = 10 317 | 318 | type Microsoft.Azure.Cosmos.Container with 319 | 320 | /// 321 | /// Executes an upsert operation and returns . 322 | /// 323 | /// Upsert operation. 324 | /// Cancellation token. 325 | member container.PlainExecuteAsync<'T> (operation : UpsertOperation<'T>, [] cancellationToken : CancellationToken) = 326 | container.UpsertItemAsync<'T> ( 327 | operation.Item, 328 | operation.PartitionKey |> ValueOption.toNullable, 329 | operation.RequestOptions, 330 | cancellationToken = cancellationToken 331 | ) 332 | 333 | /// 334 | /// Executes a upsert operation, transforms success or failure, and returns . 335 | /// 336 | /// Upsert operation 337 | /// Result transform if success 338 | /// Error transform if failure 339 | /// Cancellation token 340 | member container.ExecuteOverwriteAsync<'T, 'Result> 341 | (operation : UpsertOperation<'T>, succsess, failure, [] cancellationToken : CancellationToken) 342 | : Task> 343 | = 344 | task { 345 | try 346 | let! response = container.PlainExecuteAsync (operation, cancellationToken) 347 | return CosmosResponse.fromItemResponse succsess response 348 | with HandleException ex -> 349 | return CosmosResponse.fromException failure ex 350 | } 351 | 352 | /// 353 | /// Executes an upsert operation safely and returns . 354 | /// 355 | /// Requires ETag to be set in . 356 | /// 357 | /// 358 | /// Upsert operation. 359 | /// Cancellation token. 360 | member container.ExecuteAsync<'T> (operation : UpsertOperation<'T>, [] cancellationToken : CancellationToken) = 361 | if String.IsNullOrEmpty operation.RequestOptions.IfMatchEtag then 362 | invalidArg "eTag" "Safe replace requires ETag" 363 | 364 | container.ExecuteOverwriteAsync (operation, UpsertResult.Ok, toUpsertResult, cancellationToken) 365 | 366 | /// 367 | /// Executes an upsert operation replacing existing item if it exists and returns . 368 | /// 369 | /// Upsert operation. 370 | /// Cancellation token. 371 | member container.ExecuteOverwriteAsync<'T> 372 | (operation : UpsertOperation<'T>, [] cancellationToken : CancellationToken) 373 | = 374 | container.ExecuteOverwriteAsync (operation, UpsertResult.Ok, toUpsertResult, cancellationToken) 375 | 376 | /// 377 | /// Executes an upsert operation by applying change to item if exists and returns . 378 | /// 379 | /// Upsert operation. 380 | /// Max retry count. Default is 10. 381 | /// Cancellation token. 382 | member container.ExecuteConcurrentlyAsync<'T, 'E> 383 | ( 384 | operation : UpsertConcurrentlyOperation<'T, 'E>, 385 | [] maxRetryCount : int, 386 | [] cancellationToken : CancellationToken 387 | ) 388 | = 389 | executeConcurrentlyAsync<'T, 'E> cancellationToken container operation maxRetryCount 390 | 391 | /// 392 | /// Executes an upsert operation by applying change to item if exists and returns . 393 | /// 394 | /// Upsert operation. 395 | /// Cancellation token. 396 | member container.ExecuteConcurrentlyAsync<'T, 'E> 397 | (operation : UpsertConcurrentlyOperation<'T, 'E>, [] cancellationToken : CancellationToken) 398 | = 399 | executeConcurrentlyAsync<'T, 'E> cancellationToken container operation DefaultMaxRetryCount 400 | --------------------------------------------------------------------------------