├── .config └── dotnet-tools.json ├── .github └── ISSUE_TEMPLATE.md ├── .gitignore ├── .travis.yml ├── .vscode ├── launch.json └── tasks.json ├── FSharp.Azure.Storage.sln ├── LICENCE.txt ├── RELEASE_NOTES.md ├── Readme.md ├── appveyor.yml ├── build.cmd ├── build.fsx ├── build.sh ├── media ├── Logo.128.png ├── Logo.32.png └── Logo.512.png ├── paket.dependencies ├── paket.lock ├── src └── FSharp.Azure.Storage │ ├── AssemblyInfo.fs │ ├── FSharp.Azure.Storage.fsproj │ ├── Table.fs │ ├── Utilities.fs │ ├── paket.references │ └── paket.template └── test └── FSharp.Azure.Storage.Tests ├── FSharp.Azure.Storage.Tests.fsproj ├── Program.fs ├── Table ├── DataModificationTests.fs ├── DataQueryTests.fs └── QueryExpressionTests.fs ├── Utilities.fs └── paket.references /.config/dotnet-tools.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1, 3 | "isRoot": true, 4 | "tools": { 5 | "paket": { 6 | "version": "7.0.2", 7 | "commands": [ 8 | "paket" 9 | ] 10 | }, 11 | "fake-cli": { 12 | "version": "5.20.4-alpha.1642", 13 | "commands": [ 14 | "fake" 15 | ] 16 | } 17 | } 18 | } -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ### Description 2 | 3 | Please provide a succinct description of your issue. 4 | 5 | ### Repro steps 6 | 7 | Please provide the steps required to reproduce the problem 8 | 9 | 1. Step A 10 | 11 | 2. Step B 12 | 13 | ### Expected behavior 14 | 15 | Please provide a description of the behavior you expect. 16 | 17 | ### Actual behavior 18 | 19 | Please provide a description of the actual behavior you observe. 20 | 21 | ### Known workarounds 22 | 23 | Please provide a description of any known workarounds. 24 | 25 | ### Related information 26 | 27 | * Operating system 28 | * Branch 29 | * .NET Runtime, CoreCLR or Mono Version 30 | * Performance information, links to performance testing scripts 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Build Folders (you can keep bin if you'd like, to store dlls and pdbs) 2 | [Bb]in/ 3 | [Oo]bj/ 4 | 5 | # mstest test results 6 | TestResults 7 | 8 | ## Ignore Visual Studio temporary files, build results, and 9 | ## files generated by popular Visual Studio add-ons. 10 | 11 | # User-specific files 12 | *.suo 13 | *.user 14 | *.sln.docstates 15 | 16 | # Build results 17 | [Dd]ebug/ 18 | [Rr]elease/ 19 | x64/ 20 | *_i.c 21 | *_p.c 22 | *.ilk 23 | *.meta 24 | *.obj 25 | *.pch 26 | *.pdb 27 | *.pgc 28 | *.pgd 29 | *.rsp 30 | *.sbr 31 | *.tlb 32 | *.tli 33 | *.tlh 34 | *.tmp 35 | *.log 36 | *.vspscc 37 | *.vssscc 38 | .builds 39 | 40 | # Visual C++ cache files 41 | ipch/ 42 | *.aps 43 | *.ncb 44 | *.opensdf 45 | *.sdf 46 | 47 | # Visual Studio profiler 48 | *.psess 49 | *.vsp 50 | *.vspx 51 | 52 | # Guidance Automation Toolkit 53 | *.gpState 54 | 55 | # ReSharper is a .NET coding add-in 56 | _ReSharper* 57 | 58 | # NCrunch 59 | *.ncrunch* 60 | .*crunch*.local.xml 61 | 62 | # Installshield output folder 63 | [Ee]xpress 64 | 65 | # DocProject is a documentation generator add-in 66 | DocProject/buildhelp/ 67 | DocProject/Help/*.HxT 68 | DocProject/Help/*.HxC 69 | DocProject/Help/*.hhc 70 | DocProject/Help/*.hhk 71 | DocProject/Help/*.hhp 72 | DocProject/Help/Html2 73 | DocProject/Help/html 74 | 75 | # Click-Once directory 76 | publish 77 | 78 | # Publish Web Output 79 | *.Publish.xml 80 | 81 | # NuGet Packages Directory 82 | packages 83 | 84 | # Windows Azure Build Output 85 | csx 86 | *.build.csdef 87 | 88 | # Windows Store app package directory 89 | AppPackages/ 90 | 91 | # Others 92 | [Bb]in 93 | [Oo]bj 94 | sql 95 | TestResults 96 | [Tt]est[Rr]esult* 97 | *.Cache 98 | ClientBin 99 | [Ss]tyle[Cc]op.* 100 | ~$* 101 | *.dbmdl 102 | Generated_Code #added for RIA/Silverlight projects 103 | 104 | # Backup & report files from converting an old project file to a newer 105 | # Visual Studio version. Backup files are not needed, because we have git ;-) 106 | _UpgradeReport_Files/ 107 | Backup*/ 108 | UpgradeLog*.XML 109 | *.nupkg 110 | 111 | # Misc build outputs 112 | .fake/ 113 | .paket-files/ 114 | xunit.html 115 | paket-files/ 116 | .paket/ 117 | 118 | .vs 119 | .ionide 120 | .fake -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: csharp 2 | mono: none 3 | dotnet: 5.0.101 4 | install: 5 | script: 6 | - "./build.sh" 7 | env: 8 | global: 9 | secure: PryCvsUrxO9LIWbvQScWny5FooBHrMULVWiiXjQqOC5InYke3hmktGhfIq/MQxA2xtJjKkQuv1fAk/3R4iweZS3l/UtDuwbqNVfxIysXrvvFPLGvms4n2rJPJIlmwG0JzkWDSCYyekl2YKDKAJ4znRR0AF453/WOzN50tO5KDweq3QLGxw5Xu8T7yNe+NQozjp8gZ6QuvHz7ecHDLXEV7d64TAPQfIGR2ORWCcK02gZLd5irk9SZCMEbqIGq9w8nOaWRwyO73IF9wXhLihnlWCGdW1ujWLorKO1hxv+iOcpwdPoEqVQKq8okGO7p3jYKiaFOMXjyMzEMbJ60CG65/QFcFE1x0sJ5O5zfpEzlTdHJ9D0kS76E4f3Ur3ZEZcR1skUatOmMgxPqOF8TTrOOlOgGjlzf+PhyL8EvKSlNcXy1fmY7UCOgsEQXjPUSDLFnn/Ihj71rAcbOTETKR30ARWRn9f2LTj/wFB3/upZJrh9g8FudoGOem8zxaqvKVZIlA/VArZNkpB2YlJq8rWUti5DGgssSbAzYTmfzncIEx5aLZJHR0wL6LnJ31WVulV74W2dsQF99OweB+Aq2g1asllLHED31pm8W5ChA8UP4ELGr4KbLsbJxBxC50BYpQz71bRkHUUrg0WEwqSqXobsujtz/RRp5zyfxwFgHyk3AGxE= 10 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": ".NET Core Launch (tests)", 9 | "type": "coreclr", 10 | "request": "launch", 11 | "preLaunchTask": "build", 12 | "program": "${workspaceFolder}/test/FSharp.Azure.Storage.Tests/bin/Debug/netcoreapp2.0/FSharp.Azure.Storage.Tests.dll", 13 | "args": [], 14 | "cwd": "${workspaceFolder}", 15 | "stopAtEntry": false, 16 | "console": "internalConsole" 17 | } 18 | ] 19 | } -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=733558 3 | // for the documentation about the tasks.json format 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "label": "build", 8 | "command": "dotnet build ./FSharp.Azure.Storage.sln", 9 | "type": "shell", 10 | "group": "build", 11 | "presentation": { 12 | "reveal": "silent" 13 | }, 14 | "problemMatcher": "$msCompile" 15 | } 16 | ] 17 | } -------------------------------------------------------------------------------- /FSharp.Azure.Storage.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 15 4 | VisualStudioVersion = 15.0.27428.2037 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "FSharp.Azure.Storage", "src\FSharp.Azure.Storage\FSharp.Azure.Storage.fsproj", "{3173182F-A66D-4648-899C-3143CCCC78F2}" 7 | EndProject 8 | Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "FSharp.Azure.Storage.Tests", "test\FSharp.Azure.Storage.Tests\FSharp.Azure.Storage.Tests.fsproj", "{13F41971-9521-4148-B511-8C52462AE90B}" 9 | EndProject 10 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "paket", "paket", "{36E0D531-FDC8-40EC-8AF8-0318BA9EFCA7}" 11 | ProjectSection(SolutionItems) = preProject 12 | paket.dependencies = paket.dependencies 13 | paket.lock = paket.lock 14 | EndProjectSection 15 | EndProject 16 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "project", "project", "{B53B7F04-619E-4342-94A6-75128FB77A18}" 17 | ProjectSection(SolutionItems) = preProject 18 | .travis.yml = .travis.yml 19 | appveyor.yml = appveyor.yml 20 | build.cmd = build.cmd 21 | build.fsx = build.fsx 22 | build.sh = build.sh 23 | LICENCE.txt = LICENCE.txt 24 | Readme.md = Readme.md 25 | RELEASE_NOTES.md = RELEASE_NOTES.md 26 | EndProjectSection 27 | EndProject 28 | Global 29 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 30 | Debug|Any CPU = Debug|Any CPU 31 | Release|Any CPU = Release|Any CPU 32 | EndGlobalSection 33 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 34 | {3173182F-A66D-4648-899C-3143CCCC78F2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 35 | {3173182F-A66D-4648-899C-3143CCCC78F2}.Debug|Any CPU.Build.0 = Debug|Any CPU 36 | {3173182F-A66D-4648-899C-3143CCCC78F2}.Release|Any CPU.ActiveCfg = Release|Any CPU 37 | {3173182F-A66D-4648-899C-3143CCCC78F2}.Release|Any CPU.Build.0 = Release|Any CPU 38 | {13F41971-9521-4148-B511-8C52462AE90B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 39 | {13F41971-9521-4148-B511-8C52462AE90B}.Debug|Any CPU.Build.0 = Debug|Any CPU 40 | {13F41971-9521-4148-B511-8C52462AE90B}.Release|Any CPU.ActiveCfg = Release|Any CPU 41 | {13F41971-9521-4148-B511-8C52462AE90B}.Release|Any CPU.Build.0 = Release|Any CPU 42 | EndGlobalSection 43 | GlobalSection(SolutionProperties) = preSolution 44 | HideSolutionNode = FALSE 45 | EndGlobalSection 46 | GlobalSection(ExtensibilityGlobals) = postSolution 47 | SolutionGuid = {23D4C78C-E252-471E-BF33-644AE2EE4857} 48 | EndGlobalSection 49 | EndGlobal 50 | -------------------------------------------------------------------------------- /LICENCE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018 Daniel Chambers & Contributors 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. -------------------------------------------------------------------------------- /RELEASE_NOTES.md: -------------------------------------------------------------------------------- 1 | ### 5.0.2 2 | * Upgrade FSharp.Control.Async dependency to >= 3.0.5 3 | 4 | ### 5.0.1 5 | * Downgrade build to use FSharp.Core 4.7 6 | 7 | ### 5.0.0 8 | * New fromTableSegmentedAsyncSeq that returns an AsyncSeq of query segments 9 | * Fixed querying using DynamicTableEntity 10 | * Updated to use v5 of Unquote (potentially breaking if you're stuck on v4) 11 | 12 | ### 4.0.0 13 | * Switched from using the WindowsAzure.Storage package, which is deprecated, to the Microsoft.Azure.Cosmos.Table package. NOTE: This is a breaking change, but simple to fix; just update namespaces (see PR#39) (Thanks @JohnDoeKyrgyz). 14 | 15 | ### 3.3.1 16 | * Fixed using option type with int64. (Thanks @CameronAavik) 17 | 18 | ### 3.3.0 19 | * Support for record fields typed as union types that do not have fields. (Thanks @aaronpowell) 20 | 21 | ### 3.2.0 22 | * New FSharp.Azure.Storage.Table.Task module that contains Task implementations of the async functions. (Thanks @coolya) 23 | 24 | ### 3.1.0 25 | * New Etag and Timestamp attributes that can be applied to record fields; the row's ETag and Timestamp will be written into those fields on query. These fields are ignored when writing back into table storage. (Thanks @coolya) 26 | * The System.Uri type is now supported as a record field type. (Thanks @coolya) 27 | 28 | ### 3.0.0 29 | * .NET Standard 2.0 support. NOTE: All sync functions (eg. fromTable, etc) are now sync-over-async functions. See https://github.com/Azure/azure-storage-net/issues/367 30 | * Fixed bug in fromTableAsync where take count wasn't respected 31 | 32 | ### 2.2.0 33 | * Removed dependency on FSPowerPack; Unquote is used to perform expression tree evaluation 34 | 35 | ### 2.1.0 36 | * DateTime can now be used as a type for properties. 37 | * Renamed project from FSharp.Azure to FSharp.Azure.Storage. 38 | * Namespaces and assemblies have been changed from DigitallyCreated.FSharp.Azure to FSharp.Azure.Storage 39 | * TableStorage module has been renamed to Table -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | FSharp.Azure.Storage 2 | ==================== 3 | 4 | FSharp.Azure.Storage is a wrapper over the standard Microsoft [Microsoft.Azure.Cosmos.Table][1] 5 | library that allows you to write idiomatic F# when talking to Azure. 6 | 7 | The standard storage API is fine when you're writing C#, however when you're 8 | using F# you want to be able to use immutable record types, use the native F# 9 | async support and generally write in a functional style. 10 | 11 | [1]: 12 | 13 | NuGet [![NuGet Status](https://img.shields.io/nuget/v/FSharp.Azure.Storage.svg?style=flat)](https://www.nuget.org/packages/FSharp.Azure.Storage/) 14 | ----- 15 | `Install-Package FSharp.Azure.Storage` 16 | 17 | A Quick Taster 18 | -------------- 19 | ### Inserting/Updating Table Storage 20 | Imagine we had a record type that we wanted to save into table storage: 21 | 22 | ```f# 23 | open FSharp.Azure.Storage.Table 24 | 25 | type Game = 26 | { [] Developer: string 27 | [] Name: string 28 | HasMultiplayer: bool } 29 | ``` 30 | 31 | Now we'll define a helper function `inGameTable` that will allow us to persist these Game records to table storage into an existing table called "Games": 32 | 33 | ```f# 34 | open Microsoft.Azure.Cosmos.Table 35 | 36 | let account = CloudStorageAccount.Parse "UseDevelopmentStorage=true;" //Or your connection string here 37 | let tableClient = account.CreateCloudTableClient() 38 | 39 | let inGameTable game = inTable tableClient "Games" game 40 | ``` 41 | 42 | Now that the set up ceremony is done, let's insert a new Game into table storage: 43 | 44 | ```f# 45 | let game = { Developer = "343 Industries"; Name = "Halo 4"; HasMultiplayer = true } 46 | 47 | let result = game |> Insert |> inGameTable 48 | ``` 49 | 50 | Let's say we want to modify this game and update it in table storage: 51 | 52 | ```f# 53 | let modifiedGame = { game with HasMultiplayer = false } 54 | 55 | let result2 = (modifiedGame, result.Etag) |> Replace |> inGameTable 56 | ``` 57 | 58 | ### Querying Table Storage 59 | 60 | First we need to set up a little helper function for querying from the "Games" table: 61 | 62 | ```f# 63 | let fromGameTable q = fromTable tableClient "Games" q 64 | ``` 65 | 66 | Here's how we'd query for an individual record by PartitionKey and RowKey: 67 | 68 | ```f# 69 | let halo4, metadata = 70 | Query.all 71 | |> Query.where <@ fun g s -> s.PartitionKey = "343 Industries" && s.RowKey = "Halo 4" @> 72 | |> fromGameTable 73 | |> Seq.head 74 | ``` 75 | 76 | If we wanted to find all multiplayer games made by Valve: 77 | 78 | ```f# 79 | let multiplayerValveGames = 80 | Query.all 81 | |> Query.where <@ fun g s -> s.PartitionKey = "Valve" && g.HasMultiplayer @> 82 | |> fromGameTable 83 | ``` 84 | 85 | ### Further Information 86 | For further documentation and examples, please visit the [wiki][2]. 87 | 88 | [2]: https://github.com/fsprojects/FSharp.Azure.Storage/wiki 89 | 90 | 91 | Building 92 | -------- 93 | Run `build.cmd` or `build.sh` to restore the required dependencies using Paket and then build and run tests using FAKE. You can also build in Visual Studio. 94 | 95 | By default, the tests run against the Azure Storage Emulator. However, you can run them against any storage account by setting the `FSHARP_AZURE_STORAGE_CONNECTION_STRING` environment variable to an Azure Storage account connection string before running the tests. 96 | 97 | **AppVeyor (Windows)** 98 | [![AppVeyor Build status](https://ci.appveyor.com/api/projects/status/ssbhpme5jromcbmo?svg=true)](https://ci.appveyor.com/project/daniel-chambers/fsharp-azure-storage) 99 | 100 | **Travis (Linux)** 101 | [![Travis Build Status](https://travis-ci.org/fsprojects/FSharp.Azure.Storage.svg?branch=master)](https://travis-ci.org/fsprojects/FSharp.Azure.Storage) 102 | 103 | ## Maintainer(s) 104 | 105 | - [@daniel-chambers](https://github.com/daniel-chambers) 106 | 107 | The default maintainer account for projects under "fsprojects" is [@fsprojectsgit](https://github.com/fsprojectsgit) - F# Community Project Incubation Space (repo management) 108 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | version: 3.0.0.{build} 2 | image: Visual Studio 2019 3 | 4 | configuration: Release 5 | 6 | build_script: 7 | - cmd: build.cmd 8 | 9 | test: off 10 | 11 | environment: 12 | FSHARP_AZURE_STORAGE_CONNECTION_STRING: 13 | secure: saFGdmViFU9yF3vNqonOY3deDHBkMvgI9OYOGP7Z3InBi8bu78K6Xthr6f7YEiL2VP2B/jN09WmGst38hNv9JQ3CNeDbpClWvJ5nFGAqoYAHpoAsauK6k+b+gKr2E/prVQTrISV0Q7ve0FJHtCDStYY+H6+XlWrUMtHPWnDmATB8c6rLWNb/P3ZEv0w/2dEIwjEtoYAKngKBW0dxsVzzmN9YO4LTfo9oUW1ayXiJdbLz0FT80gtm+JllUrW6pxuK7ElxrdUNLzDgs5g7Amx/+w== -------------------------------------------------------------------------------- /build.cmd: -------------------------------------------------------------------------------- 1 | @echo off 2 | dotnet tool restore 3 | dotnet paket restore 4 | dotnet fake build %* -------------------------------------------------------------------------------- /build.fsx: -------------------------------------------------------------------------------- 1 | #r "paket: groupref FakeBuild //" 2 | #load "./.fake/build.fsx/intellisense.fsx" 3 | 4 | open System 5 | open Fake.Core 6 | open Fake.DotNet 7 | open Fake.IO 8 | open Fake.Tools 9 | open Fake.Api 10 | 11 | #nowarn "52" 12 | 13 | let gitHubToken = Environment.environVarOrDefault "GITHUB_TOKEN" "" 14 | let nugetApiKey = Environment.environVarOrDefault "NUGET_KEY" "" 15 | 16 | let gitHubOwner = "fsprojects" 17 | let gitHubName = "FSharp.Azure.Storage" 18 | 19 | let releaseNotes = 20 | File.read "./RELEASE_NOTES.md" 21 | |> ReleaseNotes.parseAll 22 | let latestReleaseNotes = List.head releaseNotes 23 | let previousReleaseNotes = List.item 1 releaseNotes 24 | 25 | Target.description "Tags the current commit with the version and pushes the tag" 26 | Target.create "GitTagAndPush" <| fun _ -> 27 | if not <| Git.Information.isCleanWorkingCopy "." then 28 | failwith "Please ensure the working copy is clean before performing a release" 29 | 30 | let remoteUrl = sprintf "github.com/%s/%s" gitHubOwner gitHubName 31 | let remote = 32 | Git.CommandHelper.getGitResult "" "remote -v" 33 | |> Seq.filter (fun (s: string) -> s.EndsWith "(push)") 34 | |> Seq.tryFind (fun (s: string) -> s.Contains remoteUrl) 35 | |> function 36 | | Some (s: string) -> s.Split().[0] 37 | | None -> failwithf "Unable to determine remote for %s" remoteUrl 38 | 39 | let tag = sprintf "v%s" latestReleaseNotes.NugetVersion 40 | Git.Branches.pushBranch "." remote <| Git.Information.getBranchName "." 41 | Git.Branches.tag "." tag 42 | Git.Branches.pushTag "." remote tag 43 | 44 | Target.description "Generates an AssemblyInfo file with version info" 45 | Target.create "GenerateAssemblyInfoFile" <| fun _ -> 46 | AssemblyInfoFile.createFSharp "./src/FSharp.Azure.Storage/obj/AssemblyInfo.Generated.fs" [ 47 | AssemblyInfo.Title "FSharp.Azure.Storage" 48 | AssemblyInfo.Product "FSharp.Azure.Storage" 49 | AssemblyInfo.Company "Daniel Chambers & Contributors" 50 | AssemblyInfo.Copyright ("Copyright \169 Daniel Chambers & Contributors " + DateTime.Now.Year.ToString ()) 51 | AssemblyInfo.Version latestReleaseNotes.AssemblyVersion 52 | AssemblyInfo.FileVersion latestReleaseNotes.AssemblyVersion 53 | AssemblyInfo.Metadata ("githash", Git.Information.getCurrentHash()) 54 | ] 55 | 56 | Target.description "Compiles the project using dotnet build" 57 | Target.create "Build" <| fun _ -> 58 | DotNet.build id "./FSharp.Azure.Storage.sln" 59 | 60 | Target.description "Runs the expecto unit tests" 61 | Target.create "Test" <| fun _ -> 62 | let result = DotNet.exec id "run" "--project ./test/FSharp.Azure.Storage.Tests -c Release" 63 | if not result.OK then failwithf "Tests failed with code %i" result.ExitCode 64 | 65 | Target.description "Deletes the contents of the ./bin directory" 66 | Target.create "PaketClean" <| fun _ -> 67 | Shell.cleanDir "./bin" 68 | 69 | Target.description "Creates the NuGet package" 70 | Target.create "PaketPack" <| fun _ -> 71 | Paket.pack <| fun p -> 72 | { p with 73 | ToolType = ToolType.CreateLocalTool() 74 | ReleaseNotes = latestReleaseNotes.Notes |> List.map (fun s -> "- " + s) |> String.concat "\n" 75 | Version = latestReleaseNotes.NugetVersion 76 | OutputPath = "./bin" } 77 | 78 | Target.description "Ensures you have specified your NuGet API key in the NUGET_KEY env var" 79 | Target.create "ValidateNugetApiKey" <| fun _ -> 80 | if String.IsNullOrWhiteSpace nugetApiKey then 81 | failwith "Please set the NUGET_KEY environment variable to your NuGet API Key" 82 | 83 | Target.description "Pushes the NuGet package to the package repository" 84 | Target.create "PaketPush" <| fun _ -> 85 | Paket.push <| fun p -> 86 | { p with 87 | ToolType = ToolType.CreateLocalTool() 88 | WorkingDir = "./bin" 89 | ApiKey = nugetApiKey } 90 | 91 | Target.description "Ensures you have specified your GitHub personal access token in the GITHUB_TOKEN env var" 92 | Target.create "ValidateGitHubCredentials" <| fun _ -> 93 | if String.IsNullOrWhiteSpace gitHubToken then 94 | failwith "Please set the GITHUB_TOKEN environment variable to a GitHub personal access token with repo access." 95 | 96 | Target.description "Creates a release on GitHub with the release notes" 97 | Target.create "GitHubRelease" <| fun _ -> 98 | let gitHubReleaseNotes = 99 | [ yield "## Changelog" 100 | yield! latestReleaseNotes.Notes |> List.map (fun s -> "- " + s) 101 | yield "" 102 | yield sprintf "Full changelog [here](https://github.com/fsprojects/FSharp.Azure.Storage/compare/v%s...v%s)" previousReleaseNotes.NugetVersion latestReleaseNotes.NugetVersion ] 103 | 104 | GitHub.createClientWithToken gitHubToken 105 | |> GitHub.draftNewRelease gitHubOwner gitHubName ("v" + latestReleaseNotes.NugetVersion) (latestReleaseNotes.SemVer.PreRelease <> None) gitHubReleaseNotes 106 | |> GitHub.publishDraft 107 | |> Async.RunSynchronously 108 | 109 | Target.create "BeginRelease" ignore 110 | 111 | Target.create "PublishRelease" ignore 112 | 113 | open Fake.Core.TargetOperators 114 | 115 | "GitTagAndPush" 116 | ?=> "GenerateAssemblyInfoFile" 117 | ==> "Build" 118 | ==> "Test" 119 | ==> "PaketClean" 120 | ==> "PaketPack" 121 | ==> "BeginRelease" 122 | ==> "PaketPush" 123 | ==> "GitHubRelease" 124 | ==> "PublishRelease" 125 | 126 | "ValidateGitHubCredentials" 127 | ?=> "BeginRelease" 128 | 129 | "ValidateGitHubCredentials" 130 | ==> "GitHubRelease" 131 | 132 | "ValidateNugetApiKey" 133 | ?=> "BeginRelease" 134 | 135 | "ValidateNugetApiKey" 136 | ==> "PaketPush" 137 | 138 | // Only do a GitTagAndPush if we're pushing a new version 139 | "GitTagAndPush" 140 | ==> "PaketPush" 141 | 142 | Target.runOrDefault "PaketPack" -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | dotnet tool restore 4 | dotnet paket restore 5 | dotnet fake build $@ -------------------------------------------------------------------------------- /media/Logo.128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fsprojects/FSharp.Azure.Storage/e016a7edcb6661736204436fdbeb07afe811eac5/media/Logo.128.png -------------------------------------------------------------------------------- /media/Logo.32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fsprojects/FSharp.Azure.Storage/e016a7edcb6661736204436fdbeb07afe811eac5/media/Logo.32.png -------------------------------------------------------------------------------- /media/Logo.512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fsprojects/FSharp.Azure.Storage/e016a7edcb6661736204436fdbeb07afe811eac5/media/Logo.512.png -------------------------------------------------------------------------------- /paket.dependencies: -------------------------------------------------------------------------------- 1 | source https://api.nuget.org/v3/index.json 2 | framework: auto-detect 3 | storage: none 4 | 5 | //Main deps 6 | nuget FSharp.Control.AsyncSeq >= 3.0.5 7 | nuget FSharp.Core >= 4.7 8 | nuget Microsoft.Azure.Cosmos.Table >= 1.0 9 | nuget TaskBuilder.fs >= 2.1.0 10 | nuget Unquote >= 5.0 11 | 12 | //Testing deps 13 | nuget Expecto 14 | 15 | group FakeBuild 16 | source https://api.nuget.org/v3/index.json 17 | storage: none 18 | framework: netstandard2.0 19 | 20 | nuget FSharp.Core ~> 5.0 # FAKE currently requires this 21 | nuget Fake.Api.GitHub 22 | nuget Fake.Core.ReleaseNotes 23 | nuget Fake.Core.Target 24 | nuget Fake.DotNet.AssemblyInfoFile 25 | nuget Fake.DotNet.Cli 26 | nuget Fake.DotNet.Paket 27 | nuget Fake.IO.FileSystem 28 | nuget Fake.Tools.Git -------------------------------------------------------------------------------- /paket.lock: -------------------------------------------------------------------------------- 1 | STORAGE: NONE 2 | RESTRICTION: || (== net5.0) (== netstandard2.0) 3 | NUGET 4 | remote: https://api.nuget.org/v3/index.json 5 | Expecto (9.0.2) 6 | FSharp.Core (>= 4.6) 7 | Mono.Cecil (>= 0.11.2) 8 | FSharp.Control.AsyncSeq (3.0.5) 9 | FSharp.Core (>= 4.3.2) 10 | Microsoft.Bcl.AsyncInterfaces (>= 5.0) - restriction: || (&& (== net5.0) (< netstandard2.1)) (== netstandard2.0) 11 | FSharp.Core (4.7) 12 | Microsoft.Azure.Cosmos.Table (1.0.8) 13 | Microsoft.Azure.DocumentDB.Core (>= 2.11.2) 14 | Microsoft.OData.Core (>= 7.6.4) 15 | Newtonsoft.Json (>= 10.0.2) 16 | Microsoft.Azure.DocumentDB.Core (2.13.1) 17 | NETStandard.Library (>= 1.6) 18 | Newtonsoft.Json (>= 9.0.1) 19 | System.Collections.Immutable (>= 1.3) 20 | System.Collections.NonGeneric (>= 4.0.1) 21 | System.Collections.Specialized (>= 4.0.1) 22 | System.Diagnostics.TraceSource (>= 4.0) 23 | System.Dynamic.Runtime (>= 4.0.11) 24 | System.Linq.Queryable (>= 4.0.1) 25 | System.Net.Http (>= 4.3.4) 26 | System.Net.NameResolution (>= 4.0) 27 | System.Net.NetworkInformation (>= 4.1) 28 | System.Net.Requests (>= 4.0.11) 29 | System.Net.Security (>= 4.3.2) 30 | System.Net.WebHeaderCollection (>= 4.0.1) 31 | System.Runtime.Serialization.Primitives (>= 4.1.1) 32 | System.Security.SecureString (>= 4.0) 33 | System.Threading.Tasks.Extensions (>= 4.5.2) 34 | System.ValueTuple (>= 4.5) 35 | Microsoft.Bcl.AsyncInterfaces (6.0) - restriction: || (&& (== net5.0) (< netstandard2.1)) (== netstandard2.0) 36 | System.Threading.Tasks.Extensions (>= 4.5.4) - restriction: || (&& (== net5.0) (>= net461)) (&& (== net5.0) (< netstandard2.1)) (== netstandard2.0) 37 | Microsoft.NETCore.Platforms (3.1) 38 | Microsoft.NETCore.Targets (2.1) 39 | Microsoft.OData.Core (7.6.4) 40 | Microsoft.OData.Edm (7.6.4) 41 | Microsoft.Spatial (7.6.4) 42 | Microsoft.OData.Edm (7.6.4) 43 | Microsoft.Spatial (7.6.4) 44 | Microsoft.Win32.Primitives (4.3) 45 | Microsoft.NETCore.Platforms (>= 1.1) 46 | Microsoft.NETCore.Targets (>= 1.1) 47 | System.Runtime (>= 4.3) 48 | Mono.Cecil (0.11.3) 49 | NETStandard.Library (2.0.3) 50 | Microsoft.NETCore.Platforms (>= 1.1) 51 | Newtonsoft.Json (11.0.2) 52 | runtime.debian.8-x64.runtime.native.System.Security.Cryptography.OpenSsl (4.3.3) 53 | runtime.debian.9-x64.runtime.native.System.Security.Cryptography.OpenSsl (4.3.3) 54 | runtime.fedora.23-x64.runtime.native.System.Security.Cryptography.OpenSsl (4.3.3) 55 | runtime.fedora.24-x64.runtime.native.System.Security.Cryptography.OpenSsl (4.3.3) 56 | runtime.fedora.27-x64.runtime.native.System.Security.Cryptography.OpenSsl (4.3.3) 57 | runtime.fedora.28-x64.runtime.native.System.Security.Cryptography.OpenSsl (4.3.3) 58 | runtime.native.System (4.3.1) 59 | Microsoft.NETCore.Platforms (>= 1.1.1) 60 | Microsoft.NETCore.Targets (>= 1.1.3) 61 | runtime.native.System.Net.Http (4.3.1) 62 | Microsoft.NETCore.Platforms (>= 1.1.1) 63 | Microsoft.NETCore.Targets (>= 1.1.3) 64 | runtime.native.System.Net.Security (4.3.1) 65 | Microsoft.NETCore.Platforms (>= 1.1.1) 66 | Microsoft.NETCore.Targets (>= 1.1.3) 67 | runtime.native.System.Security.Cryptography.Apple (4.3.1) 68 | runtime.osx.10.10-x64.runtime.native.System.Security.Cryptography.Apple (>= 4.3.1) 69 | runtime.native.System.Security.Cryptography.OpenSsl (4.3.3) 70 | runtime.debian.8-x64.runtime.native.System.Security.Cryptography.OpenSsl (>= 4.3.3) 71 | runtime.debian.9-x64.runtime.native.System.Security.Cryptography.OpenSsl (>= 4.3.3) 72 | runtime.fedora.23-x64.runtime.native.System.Security.Cryptography.OpenSsl (>= 4.3.3) 73 | runtime.fedora.24-x64.runtime.native.System.Security.Cryptography.OpenSsl (>= 4.3.3) 74 | runtime.fedora.27-x64.runtime.native.System.Security.Cryptography.OpenSsl (>= 4.3.3) 75 | runtime.fedora.28-x64.runtime.native.System.Security.Cryptography.OpenSsl (>= 4.3.3) 76 | runtime.opensuse.13.2-x64.runtime.native.System.Security.Cryptography.OpenSsl (>= 4.3.3) 77 | runtime.opensuse.42.1-x64.runtime.native.System.Security.Cryptography.OpenSsl (>= 4.3.3) 78 | runtime.opensuse.42.3-x64.runtime.native.System.Security.Cryptography.OpenSsl (>= 4.3.3) 79 | runtime.osx.10.10-x64.runtime.native.System.Security.Cryptography.OpenSsl (>= 4.3.3) 80 | runtime.rhel.7-x64.runtime.native.System.Security.Cryptography.OpenSsl (>= 4.3.3) 81 | runtime.ubuntu.14.04-x64.runtime.native.System.Security.Cryptography.OpenSsl (>= 4.3.3) 82 | runtime.ubuntu.16.04-x64.runtime.native.System.Security.Cryptography.OpenSsl (>= 4.3.3) 83 | runtime.ubuntu.16.10-x64.runtime.native.System.Security.Cryptography.OpenSsl (>= 4.3.3) 84 | runtime.ubuntu.18.04-x64.runtime.native.System.Security.Cryptography.OpenSsl (>= 4.3.3) 85 | runtime.opensuse.13.2-x64.runtime.native.System.Security.Cryptography.OpenSsl (4.3.3) 86 | runtime.opensuse.42.1-x64.runtime.native.System.Security.Cryptography.OpenSsl (4.3.3) 87 | runtime.opensuse.42.3-x64.runtime.native.System.Security.Cryptography.OpenSsl (4.3.3) 88 | runtime.osx.10.10-x64.runtime.native.System.Security.Cryptography.Apple (4.3.1) 89 | runtime.osx.10.10-x64.runtime.native.System.Security.Cryptography.OpenSsl (4.3.3) 90 | runtime.rhel.7-x64.runtime.native.System.Security.Cryptography.OpenSsl (4.3.3) 91 | runtime.ubuntu.14.04-x64.runtime.native.System.Security.Cryptography.OpenSsl (4.3.3) 92 | runtime.ubuntu.16.04-x64.runtime.native.System.Security.Cryptography.OpenSsl (4.3.3) 93 | runtime.ubuntu.16.10-x64.runtime.native.System.Security.Cryptography.OpenSsl (4.3.3) 94 | runtime.ubuntu.18.04-x64.runtime.native.System.Security.Cryptography.OpenSsl (4.3.3) 95 | System.Buffers (4.5.1) - restriction: || (&& (== net5.0) (>= monotouch)) (&& (== net5.0) (>= net45) (< netstandard1.3)) (&& (== net5.0) (>= net46)) (&& (== net5.0) (>= net461)) (&& (== net5.0) (< netcoreapp2.0)) (&& (== net5.0) (< netcoreapp2.1) (>= xamarinios)) (&& (== net5.0) (< netcoreapp2.1) (>= xamarinmac)) (&& (== net5.0) (< netcoreapp2.1) (>= xamarintvos)) (&& (== net5.0) (< netcoreapp2.1) (>= xamarinwatchos)) (&& (== net5.0) (< netstandard1.1)) (&& (== net5.0) (< netstandard2.0)) (&& (== net5.0) (>= uap10.1)) (== netstandard2.0) 96 | System.Collections (4.3) 97 | Microsoft.NETCore.Platforms (>= 1.1) 98 | Microsoft.NETCore.Targets (>= 1.1) 99 | System.Runtime (>= 4.3) 100 | System.Collections.Concurrent (4.3) 101 | System.Collections (>= 4.3) 102 | System.Diagnostics.Debug (>= 4.3) 103 | System.Diagnostics.Tracing (>= 4.3) 104 | System.Globalization (>= 4.3) 105 | System.Reflection (>= 4.3) 106 | System.Resources.ResourceManager (>= 4.3) 107 | System.Runtime (>= 4.3) 108 | System.Runtime.Extensions (>= 4.3) 109 | System.Threading (>= 4.3) 110 | System.Threading.Tasks (>= 4.3) 111 | System.Collections.Immutable (1.5) 112 | System.Collections.NonGeneric (4.3) 113 | System.Diagnostics.Debug (>= 4.3) 114 | System.Globalization (>= 4.3) 115 | System.Resources.ResourceManager (>= 4.3) 116 | System.Runtime (>= 4.3) 117 | System.Runtime.Extensions (>= 4.3) 118 | System.Threading (>= 4.3) 119 | System.Collections.Specialized (4.3) 120 | System.Collections.NonGeneric (>= 4.3) 121 | System.Globalization (>= 4.3) 122 | System.Globalization.Extensions (>= 4.3) 123 | System.Resources.ResourceManager (>= 4.3) 124 | System.Runtime (>= 4.3) 125 | System.Runtime.Extensions (>= 4.3) 126 | System.Threading (>= 4.3) 127 | System.Diagnostics.Debug (4.3) 128 | Microsoft.NETCore.Platforms (>= 1.1) 129 | Microsoft.NETCore.Targets (>= 1.1) 130 | System.Runtime (>= 4.3) 131 | System.Diagnostics.DiagnosticSource (4.7) 132 | System.Memory (>= 4.5.3) - restriction: || (&& (== net5.0) (>= net45) (< netstandard1.3)) (&& (== net5.0) (>= net46)) (&& (== net5.0) (< netcoreapp2.1)) (&& (== net5.0) (< netstandard2.0)) (&& (== net5.0) (>= uap10.1)) (== netstandard2.0) 133 | System.Diagnostics.TraceSource (4.3) 134 | Microsoft.NETCore.Platforms (>= 1.1) 135 | runtime.native.System (>= 4.3) 136 | System.Collections (>= 4.3) 137 | System.Diagnostics.Debug (>= 4.3) 138 | System.Globalization (>= 4.3) 139 | System.Resources.ResourceManager (>= 4.3) 140 | System.Runtime (>= 4.3) 141 | System.Runtime.Extensions (>= 4.3) 142 | System.Threading (>= 4.3) 143 | System.Diagnostics.Tracing (4.3) 144 | Microsoft.NETCore.Platforms (>= 1.1) 145 | Microsoft.NETCore.Targets (>= 1.1) 146 | System.Runtime (>= 4.3) 147 | System.Dynamic.Runtime (4.3) 148 | System.Collections (>= 4.3) 149 | System.Diagnostics.Debug (>= 4.3) 150 | System.Linq (>= 4.3) 151 | System.Linq.Expressions (>= 4.3) 152 | System.ObjectModel (>= 4.3) 153 | System.Reflection (>= 4.3) 154 | System.Reflection.Emit (>= 4.3) 155 | System.Reflection.Emit.ILGeneration (>= 4.3) 156 | System.Reflection.Primitives (>= 4.3) 157 | System.Reflection.TypeExtensions (>= 4.3) 158 | System.Resources.ResourceManager (>= 4.3) 159 | System.Runtime (>= 4.3) 160 | System.Runtime.Extensions (>= 4.3) 161 | System.Threading (>= 4.3) 162 | System.Globalization (4.3) 163 | Microsoft.NETCore.Platforms (>= 1.1) 164 | Microsoft.NETCore.Targets (>= 1.1) 165 | System.Runtime (>= 4.3) 166 | System.Globalization.Calendars (4.3) 167 | Microsoft.NETCore.Platforms (>= 1.1) 168 | Microsoft.NETCore.Targets (>= 1.1) 169 | System.Globalization (>= 4.3) 170 | System.Runtime (>= 4.3) 171 | System.Globalization.Extensions (4.3) 172 | Microsoft.NETCore.Platforms (>= 1.1) 173 | System.Globalization (>= 4.3) 174 | System.Resources.ResourceManager (>= 4.3) 175 | System.Runtime (>= 4.3) 176 | System.Runtime.Extensions (>= 4.3) 177 | System.Runtime.InteropServices (>= 4.3) 178 | System.IO (4.3) 179 | Microsoft.NETCore.Platforms (>= 1.1) 180 | Microsoft.NETCore.Targets (>= 1.1) 181 | System.Runtime (>= 4.3) 182 | System.Text.Encoding (>= 4.3) 183 | System.Threading.Tasks (>= 4.3) 184 | System.IO.FileSystem (4.3) 185 | Microsoft.NETCore.Platforms (>= 1.1) 186 | Microsoft.NETCore.Targets (>= 1.1) 187 | System.IO (>= 4.3) 188 | System.IO.FileSystem.Primitives (>= 4.3) 189 | System.Runtime (>= 4.3) 190 | System.Runtime.Handles (>= 4.3) 191 | System.Text.Encoding (>= 4.3) 192 | System.Threading.Tasks (>= 4.3) 193 | System.IO.FileSystem.Primitives (4.3) 194 | System.Runtime (>= 4.3) 195 | System.Linq (4.3) 196 | System.Collections (>= 4.3) 197 | System.Diagnostics.Debug (>= 4.3) 198 | System.Resources.ResourceManager (>= 4.3) 199 | System.Runtime (>= 4.3) 200 | System.Runtime.Extensions (>= 4.3) 201 | System.Linq.Expressions (4.3) 202 | System.Collections (>= 4.3) 203 | System.Diagnostics.Debug (>= 4.3) 204 | System.Globalization (>= 4.3) 205 | System.IO (>= 4.3) 206 | System.Linq (>= 4.3) 207 | System.ObjectModel (>= 4.3) 208 | System.Reflection (>= 4.3) 209 | System.Reflection.Emit (>= 4.3) 210 | System.Reflection.Emit.ILGeneration (>= 4.3) 211 | System.Reflection.Emit.Lightweight (>= 4.3) 212 | System.Reflection.Extensions (>= 4.3) 213 | System.Reflection.Primitives (>= 4.3) 214 | System.Reflection.TypeExtensions (>= 4.3) 215 | System.Resources.ResourceManager (>= 4.3) 216 | System.Runtime (>= 4.3) 217 | System.Runtime.Extensions (>= 4.3) 218 | System.Threading (>= 4.3) 219 | System.Linq.Queryable (4.3) 220 | System.Collections (>= 4.3) 221 | System.Diagnostics.Debug (>= 4.3) 222 | System.Linq (>= 4.3) 223 | System.Linq.Expressions (>= 4.3) 224 | System.Reflection (>= 4.3) 225 | System.Reflection.Extensions (>= 4.3) 226 | System.Resources.ResourceManager (>= 4.3) 227 | System.Runtime (>= 4.3) 228 | System.Memory (4.5.4) - restriction: || (&& (== net5.0) (>= net45) (< netstandard1.3)) (&& (== net5.0) (>= net46)) (&& (== net5.0) (< netcoreapp2.1)) (&& (== net5.0) (< netstandard2.0)) (&& (== net5.0) (>= uap10.1)) (== netstandard2.0) 229 | System.Buffers (>= 4.5.1) - restriction: || (&& (== net5.0) (>= monotouch)) (&& (== net5.0) (>= net461)) (&& (== net5.0) (< netcoreapp2.0)) (&& (== net5.0) (< netstandard1.1)) (&& (== net5.0) (< netstandard2.0)) (&& (== net5.0) (>= xamarinios)) (&& (== net5.0) (>= xamarinmac)) (&& (== net5.0) (>= xamarintvos)) (&& (== net5.0) (>= xamarinwatchos)) (== netstandard2.0) 230 | System.Numerics.Vectors (>= 4.4) - restriction: || (&& (== net5.0) (< netcoreapp2.0)) (== netstandard2.0) 231 | System.Runtime.CompilerServices.Unsafe (>= 4.5.3) - restriction: || (&& (== net5.0) (>= monotouch)) (&& (== net5.0) (>= net461)) (&& (== net5.0) (< netcoreapp2.0)) (&& (== net5.0) (< netcoreapp2.1)) (&& (== net5.0) (< netstandard1.1)) (&& (== net5.0) (< netstandard2.0)) (&& (== net5.0) (>= uap10.1)) (&& (== net5.0) (>= xamarinios)) (&& (== net5.0) (>= xamarinmac)) (&& (== net5.0) (>= xamarintvos)) (&& (== net5.0) (>= xamarinwatchos)) (== netstandard2.0) 232 | System.Net.Http (4.3.4) 233 | Microsoft.NETCore.Platforms (>= 1.1.1) 234 | runtime.native.System (>= 4.3) 235 | runtime.native.System.Net.Http (>= 4.3) 236 | runtime.native.System.Security.Cryptography.OpenSsl (>= 4.3.2) 237 | System.Collections (>= 4.3) 238 | System.Diagnostics.Debug (>= 4.3) 239 | System.Diagnostics.DiagnosticSource (>= 4.3) 240 | System.Diagnostics.Tracing (>= 4.3) 241 | System.Globalization (>= 4.3) 242 | System.Globalization.Extensions (>= 4.3) 243 | System.IO (>= 4.3) 244 | System.IO.FileSystem (>= 4.3) 245 | System.Net.Primitives (>= 4.3) 246 | System.Resources.ResourceManager (>= 4.3) 247 | System.Runtime (>= 4.3) 248 | System.Runtime.Extensions (>= 4.3) 249 | System.Runtime.Handles (>= 4.3) 250 | System.Runtime.InteropServices (>= 4.3) 251 | System.Security.Cryptography.Algorithms (>= 4.3) 252 | System.Security.Cryptography.Encoding (>= 4.3) 253 | System.Security.Cryptography.OpenSsl (>= 4.3) 254 | System.Security.Cryptography.Primitives (>= 4.3) 255 | System.Security.Cryptography.X509Certificates (>= 4.3) 256 | System.Text.Encoding (>= 4.3) 257 | System.Threading (>= 4.3) 258 | System.Threading.Tasks (>= 4.3) 259 | System.Net.NameResolution (4.3) 260 | Microsoft.NETCore.Platforms (>= 1.1) 261 | runtime.native.System (>= 4.3) 262 | System.Collections (>= 4.3) 263 | System.Diagnostics.Tracing (>= 4.3) 264 | System.Globalization (>= 4.3) 265 | System.Net.Primitives (>= 4.3) 266 | System.Resources.ResourceManager (>= 4.3) 267 | System.Runtime (>= 4.3) 268 | System.Runtime.Extensions (>= 4.3) 269 | System.Runtime.Handles (>= 4.3) 270 | System.Runtime.InteropServices (>= 4.3) 271 | System.Security.Principal.Windows (>= 4.3) 272 | System.Threading (>= 4.3) 273 | System.Threading.Tasks (>= 4.3) 274 | System.Net.NetworkInformation (4.3) 275 | Microsoft.NETCore.Platforms (>= 1.1) 276 | Microsoft.Win32.Primitives (>= 4.3) 277 | runtime.native.System (>= 4.3) 278 | System.Collections (>= 4.3) 279 | System.Diagnostics.Tracing (>= 4.3) 280 | System.Globalization (>= 4.3) 281 | System.IO (>= 4.3) 282 | System.IO.FileSystem (>= 4.3) 283 | System.IO.FileSystem.Primitives (>= 4.3) 284 | System.Linq (>= 4.3) 285 | System.Net.Primitives (>= 4.3) 286 | System.Net.Sockets (>= 4.3) 287 | System.Resources.ResourceManager (>= 4.3) 288 | System.Runtime (>= 4.3) 289 | System.Runtime.Extensions (>= 4.3) 290 | System.Runtime.Handles (>= 4.3) 291 | System.Runtime.InteropServices (>= 4.3) 292 | System.Security.Principal.Windows (>= 4.3) 293 | System.Threading (>= 4.3) 294 | System.Threading.Overlapped (>= 4.3) 295 | System.Threading.Tasks (>= 4.3) 296 | System.Threading.Thread (>= 4.3) 297 | System.Threading.ThreadPool (>= 4.3) 298 | System.Net.Primitives (4.3) 299 | Microsoft.NETCore.Platforms (>= 1.1) 300 | Microsoft.NETCore.Targets (>= 1.1) 301 | System.Runtime (>= 4.3) 302 | System.Runtime.Handles (>= 4.3) 303 | System.Net.Requests (4.3) 304 | Microsoft.NETCore.Platforms (>= 1.1) 305 | System.Collections (>= 4.3) 306 | System.Diagnostics.Debug (>= 4.3) 307 | System.Diagnostics.Tracing (>= 4.3) 308 | System.Globalization (>= 4.3) 309 | System.IO (>= 4.3) 310 | System.Net.Http (>= 4.3) 311 | System.Net.Primitives (>= 4.3) 312 | System.Net.WebHeaderCollection (>= 4.3) 313 | System.Resources.ResourceManager (>= 4.3) 314 | System.Runtime (>= 4.3) 315 | System.Threading (>= 4.3) 316 | System.Threading.Tasks (>= 4.3) 317 | System.Net.Security (4.3.2) 318 | Microsoft.NETCore.Platforms (>= 1.1) 319 | Microsoft.Win32.Primitives (>= 4.3) 320 | runtime.native.System (>= 4.3) 321 | runtime.native.System.Net.Security (>= 4.3) 322 | runtime.native.System.Security.Cryptography.OpenSsl (>= 4.3.2) 323 | System.Collections (>= 4.3) 324 | System.Collections.Concurrent (>= 4.3) 325 | System.Diagnostics.Tracing (>= 4.3) 326 | System.Globalization (>= 4.3) 327 | System.Globalization.Extensions (>= 4.3) 328 | System.IO (>= 4.3) 329 | System.Net.Primitives (>= 4.3) 330 | System.Resources.ResourceManager (>= 4.3) 331 | System.Runtime (>= 4.3) 332 | System.Runtime.Extensions (>= 4.3) 333 | System.Runtime.Handles (>= 4.3) 334 | System.Runtime.InteropServices (>= 4.3) 335 | System.Security.Claims (>= 4.3) 336 | System.Security.Cryptography.Algorithms (>= 4.3) 337 | System.Security.Cryptography.Encoding (>= 4.3) 338 | System.Security.Cryptography.OpenSsl (>= 4.3) 339 | System.Security.Cryptography.Primitives (>= 4.3) 340 | System.Security.Cryptography.X509Certificates (>= 4.3) 341 | System.Security.Principal (>= 4.3) 342 | System.Text.Encoding (>= 4.3) 343 | System.Threading (>= 4.3) 344 | System.Threading.Tasks (>= 4.3) 345 | System.Threading.ThreadPool (>= 4.3) 346 | System.Net.Sockets (4.3) 347 | Microsoft.NETCore.Platforms (>= 1.1) 348 | Microsoft.NETCore.Targets (>= 1.1) 349 | System.IO (>= 4.3) 350 | System.Net.Primitives (>= 4.3) 351 | System.Runtime (>= 4.3) 352 | System.Threading.Tasks (>= 4.3) 353 | System.Net.WebHeaderCollection (4.3) 354 | System.Collections (>= 4.3) 355 | System.Resources.ResourceManager (>= 4.3) 356 | System.Runtime (>= 4.3) 357 | System.Runtime.Extensions (>= 4.3) 358 | System.Numerics.Vectors (4.5) - restriction: || (&& (== net5.0) (>= net45) (< netstandard1.3)) (&& (== net5.0) (>= net46)) (&& (== net5.0) (< netcoreapp2.0)) (&& (== net5.0) (< netstandard2.0)) (&& (== net5.0) (>= uap10.1)) (== netstandard2.0) 359 | System.ObjectModel (4.3) 360 | System.Collections (>= 4.3) 361 | System.Diagnostics.Debug (>= 4.3) 362 | System.Resources.ResourceManager (>= 4.3) 363 | System.Runtime (>= 4.3) 364 | System.Threading (>= 4.3) 365 | System.Reflection (4.3) 366 | Microsoft.NETCore.Platforms (>= 1.1) 367 | Microsoft.NETCore.Targets (>= 1.1) 368 | System.IO (>= 4.3) 369 | System.Reflection.Primitives (>= 4.3) 370 | System.Runtime (>= 4.3) 371 | System.Reflection.Emit (4.3) 372 | System.IO (>= 4.3) 373 | System.Reflection (>= 4.3) 374 | System.Reflection.Emit.ILGeneration (>= 4.3) 375 | System.Reflection.Primitives (>= 4.3) 376 | System.Runtime (>= 4.3) 377 | System.Reflection.Emit.ILGeneration (4.3) 378 | System.Reflection (>= 4.3) 379 | System.Reflection.Primitives (>= 4.3) 380 | System.Runtime (>= 4.3) 381 | System.Reflection.Emit.Lightweight (4.3) 382 | System.Reflection (>= 4.3) 383 | System.Reflection.Emit.ILGeneration (>= 4.3) 384 | System.Reflection.Primitives (>= 4.3) 385 | System.Runtime (>= 4.3) 386 | System.Reflection.Extensions (4.3) 387 | Microsoft.NETCore.Platforms (>= 1.1) 388 | Microsoft.NETCore.Targets (>= 1.1) 389 | System.Reflection (>= 4.3) 390 | System.Runtime (>= 4.3) 391 | System.Reflection.Primitives (4.3) 392 | Microsoft.NETCore.Platforms (>= 1.1) 393 | Microsoft.NETCore.Targets (>= 1.1) 394 | System.Runtime (>= 4.3) 395 | System.Reflection.TypeExtensions (4.5.1) 396 | System.Resources.ResourceManager (4.3) 397 | Microsoft.NETCore.Platforms (>= 1.1) 398 | Microsoft.NETCore.Targets (>= 1.1) 399 | System.Globalization (>= 4.3) 400 | System.Reflection (>= 4.3) 401 | System.Runtime (>= 4.3) 402 | System.Runtime (4.3) 403 | Microsoft.NETCore.Platforms (>= 1.1) 404 | Microsoft.NETCore.Targets (>= 1.1) 405 | System.Runtime.CompilerServices.Unsafe (4.7.1) - restriction: || (&& (== net5.0) (>= net461)) (&& (== net5.0) (< netcoreapp2.1) (< netstandard2.1)) (&& (== net5.0) (< netstandard1.0)) (&& (== net5.0) (< netstandard2.0)) (&& (== net5.0) (>= wp8)) (== netstandard2.0) 406 | System.Runtime.Extensions (4.3) 407 | Microsoft.NETCore.Platforms (>= 1.1) 408 | Microsoft.NETCore.Targets (>= 1.1) 409 | System.Runtime (>= 4.3) 410 | System.Runtime.Handles (4.3) 411 | Microsoft.NETCore.Platforms (>= 1.1) 412 | Microsoft.NETCore.Targets (>= 1.1) 413 | System.Runtime (>= 4.3) 414 | System.Runtime.InteropServices (4.3) 415 | Microsoft.NETCore.Platforms (>= 1.1) 416 | Microsoft.NETCore.Targets (>= 1.1) 417 | System.Reflection (>= 4.3) 418 | System.Reflection.Primitives (>= 4.3) 419 | System.Runtime (>= 4.3) 420 | System.Runtime.Handles (>= 4.3) 421 | System.Runtime.Numerics (4.3) 422 | System.Globalization (>= 4.3) 423 | System.Resources.ResourceManager (>= 4.3) 424 | System.Runtime (>= 4.3) 425 | System.Runtime.Extensions (>= 4.3) 426 | System.Runtime.Serialization.Primitives (4.3) 427 | System.Resources.ResourceManager (>= 4.3) 428 | System.Runtime (>= 4.3) 429 | System.Security.Claims (4.3) 430 | System.Collections (>= 4.3) 431 | System.Globalization (>= 4.3) 432 | System.IO (>= 4.3) 433 | System.Resources.ResourceManager (>= 4.3) 434 | System.Runtime (>= 4.3) 435 | System.Runtime.Extensions (>= 4.3) 436 | System.Security.Principal (>= 4.3) 437 | System.Security.Cryptography.Algorithms (4.3.1) 438 | Microsoft.NETCore.Platforms (>= 1.1) 439 | runtime.native.System.Security.Cryptography.Apple (>= 4.3.1) 440 | runtime.native.System.Security.Cryptography.OpenSsl (>= 4.3.2) 441 | System.Collections (>= 4.3) 442 | System.IO (>= 4.3) 443 | System.Resources.ResourceManager (>= 4.3) 444 | System.Runtime (>= 4.3) 445 | System.Runtime.Extensions (>= 4.3) 446 | System.Runtime.Handles (>= 4.3) 447 | System.Runtime.InteropServices (>= 4.3) 448 | System.Runtime.Numerics (>= 4.3) 449 | System.Security.Cryptography.Encoding (>= 4.3) 450 | System.Security.Cryptography.Primitives (>= 4.3) 451 | System.Text.Encoding (>= 4.3) 452 | System.Security.Cryptography.Cng (4.7) 453 | System.Security.Cryptography.Csp (4.3) 454 | Microsoft.NETCore.Platforms (>= 1.1) 455 | System.IO (>= 4.3) 456 | System.Reflection (>= 4.3) 457 | System.Resources.ResourceManager (>= 4.3) 458 | System.Runtime (>= 4.3) 459 | System.Runtime.Extensions (>= 4.3) 460 | System.Runtime.Handles (>= 4.3) 461 | System.Runtime.InteropServices (>= 4.3) 462 | System.Security.Cryptography.Algorithms (>= 4.3) 463 | System.Security.Cryptography.Encoding (>= 4.3) 464 | System.Security.Cryptography.Primitives (>= 4.3) 465 | System.Text.Encoding (>= 4.3) 466 | System.Threading (>= 4.3) 467 | System.Security.Cryptography.Encoding (4.3) 468 | Microsoft.NETCore.Platforms (>= 1.1) 469 | runtime.native.System.Security.Cryptography.OpenSsl (>= 4.3) 470 | System.Collections (>= 4.3) 471 | System.Collections.Concurrent (>= 4.3) 472 | System.Linq (>= 4.3) 473 | System.Resources.ResourceManager (>= 4.3) 474 | System.Runtime (>= 4.3) 475 | System.Runtime.Extensions (>= 4.3) 476 | System.Runtime.Handles (>= 4.3) 477 | System.Runtime.InteropServices (>= 4.3) 478 | System.Security.Cryptography.Primitives (>= 4.3) 479 | System.Text.Encoding (>= 4.3) 480 | System.Security.Cryptography.OpenSsl (4.7) 481 | System.Security.Cryptography.Primitives (4.3) 482 | System.Diagnostics.Debug (>= 4.3) 483 | System.Globalization (>= 4.3) 484 | System.IO (>= 4.3) 485 | System.Resources.ResourceManager (>= 4.3) 486 | System.Runtime (>= 4.3) 487 | System.Threading (>= 4.3) 488 | System.Threading.Tasks (>= 4.3) 489 | System.Security.Cryptography.X509Certificates (4.3.2) 490 | Microsoft.NETCore.Platforms (>= 1.1) 491 | runtime.native.System (>= 4.3) 492 | runtime.native.System.Net.Http (>= 4.3) 493 | runtime.native.System.Security.Cryptography.OpenSsl (>= 4.3.2) 494 | System.Collections (>= 4.3) 495 | System.Diagnostics.Debug (>= 4.3) 496 | System.Globalization (>= 4.3) 497 | System.Globalization.Calendars (>= 4.3) 498 | System.IO (>= 4.3) 499 | System.IO.FileSystem (>= 4.3) 500 | System.IO.FileSystem.Primitives (>= 4.3) 501 | System.Resources.ResourceManager (>= 4.3) 502 | System.Runtime (>= 4.3) 503 | System.Runtime.Extensions (>= 4.3) 504 | System.Runtime.Handles (>= 4.3) 505 | System.Runtime.InteropServices (>= 4.3) 506 | System.Runtime.Numerics (>= 4.3) 507 | System.Security.Cryptography.Algorithms (>= 4.3) 508 | System.Security.Cryptography.Cng (>= 4.3) 509 | System.Security.Cryptography.Csp (>= 4.3) 510 | System.Security.Cryptography.Encoding (>= 4.3) 511 | System.Security.Cryptography.OpenSsl (>= 4.3) 512 | System.Security.Cryptography.Primitives (>= 4.3) 513 | System.Text.Encoding (>= 4.3) 514 | System.Threading (>= 4.3) 515 | System.Security.Principal (4.3) 516 | System.Runtime (>= 4.3) 517 | System.Security.Principal.Windows (4.5) 518 | Microsoft.NETCore.Platforms (>= 2.0) - restriction: || (== net5.0) (&& (== netstandard2.0) (>= netcoreapp2.0)) 519 | System.Security.SecureString (4.3) 520 | Microsoft.NETCore.Platforms (>= 1.1) 521 | System.Resources.ResourceManager (>= 4.3) 522 | System.Runtime (>= 4.3) 523 | System.Runtime.Handles (>= 4.3) 524 | System.Runtime.InteropServices (>= 4.3) 525 | System.Security.Cryptography.Primitives (>= 4.3) 526 | System.Text.Encoding (>= 4.3) 527 | System.Threading (>= 4.3) 528 | System.Text.Encoding (4.3) 529 | Microsoft.NETCore.Platforms (>= 1.1) 530 | Microsoft.NETCore.Targets (>= 1.1) 531 | System.Runtime (>= 4.3) 532 | System.Threading (4.3) 533 | System.Runtime (>= 4.3) 534 | System.Threading.Tasks (>= 4.3) 535 | System.Threading.Overlapped (4.3) 536 | Microsoft.NETCore.Platforms (>= 1.1) 537 | System.Resources.ResourceManager (>= 4.3) 538 | System.Runtime (>= 4.3) 539 | System.Runtime.Handles (>= 4.3) 540 | System.Threading.Tasks (4.3) 541 | Microsoft.NETCore.Platforms (>= 1.1) 542 | Microsoft.NETCore.Targets (>= 1.1) 543 | System.Runtime (>= 4.3) 544 | System.Threading.Tasks.Extensions (4.5.4) 545 | System.Runtime.CompilerServices.Unsafe (>= 4.5.3) - restriction: || (&& (== net5.0) (>= net461)) (&& (== net5.0) (< netcoreapp2.1)) (&& (== net5.0) (< netstandard1.0)) (&& (== net5.0) (< netstandard2.0)) (&& (== net5.0) (>= wp8)) (== netstandard2.0) 546 | System.Threading.Thread (4.3) 547 | System.Runtime (>= 4.3) 548 | System.Threading.ThreadPool (4.3) 549 | System.Runtime (>= 4.3) 550 | System.Runtime.Handles (>= 4.3) 551 | System.ValueTuple (4.5) 552 | TaskBuilder.fs (2.1) 553 | FSharp.Core (>= 4.1.17) 554 | NETStandard.Library (>= 1.6.1) 555 | System.ValueTuple (>= 4.4) 556 | Unquote (5.0) 557 | FSharp.Core (>= 4.6.2) 558 | 559 | GROUP FakeBuild 560 | STORAGE: NONE 561 | RESTRICTION: == netstandard2.0 562 | NUGET 563 | remote: https://api.nuget.org/v3/index.json 564 | BlackFox.VsWhere (1.1) 565 | FSharp.Core (>= 4.2.3) 566 | Microsoft.Win32.Registry (>= 4.7) 567 | Fake.Api.GitHub (5.20.4) 568 | FSharp.Core (>= 4.7.2) 569 | Octokit (>= 0.48) 570 | Fake.Core.CommandLineParsing (5.20.4) 571 | FParsec (>= 1.1.1) 572 | FSharp.Core (>= 4.7.2) 573 | Fake.Core.Context (5.20.4) 574 | FSharp.Core (>= 4.7.2) 575 | Fake.Core.Environment (5.20.4) 576 | FSharp.Core (>= 4.7.2) 577 | Fake.Core.FakeVar (5.20.4) 578 | Fake.Core.Context (>= 5.20.4) 579 | FSharp.Core (>= 4.7.2) 580 | Fake.Core.Process (5.20.4) 581 | Fake.Core.Environment (>= 5.20.4) 582 | Fake.Core.FakeVar (>= 5.20.4) 583 | Fake.Core.String (>= 5.20.4) 584 | Fake.Core.Trace (>= 5.20.4) 585 | Fake.IO.FileSystem (>= 5.20.4) 586 | FSharp.Core (>= 4.7.2) 587 | System.Collections.Immutable (>= 1.7.1) 588 | Fake.Core.ReleaseNotes (5.20.4) 589 | Fake.Core.SemVer (>= 5.20.4) 590 | Fake.Core.String (>= 5.20.4) 591 | FSharp.Core (>= 4.7.2) 592 | Fake.Core.SemVer (5.20.4) 593 | FSharp.Core (>= 4.7.2) 594 | Fake.Core.String (5.20.4) 595 | FSharp.Core (>= 4.7.2) 596 | Fake.Core.Target (5.20.4) 597 | Fake.Core.CommandLineParsing (>= 5.20.4) 598 | Fake.Core.Context (>= 5.20.4) 599 | Fake.Core.Environment (>= 5.20.4) 600 | Fake.Core.FakeVar (>= 5.20.4) 601 | Fake.Core.Process (>= 5.20.4) 602 | Fake.Core.String (>= 5.20.4) 603 | Fake.Core.Trace (>= 5.20.4) 604 | FSharp.Control.Reactive (>= 4.4.2) 605 | FSharp.Core (>= 4.7.2) 606 | Fake.Core.Tasks (5.20.4) 607 | Fake.Core.Trace (>= 5.20.4) 608 | FSharp.Core (>= 4.7.2) 609 | Fake.Core.Trace (5.20.4) 610 | Fake.Core.Environment (>= 5.20.4) 611 | Fake.Core.FakeVar (>= 5.20.4) 612 | FSharp.Core (>= 4.7.2) 613 | Fake.Core.Xml (5.20.4) 614 | Fake.Core.String (>= 5.20.4) 615 | FSharp.Core (>= 4.7.2) 616 | Fake.DotNet.AssemblyInfoFile (5.20.4) 617 | Fake.Core.Environment (>= 5.20.4) 618 | Fake.Core.String (>= 5.20.4) 619 | Fake.Core.Trace (>= 5.20.4) 620 | Fake.IO.FileSystem (>= 5.20.4) 621 | FSharp.Core (>= 4.7.2) 622 | Fake.DotNet.Cli (5.20.4) 623 | Fake.Core.Environment (>= 5.20.4) 624 | Fake.Core.Process (>= 5.20.4) 625 | Fake.Core.String (>= 5.20.4) 626 | Fake.Core.Trace (>= 5.20.4) 627 | Fake.DotNet.MSBuild (>= 5.20.4) 628 | Fake.DotNet.NuGet (>= 5.20.4) 629 | Fake.IO.FileSystem (>= 5.20.4) 630 | FSharp.Core (>= 4.7.2) 631 | Mono.Posix.NETStandard (>= 1.0) 632 | Newtonsoft.Json (>= 12.0.3) 633 | Fake.DotNet.MSBuild (5.20.4) 634 | BlackFox.VsWhere (>= 1.1) 635 | Fake.Core.Environment (>= 5.20.4) 636 | Fake.Core.Process (>= 5.20.4) 637 | Fake.Core.String (>= 5.20.4) 638 | Fake.Core.Trace (>= 5.20.4) 639 | Fake.IO.FileSystem (>= 5.20.4) 640 | FSharp.Core (>= 4.7.2) 641 | MSBuild.StructuredLogger (>= 2.1.176) 642 | Fake.DotNet.NuGet (5.20.4) 643 | Fake.Core.Environment (>= 5.20.4) 644 | Fake.Core.Process (>= 5.20.4) 645 | Fake.Core.SemVer (>= 5.20.4) 646 | Fake.Core.String (>= 5.20.4) 647 | Fake.Core.Tasks (>= 5.20.4) 648 | Fake.Core.Trace (>= 5.20.4) 649 | Fake.Core.Xml (>= 5.20.4) 650 | Fake.IO.FileSystem (>= 5.20.4) 651 | Fake.Net.Http (>= 5.20.4) 652 | FSharp.Core (>= 4.7.2) 653 | Newtonsoft.Json (>= 12.0.3) 654 | NuGet.Protocol (>= 5.6) 655 | Fake.DotNet.Paket (5.20.4) 656 | Fake.Core.Process (>= 5.20.4) 657 | Fake.Core.String (>= 5.20.4) 658 | Fake.Core.Trace (>= 5.20.4) 659 | Fake.DotNet.Cli (>= 5.20.4) 660 | Fake.IO.FileSystem (>= 5.20.4) 661 | FSharp.Core (>= 4.7.2) 662 | Fake.IO.FileSystem (5.20.4) 663 | Fake.Core.String (>= 5.20.4) 664 | FSharp.Core (>= 4.7.2) 665 | Fake.Net.Http (5.20.4) 666 | Fake.Core.Trace (>= 5.20.4) 667 | FSharp.Core (>= 4.7.2) 668 | Fake.Tools.Git (5.20.4) 669 | Fake.Core.Environment (>= 5.20.4) 670 | Fake.Core.Process (>= 5.20.4) 671 | Fake.Core.SemVer (>= 5.20.4) 672 | Fake.Core.String (>= 5.20.4) 673 | Fake.Core.Trace (>= 5.20.4) 674 | Fake.IO.FileSystem (>= 5.20.4) 675 | FSharp.Core (>= 4.7.2) 676 | FParsec (1.1.1) 677 | FSharp.Core (>= 4.3.4) 678 | FSharp.Control.Reactive (5.0.2) 679 | FSharp.Core (>= 4.7.2) 680 | System.Reactive (>= 5.0) 681 | FSharp.Core (5.0.2) 682 | Microsoft.Build (17.1) 683 | Microsoft.Build.Framework (17.1) 684 | Microsoft.Win32.Registry (>= 4.3) 685 | System.Security.Permissions (>= 4.7) 686 | Microsoft.Build.Tasks.Core (17.1) 687 | Microsoft.Build.Framework (>= 17.1) 688 | Microsoft.Build.Utilities.Core (>= 17.1) 689 | Microsoft.NET.StringTools (>= 1.0) 690 | Microsoft.Win32.Registry (>= 4.3) 691 | System.CodeDom (>= 4.4) 692 | System.Collections.Immutable (>= 5.0) 693 | System.Reflection.Metadata (>= 1.6) 694 | System.Resources.Extensions (>= 4.6) 695 | System.Security.Cryptography.Pkcs (>= 4.7) 696 | System.Security.Cryptography.Xml (>= 4.7) 697 | System.Security.Permissions (>= 4.7) 698 | System.Threading.Tasks.Dataflow (>= 6.0) 699 | Microsoft.Build.Utilities.Core (17.1) 700 | Microsoft.Build.Framework (>= 17.1) 701 | Microsoft.NET.StringTools (>= 1.0) 702 | Microsoft.Win32.Registry (>= 4.3) 703 | System.Collections.Immutable (>= 5.0) 704 | System.Configuration.ConfigurationManager (>= 4.7) 705 | System.Security.Permissions (>= 4.7) 706 | System.Text.Encoding.CodePages (>= 4.0.1) 707 | Microsoft.NET.StringTools (1.0) 708 | System.Memory (>= 4.5.4) 709 | System.Runtime.CompilerServices.Unsafe (>= 5.0) 710 | Microsoft.NETCore.Platforms (6.0.2) 711 | Microsoft.NETCore.Targets (5.0) 712 | Microsoft.Win32.Registry (5.0) 713 | System.Buffers (>= 4.5.1) 714 | System.Memory (>= 4.5.4) 715 | System.Security.AccessControl (>= 5.0) 716 | System.Security.Principal.Windows (>= 5.0) 717 | Mono.Posix.NETStandard (1.0) 718 | MSBuild.StructuredLogger (2.1.630) 719 | Microsoft.Build (>= 16.10) 720 | Microsoft.Build.Framework (>= 16.10) 721 | Microsoft.Build.Tasks.Core (>= 16.10) 722 | Microsoft.Build.Utilities.Core (>= 16.10) 723 | Newtonsoft.Json (13.0.1) 724 | NuGet.Common (6.1) 725 | NuGet.Frameworks (>= 6.1) 726 | NuGet.Configuration (6.1) 727 | NuGet.Common (>= 6.1) 728 | System.Security.Cryptography.ProtectedData (>= 4.4) 729 | NuGet.Frameworks (6.1) 730 | NuGet.Packaging (6.1) 731 | Newtonsoft.Json (>= 13.0.1) 732 | NuGet.Configuration (>= 6.1) 733 | NuGet.Versioning (>= 6.1) 734 | System.Security.Cryptography.Cng (>= 5.0) 735 | System.Security.Cryptography.Pkcs (>= 5.0) 736 | NuGet.Protocol (6.1) 737 | NuGet.Packaging (>= 6.1) 738 | NuGet.Versioning (6.1) 739 | Octokit (0.50) 740 | System.Buffers (4.5.1) 741 | System.CodeDom (6.0) 742 | System.Collections.Immutable (6.0) 743 | System.Memory (>= 4.5.4) 744 | System.Runtime.CompilerServices.Unsafe (>= 6.0) 745 | System.Configuration.ConfigurationManager (6.0) 746 | System.Security.Cryptography.ProtectedData (>= 6.0) 747 | System.Security.Permissions (>= 6.0) 748 | System.Formats.Asn1 (6.0) 749 | System.Buffers (>= 4.5.1) 750 | System.Memory (>= 4.5.4) 751 | System.Memory (4.5.4) 752 | System.Buffers (>= 4.5.1) 753 | System.Numerics.Vectors (>= 4.4) 754 | System.Runtime.CompilerServices.Unsafe (>= 4.5.3) 755 | System.Numerics.Vectors (4.5) 756 | System.Reactive (5.0) 757 | System.Runtime.InteropServices.WindowsRuntime (>= 4.3) 758 | System.Threading.Tasks.Extensions (>= 4.5.4) 759 | System.Reflection.Metadata (6.0.1) 760 | System.Collections.Immutable (>= 6.0) 761 | System.Resources.Extensions (6.0) 762 | System.Memory (>= 4.5.4) 763 | System.Runtime (4.3.1) 764 | Microsoft.NETCore.Platforms (>= 1.1.1) 765 | Microsoft.NETCore.Targets (>= 1.1.3) 766 | System.Runtime.CompilerServices.Unsafe (6.0) 767 | System.Runtime.InteropServices.WindowsRuntime (4.3) 768 | System.Runtime (>= 4.3) 769 | System.Security.AccessControl (6.0) 770 | System.Security.Principal.Windows (>= 5.0) 771 | System.Security.Cryptography.Cng (5.0) 772 | System.Security.Cryptography.Pkcs (6.0) 773 | System.Buffers (>= 4.5.1) 774 | System.Formats.Asn1 (>= 6.0) 775 | System.Memory (>= 4.5.4) 776 | System.Security.Cryptography.Cng (>= 5.0) 777 | System.Security.Cryptography.ProtectedData (6.0) 778 | System.Memory (>= 4.5.4) 779 | System.Security.Cryptography.Xml (6.0) 780 | System.Memory (>= 4.5.4) 781 | System.Security.AccessControl (>= 6.0) 782 | System.Security.Cryptography.Pkcs (>= 6.0) 783 | System.Security.Permissions (6.0) 784 | System.Security.AccessControl (>= 6.0) 785 | System.Security.Principal.Windows (5.0) 786 | System.Text.Encoding.CodePages (6.0) 787 | System.Memory (>= 4.5.4) 788 | System.Runtime.CompilerServices.Unsafe (>= 6.0) 789 | System.Threading.Tasks.Dataflow (6.0) 790 | System.Threading.Tasks.Extensions (4.5.4) 791 | System.Runtime.CompilerServices.Unsafe (>= 4.5.3) 792 | -------------------------------------------------------------------------------- /src/FSharp.Azure.Storage/AssemblyInfo.fs: -------------------------------------------------------------------------------- 1 | namespace System 2 | open System.Runtime.CompilerServices 3 | 4 | [] 5 | do () -------------------------------------------------------------------------------- /src/FSharp.Azure.Storage/FSharp.Azure.Storage.fsproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | netstandard2.0 4 | obj\AssemblyInfo.Generated.fs 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/FSharp.Azure.Storage/Table.fs: -------------------------------------------------------------------------------- 1 | namespace FSharp.Azure.Storage 2 | 3 | open System 4 | 5 | module Table = 6 | 7 | open System.Collections.Generic 8 | open System.Linq 9 | open System.Reflection 10 | open Microsoft.FSharp.Quotations 11 | open Microsoft.FSharp.Quotations.Patterns 12 | open Microsoft.FSharp.Reflection 13 | open Microsoft.Azure.Cosmos.Table 14 | open FSharp.Control 15 | open Swensen.Unquote 16 | open Utilities 17 | 18 | let MaxBatchSize = 100 19 | 20 | [] 21 | type PartitionKeyAttribute () = inherit Attribute() 22 | 23 | [] 24 | type RowKeyAttribute () = inherit Attribute() 25 | 26 | [] 27 | type EtagAttribute () = inherit Attribute() 28 | 29 | [] 30 | type TimestampAttribute () = inherit Attribute() 31 | 32 | type EntityIdentifier = { [] PartitionKey : string; [] RowKey : string; } 33 | type OperationResult = { HttpStatusCode : int; Etag : string } 34 | type EntityMetadata = { Etag : string; Timestamp : DateTimeOffset } 35 | 36 | type IEntityIdentifiable = 37 | abstract member GetIdentifier : unit -> EntityIdentifier 38 | 39 | type Operation<'T> = 40 | | Insert of entity : 'T 41 | | InsertOrMerge of entity : 'T 42 | | InsertOrReplace of entity : 'T 43 | | Replace of entity : 'T * etag : string 44 | | ForceReplace of entity : 'T 45 | | Merge of entity : 'T * etag : string 46 | | ForceMerge of entity : 'T 47 | | Delete of entity : 'T * etag : string 48 | | ForceDelete of entity : 'T 49 | member this.GetEntity() = 50 | match this with 51 | | Insert (entity) -> entity 52 | | InsertOrMerge (entity) -> entity 53 | | InsertOrReplace (entity) -> entity 54 | | Merge (entity, _) -> entity 55 | | ForceMerge (entity) -> entity 56 | | Replace (entity, _) -> entity 57 | | ForceReplace (entity) -> entity 58 | | Delete (entity, _) -> entity 59 | | ForceDelete (entity) -> entity 60 | 61 | let getOperationEntity (op : Operation<_>) = op.GetEntity() 62 | 63 | let private isRecordType t = FSharpType.IsRecord (t, Reflection.BindingFlags.Public ||| Reflection.BindingFlags.NonPublic) 64 | 65 | let private unionFromString t s = 66 | let cases = FSharpType.GetUnionCases t 67 | match cases |> Array.tryFind (fun case -> case.Name = s) with 68 | | Some case -> FSharpValue.MakeUnion (case,[||]) 69 | | None -> failwithf "The value %s is not a valid union case for %s" s t.Name 70 | 71 | [] 72 | type EntityIdentiferReader<'T> private () = 73 | static let buildIdentiferFromAttributesFunc() = 74 | let partitionKeyProperty = getPropertyByAttribute<'T, PartitionKeyAttribute, string>() 75 | let rowKeyProperty = getPropertyByAttribute<'T, RowKeyAttribute, string>() 76 | 77 | 78 | fun (t:'T) -> 79 | let pk = partitionKeyProperty.GetValue(t) :?> string 80 | let rk = rowKeyProperty.GetValue(t) :?> string 81 | { PartitionKey = pk ; RowKey = rk } 82 | 83 | static let defaultGetIdentifier = lazy( 84 | match typeof<'T> with 85 | | t when typeof.IsAssignableFrom t -> fun (e : 'T) -> (box e :?> IEntityIdentifiable).GetIdentifier() 86 | | t when typeof.IsAssignableFrom t -> fun (e : 'T) -> 87 | let tableEntity = (box e :?> ITableEntity) 88 | { PartitionKey = tableEntity.PartitionKey; RowKey = tableEntity.RowKey } 89 | | _ -> buildIdentiferFromAttributesFunc()) 90 | 91 | static let mutable getIdentifier = fun g -> defaultGetIdentifier.Value g 92 | 93 | static member GetIdentifier with get() = getIdentifier and set fn = getIdentifier <- fn 94 | 95 | type private RecordTableEntityWrapper<'T>(record : 'T, identifier, etag) = 96 | static let recordFields = 97 | FSharpType.GetRecordFields (typeof<'T>, true) 98 | static let recordReader = 99 | FSharpValue.PreComputeRecordReader (typeof<'T>, true) 100 | static let recordWriter = 101 | FSharpValue.PreComputeRecordConstructor (typeof<'T>, true) >> (fun o -> o :?> 'T) 102 | static let propertiesWithUnionTypesThatHaveCasesWithFields = 103 | recordFields 104 | |> Seq.collect (fun pi -> 105 | let underlyingType = getUnderlyingTypeIfOption pi.PropertyType 106 | if FSharpType.IsUnion underlyingType then 107 | FSharpType.GetUnionCases underlyingType 108 | |> Seq.filter (fun case -> case.GetFields() |> Array.isEmpty |> not) 109 | |> Seq.map (fun case -> (pi, underlyingType, case)) 110 | else 111 | Seq.empty 112 | ) 113 | |> Seq.toArray 114 | static let throwIfPropertiesAreOfUnionTypesThatHaveCasesWithFields () = 115 | if propertiesWithUnionTypesThatHaveCasesWithFields.Length > 0 then 116 | propertiesWithUnionTypesThatHaveCasesWithFields 117 | |> Seq.map (fun (pi, underlyingType, case) -> sprintf "- Field '%s' using union type '%s' has case '%s' that has fields" pi.Name underlyingType.FullName case.Name) 118 | |> String.join Environment.NewLine 119 | |> failwithf "Record %s has properties that contains one or more union types with fields, which is unsupported.%s%s" typeof<'T>.FullName Environment.NewLine 120 | 121 | do throwIfPropertiesAreOfUnionTypesThatHaveCasesWithFields () 122 | 123 | static member ResolveRecord (pk : string) (rk : string) (timestamp: DateTimeOffset) (properties : IDictionary) (etag : string) = 124 | throwIfPropertiesAreOfUnionTypesThatHaveCasesWithFields () 125 | let propValues = 126 | recordFields 127 | |> Seq.map (fun f -> 128 | match properties |> tryGet f.Name with 129 | | Some prop -> 130 | let underlyingPropertyType = getUnderlyingTypeIfOption f.PropertyType 131 | match prop.PropertyAsObject with 132 | | null -> runtimeGetUncheckedDefault f.PropertyType 133 | | value when value.GetType() = typeof && underlyingPropertyType = typeof -> 134 | DateTimeOffset(value :?> DateTime) |> wrapIfOption f.PropertyType 135 | | value when value.GetType() = typeof && underlyingPropertyType = typeof -> 136 | Uri(value :?> string) |> wrapIfOption f.PropertyType 137 | | value when value.GetType() = typeof && FSharpType.IsUnion underlyingPropertyType -> 138 | unionFromString underlyingPropertyType (value :?> string) |> wrapIfOption f.PropertyType 139 | | value when underlyingPropertyType.IsEnum -> 140 | Enum.Parse(underlyingPropertyType, (value :?> string)) |> wrapIfOption f.PropertyType 141 | | value when value.GetType() <> underlyingPropertyType -> 142 | failwithf "The property %s on type %s of type %s has deserialized as the incorrect type %s" f.Name typeof<'T>.Name f.PropertyType.Name (value.GetType().Name) 143 | | value -> value |> wrapIfOption f.PropertyType 144 | | None -> 145 | match (f.Name, f.GetCustomAttribute(), f.GetCustomAttribute()) with 146 | | ("PartitionKey", _ , _) when f.PropertyType = typeof -> pk :> obj 147 | | ("RowKey", _, _) when f.PropertyType = typeof -> rk :> obj 148 | | ("Timestamp", _, _) when f.PropertyType = typeof -> timestamp :> obj 149 | | (_, _, ts) when ts <> null && f.PropertyType = typeof -> timestamp :> obj 150 | | (_, et, _) when et <> null && f.PropertyType = typeof -> etag :> obj 151 | | _ -> runtimeGetUncheckedDefault f.PropertyType) 152 | |> Seq.toArray 153 | (recordWriter propValues), { Etag = etag; Timestamp = timestamp } 154 | 155 | static member RecordFields = recordFields 156 | 157 | interface ITableEntity with 158 | member val PartitionKey : string = identifier.PartitionKey with get, set 159 | member val RowKey : string = identifier.RowKey with get, set 160 | member val ETag : string = etag with get, set 161 | member val Timestamp : DateTimeOffset = Unchecked.defaultof<_> with get, set 162 | 163 | member __.ReadEntity(_, _) = 164 | notImplemented() 165 | 166 | member __.WriteEntity(_) = 167 | 168 | let filter (info : PropertyInfo, _) = 169 | info.Name <> "PartitionKey" 170 | && info.Name <> "RowKey" 171 | && info.GetCustomAttribute() = null 172 | && info.GetCustomAttribute() = null 173 | 174 | record 175 | |> recordReader 176 | |> Seq.map (unwrapIfOption >> EntityProperty.CreateEntityPropertyFromObject) 177 | |> Seq.zip (recordFields) 178 | |> Seq.filter (filter) 179 | |> Seq.map (fun (info, obj) -> (info.Name, obj)) 180 | |> dict 181 | 182 | [] 183 | type private EntityTypeCache<'T> private () = 184 | static let tableEntityTypeConstructor = lazy( 185 | let ctor = typeof<'T>.GetConstructor([||]) 186 | if ctor = null then failwithf "Type %s does not have a parameterless constructor" typeof<'T>.Name 187 | fun () -> ctor.Invoke [||] :?> 'T) 188 | 189 | static let resolveTableEntity (pk : string) (rk : string) (timestamp: DateTimeOffset) (properties : IDictionary) (etag : string) = 190 | let entity = tableEntityTypeConstructor.Value() 191 | let tableEntity = box entity :?> ITableEntity 192 | do tableEntity.PartitionKey <- pk 193 | do tableEntity.RowKey <- rk 194 | do tableEntity.Timestamp <- timestamp 195 | do tableEntity.ReadEntity(properties, null) 196 | do tableEntity.ETag <- etag 197 | entity, { Etag = etag; Timestamp = timestamp } 198 | 199 | static let createTableOperationFromTableEntity (tableOperation : ITableEntity -> TableOperation) (entity : 'T) etag = 200 | let entity = box entity :?> ITableEntity 201 | entity.ETag <- etag 202 | entity |> tableOperation 203 | 204 | static let createTableOperationFromRecord (tableOperation : ITableEntity -> TableOperation) (record : 'T) etag = 205 | let eId = EntityIdentiferReader.GetIdentifier record 206 | RecordTableEntityWrapper (record, eId, etag) |> tableOperation 207 | 208 | static member val PropertyNames = lazy ( 209 | let getTableEntityProperty (p : PropertyInfo) = 210 | match p.GetCustomAttribute() with 211 | | null when p.Name <> "ETag" -> Some (p.Name) //ETag is not an entity property to be queried 212 | | _ -> None 213 | 214 | match typeof<'T> with 215 | | t when typeof.IsAssignableFrom t -> 216 | Set.empty // It's dynamic so we don't know what properties to query for, so query them all 217 | | t when typeof.IsAssignableFrom t -> 218 | t.GetProperties() 219 | |> Seq.choose getTableEntityProperty 220 | |> Set.ofSeq 221 | | t when isRecordType t -> 222 | RecordTableEntityWrapper<'T>.RecordFields 223 | |> Seq.map (fun p -> p.Name) 224 | |> Set.ofSeq 225 | | t -> failwithf "Type %s must be either an ITableEntity or an F# record type" t.Name 226 | ) 227 | 228 | static member val Resolver = lazy ( 229 | match typeof<'T> with 230 | | t when typeof.IsAssignableFrom t -> resolveTableEntity 231 | | t when isRecordType t -> RecordTableEntityWrapper.ResolveRecord 232 | | t -> failwithf "Type %s must be either an ITableEntity or an F# record type" t.Name) 233 | 234 | static member val CreateTableOperation = lazy ( 235 | match typeof<'T> with 236 | | t when typeof.IsAssignableFrom t -> createTableOperationFromTableEntity 237 | | t when isRecordType t -> createTableOperationFromRecord 238 | | _ -> failwithf "Type %s must be either an ITableEntity or an F# record type" typeof<'T>.Name) 239 | 240 | type EntityQuery<'T> = 241 | { Filter : string 242 | TakeCount : int option 243 | SelectColumns : string Set } 244 | static member get_Zero() : EntityQuery<'T> = 245 | { Filter = "" 246 | TakeCount = None 247 | SelectColumns = EntityTypeCache<'T>.PropertyNames.Value } 248 | static member (+) (left : EntityQuery<'T>, right : EntityQuery<'T>) = 249 | let filter = 250 | match left.Filter, right.Filter with 251 | | "", "" -> "" 252 | | l, "" -> l 253 | | "", r -> r 254 | | l, r -> TableQuery.CombineFilters (l, "and", r) 255 | let takeCount = 256 | match left.TakeCount, right.TakeCount with 257 | | Some l, Some r -> Some (min l r) 258 | | Some l, None -> Some l 259 | | None, Some r -> Some r 260 | | None, None -> None 261 | let selectColumns = left.SelectColumns |> Set.intersect right.SelectColumns 262 | 263 | { Filter = filter; TakeCount = takeCount; SelectColumns = selectColumns } 264 | 265 | member this.ToTableQuery() = 266 | TableQuery ( 267 | FilterString = this.Filter, 268 | TakeCount = (this.TakeCount |> toNullable), 269 | SelectColumns = (this.SelectColumns |> Set.toArray)) 270 | 271 | 272 | module Query = 273 | open DerivedPatterns 274 | 275 | type SystemProperties = 276 | { PartitionKey : string 277 | RowKey : string 278 | Timestamp : DateTimeOffset } 279 | 280 | type private Comparison = 281 | | Equals 282 | | GreaterThan 283 | | GreaterThanOrEqual 284 | | LessThan 285 | | LessThanOrEqual 286 | | NotEqual 287 | member this.CommutativeInvert() = 288 | match this with 289 | | GreaterThan -> LessThan 290 | | GreaterThanOrEqual -> LessThanOrEqual 291 | | LessThan -> GreaterThan 292 | | LessThanOrEqual -> GreaterThanOrEqual 293 | | Equals -> Equals 294 | | NotEqual -> NotEqual 295 | 296 | let private toOperator comparison = 297 | match comparison with 298 | | Equals -> QueryComparisons.Equal 299 | | GreaterThan -> QueryComparisons.GreaterThan 300 | | GreaterThanOrEqual -> QueryComparisons.GreaterThanOrEqual 301 | | LessThan -> QueryComparisons.LessThan 302 | | LessThanOrEqual -> QueryComparisons.LessThanOrEqual 303 | | NotEqual -> QueryComparisons.NotEqual 304 | 305 | let private notFilter filter = 306 | sprintf "not (%s)" filter 307 | 308 | let private (|ComparisonOp|_|) (expr : Expr) = 309 | match expr with 310 | | SpecificCall <@ (=) @> (_, _, [left; right]) -> Some (ComparisonOp Equals, left, right) 311 | | SpecificCall <@ (>) @> (_, _, [left; right]) -> Some (ComparisonOp GreaterThan, left, right) 312 | | SpecificCall <@ (>=) @> (_, _, [left; right]) -> Some (ComparisonOp GreaterThanOrEqual, left, right) 313 | | SpecificCall <@ (<) @> (_, _, [left; right]) -> Some (ComparisonOp LessThan, left, right) 314 | | SpecificCall <@ (<=) @> (_, _, [left; right]) -> Some (ComparisonOp LessThanOrEqual, left, right) 315 | | SpecificCall <@ (<>) @> (_, _, [left; right]) -> Some (ComparisonOp NotEqual, left, right) 316 | | _ -> None 317 | 318 | let private (|PropertyComparison|_|) (expr : Expr) = 319 | match expr with 320 | | ComparisonOp (op, PropertyGet (Some (Var(v)), prop, []), valExpr) -> 321 | Some (PropertyComparison (v, prop, op, valExpr)) 322 | | ComparisonOp (op, valExpr, PropertyGet (Some (Var(v)), prop, [])) -> 323 | Some (PropertyComparison (v, prop, op.CommutativeInvert(), valExpr)) 324 | | PropertyGet (Some (Var(v)), prop, []) when prop.PropertyType = typeof -> 325 | Some (PropertyComparison (v, prop, Equals, Expr.Value(true))) 326 | | SpecificCall <@ not @> (None, _, [ PropertyGet (Some (Var(v)), prop, []) ]) when prop.PropertyType = typeof -> 327 | Some (PropertyComparison (v, prop, Equals, Expr.Value(false))) 328 | | _ -> None 329 | 330 | let private (|ComparisonValue|_|) (expr : Expr) = 331 | match expr with 332 | | Value (o, _) -> Some(ComparisonValue (o)) 333 | | expr when expr.GetFreeVars().Any() -> failwithf "Cannot evaluate %A to a comparison value as it contains free variables" expr 334 | | expr -> Some(ComparisonValue (evalRaw expr)) 335 | 336 | let private generateFilterCondition (type' : Type) propertyName op (value : obj) = 337 | match value with 338 | | null -> failwithf "Null comparison is not supported by table storage (property: %s)" propertyName 339 | | :? string as v -> TableQuery.GenerateFilterCondition (propertyName, op |> toOperator, v) 340 | | :? (string option) as v -> TableQuery.GenerateFilterCondition (propertyName, op |> toOperator, v.Value) 341 | | :? (byte[]) as v -> TableQuery.GenerateFilterConditionForBinary (propertyName, op |> toOperator, v) 342 | | :? (byte[] option) as v -> TableQuery.GenerateFilterConditionForBinary (propertyName, op |> toOperator, v.Value) 343 | | :? bool as v -> TableQuery.GenerateFilterConditionForBool (propertyName, op |> toOperator, v) 344 | | :? (bool option) as v -> TableQuery.GenerateFilterConditionForBool (propertyName, op |> toOperator, v.Value) 345 | | :? DateTimeOffset as v -> TableQuery.GenerateFilterConditionForDate (propertyName, op |> toOperator, v) 346 | | :? (DateTimeOffset option) as v -> TableQuery.GenerateFilterConditionForDate (propertyName, op |> toOperator, v.Value) 347 | | :? DateTime as v -> TableQuery.GenerateFilterConditionForDate (propertyName, op |> toOperator, new DateTimeOffset(v)) 348 | | :? (DateTime option) as v -> TableQuery.GenerateFilterConditionForDate (propertyName, op |> toOperator, new DateTimeOffset(v.Value)) 349 | | :? double as v -> TableQuery.GenerateFilterConditionForDouble (propertyName, op |> toOperator, v) 350 | | :? (double option) as v -> TableQuery.GenerateFilterConditionForDouble (propertyName, op |> toOperator, v.Value) 351 | | :? Guid as v -> TableQuery.GenerateFilterConditionForGuid (propertyName, op |> toOperator, v) 352 | | :? (Guid option) as v -> TableQuery.GenerateFilterConditionForGuid (propertyName, op |> toOperator, v.Value) 353 | | :? int as v -> TableQuery.GenerateFilterConditionForInt (propertyName, op |> toOperator, v) 354 | | :? (int option) as v -> TableQuery.GenerateFilterConditionForInt (propertyName, op |> toOperator, v.Value) 355 | | :? int64 as v -> TableQuery.GenerateFilterConditionForLong (propertyName, op |> toOperator, v) 356 | | :? (int64 option) as v -> TableQuery.GenerateFilterConditionForLong (propertyName, op |> toOperator, v.Value) 357 | | :? Uri as v -> TableQuery.GenerateFilterCondition(propertyName, op |> toOperator, v.ToString()) 358 | | :? (Uri option) as v -> TableQuery.GenerateFilterCondition(propertyName, op |> toOperator, v.Value.ToString()) 359 | | _ -> failwithf "Unexpected property type %s for property %s" type'.Name propertyName 360 | 361 | let private isPropertyComparisonAgainstBool expr = 362 | match expr with 363 | | PropertyComparison (_, prop, _, _) when prop.PropertyType = typeof -> true 364 | | _ -> false 365 | 366 | let private buildPropertyFilter entityVar sysPropVar expr = 367 | let rec buildPropertyFilterRec expr = 368 | match expr with 369 | | AndAlso (left, right) -> 370 | TableQuery.CombineFilters(buildPropertyFilterRec left, "and", buildPropertyFilterRec right) 371 | | OrElse (left, right) -> 372 | TableQuery.CombineFilters(buildPropertyFilterRec left, "or", buildPropertyFilterRec right) 373 | | SpecificCall <@ not @> (None, _, [nottedExpr]) when not (nottedExpr |> isPropertyComparisonAgainstBool) -> 374 | notFilter (buildPropertyFilterRec nottedExpr) 375 | | PropertyComparison (v, prop, op, ComparisonValue (value)) -> 376 | if v <> entityVar && v <> sysPropVar then 377 | failwithf "Comparison (%A) to property (%s) on value that is not the function parameter (%s)" op prop.Name v.Name 378 | generateFilterCondition prop.PropertyType prop.Name op value 379 | | _ -> failwithf "Unable to understand expression: %A" expr 380 | buildPropertyFilterRec expr 381 | 382 | let private makePropertyFilter (expr : Expr<'T -> SystemProperties -> bool>) = 383 | if expr.GetFreeVars().Any() then 384 | failwithf "The expression %A contains free variables." expr 385 | match expr with 386 | | Lambda (entityVar, Lambda (sysPropVar, expr)) -> buildPropertyFilter entityVar sysPropVar expr 387 | | _ -> failwith "Unexpected expression; lambda not found" 388 | 389 | 390 | let all<'T> : EntityQuery<'T> = EntityQuery.get_Zero() 391 | 392 | let where (expr : Expr<'T -> SystemProperties -> bool>) (query : EntityQuery<'T>) = 393 | [ query; { EntityQuery<'T>.get_Zero() with Filter = expr |> makePropertyFilter } ] |> List.reduce (+) 394 | 395 | let take count (query : EntityQuery<'T>) = 396 | [ query; { EntityQuery<'T>.get_Zero() with TakeCount = Some count } ] |> List.reduce (+) 397 | 398 | 399 | let convertToTableOperation operation = 400 | match operation with 401 | | Insert (entity) -> EntityTypeCache.CreateTableOperation.Value TableOperation.Insert entity null 402 | | InsertOrMerge (entity) -> EntityTypeCache.CreateTableOperation.Value TableOperation.InsertOrMerge entity null 403 | | InsertOrReplace (entity) -> EntityTypeCache.CreateTableOperation.Value TableOperation.InsertOrReplace entity null 404 | | Merge (entity, etag) -> EntityTypeCache.CreateTableOperation.Value TableOperation.Merge entity etag 405 | | ForceMerge (entity) -> EntityTypeCache.CreateTableOperation.Value TableOperation.Merge entity "*" 406 | | Replace (entity, etag) -> EntityTypeCache.CreateTableOperation.Value TableOperation.Replace entity etag 407 | | ForceReplace (entity) -> EntityTypeCache.CreateTableOperation.Value TableOperation.Replace entity "*" 408 | | Delete (entity, etag) -> EntityTypeCache.CreateTableOperation.Value TableOperation.Delete entity etag 409 | | ForceDelete (entity) -> EntityTypeCache.CreateTableOperation.Value TableOperation.Delete entity "*" 410 | 411 | let private createBatchOperation operations = 412 | let batchOperation = TableBatchOperation() 413 | do operations |> Seq.map convertToTableOperation |> Seq.iter batchOperation.Add 414 | batchOperation 415 | 416 | let private convertToOperationResult (result : TableResult) = 417 | { HttpStatusCode = result.HttpStatusCode; Etag = result.Etag } 418 | 419 | let inline private syncOverAsync a = 420 | a |> Async.UnwrapAggregateException |> Async.RunSynchronously 421 | 422 | module Task = 423 | open FSharp.Control.Tasks.V2 424 | 425 | let inTableAsync (client: CloudTableClient) tableName operation = 426 | task { 427 | let table = client.GetTableReference tableName 428 | let tableOperation = operation |> convertToTableOperation 429 | let! result = tableOperation |> table.ExecuteAsync 430 | return result |> convertToOperationResult 431 | } 432 | 433 | let inTableAsBatchAsync (client: CloudTableClient) tableName operations = 434 | task { 435 | let table = client.GetTableReference tableName 436 | let batchOperation = operations |> createBatchOperation 437 | let! results = batchOperation |> table.ExecuteBatchAsync 438 | return results |> Seq.map convertToOperationResult |> Seq.toList 439 | } 440 | 441 | let fromTableSegmentedAsync (client: CloudTableClient) tableName continuationToken (query : EntityQuery<'T>) = 442 | let table = client.GetTableReference tableName 443 | let tableQuery = query.ToTableQuery() 444 | let resolver = EntityTypeCache.Resolver.Value //Do not inline this otherwise FSharp will delay execution of .Value until the resolver delegate is called 445 | task { 446 | let! result = table.ExecuteQuerySegmentedAsync<'T * EntityMetadata>(tableQuery, EntityResolver(resolver), continuationToken |> toNullRef) 447 | return result.Results, result.ContinuationToken |> toOption 448 | } 449 | 450 | let fromTableAsync (client: CloudTableClient) tableName (query : EntityQuery<'T>) = 451 | task { 452 | let takeCount = query.TakeCount |> Option.defaultValue Int32.MaxValue 453 | 454 | // we need to use mutation here because TaskBuilder.fs doesn't support tail recursion and the 455 | // stack could blow up in case we have a very large result set. 456 | // allResults is a normal dotnet List and not a fsharp list so we mutate it. 457 | let allResults = List<'T * EntityMetadata>() 458 | let mutable token = None 459 | let mutable shouldContinue = true 460 | 461 | while shouldContinue do 462 | //When using segmentation, the table storage take param is applied to each segment not to the entire resultset 463 | //So we need to keep track of how many results we want to actually take and stop early if necessary 464 | let! segmentResult = query |> fromTableSegmentedAsync client tableName token 465 | let currentResult = segmentResult |> fst 466 | 467 | token <- segmentResult |> snd 468 | 469 | let totalSize = allResults.Count + currentResult.Count 470 | if totalSize > takeCount then 471 | allResults.AddRange(currentResult.Take(takeCount - allResults.Count)) 472 | shouldContinue <- false 473 | else 474 | allResults.AddRange(currentResult) 475 | shouldContinue <- (token |> Option.isSome) && allResults.Count < takeCount 476 | 477 | return (allResults :> seq<_>) 478 | } 479 | 480 | let inTableAsync (client: CloudTableClient) tableName operation = 481 | async { return! Task.inTableAsync client tableName operation |> Async.AwaitTask } 482 | 483 | let inTable client tableName operation = 484 | inTableAsync client tableName operation |> syncOverAsync 485 | 486 | let inTableAsBatchAsync (client: CloudTableClient) tableName operations = 487 | async { return! Task.inTableAsBatchAsync client tableName operations |> Async.AwaitTask } 488 | 489 | let inTableAsBatch client tableName operations = 490 | inTableAsBatchAsync client tableName operations |> syncOverAsync 491 | 492 | let private validateNoDuplicateRowKeys pk ops = 493 | let duplicates = 494 | ops 495 | |> Seq.countBy (fun (eId, _) -> eId.RowKey) 496 | |> Seq.filter (fun (_, count) -> count > 1) 497 | |> Seq.cache 498 | if duplicates |> Seq.isEmpty |> not then 499 | let dupStr = duplicates |> Seq.fold (fun str (rk, _) -> str + sprintf "\r\n- '%s'" rk) "" 500 | failwithf "Cannot automatically batch operations because multiple entities addressing the same rows exist for partition '%s' with row keys:%s" pk dupStr 501 | else ops 502 | 503 | let autobatch operations = 504 | operations 505 | |> Seq.map (fun o -> o |> getOperationEntity |> EntityIdentiferReader.GetIdentifier, o) 506 | |> Seq.groupBy (fun (eId, _) -> eId.PartitionKey) 507 | |> Seq.collect (fun (pk, ops) -> 508 | ops 509 | |> validateNoDuplicateRowKeys pk 510 | |> Seq.map snd 511 | |> Seq.split MaxBatchSize 512 | |> Seq.map Seq.toList) 513 | |> Seq.toList 514 | 515 | let fromTableSegmentedAsync (client: CloudTableClient) tableName continuationToken (query : EntityQuery<'T>) = 516 | async { return! Task.fromTableSegmentedAsync client tableName continuationToken query |> Async.AwaitTask } 517 | 518 | let fromTableSegmentedAsyncSeq (client: CloudTableClient) tableName (query : EntityQuery<'T>) = 519 | AsyncSeq.unfoldAsync (fun token -> async { 520 | match token with 521 | | Some token -> 522 | let! (rows, continuationToken) = 523 | query 524 | |> fromTableSegmentedAsync client tableName token 525 | return Some (rows, continuationToken |> Option.map Some) 526 | | None -> 527 | return None 528 | }) (Some None) 529 | 530 | let fromTableAsync (client: CloudTableClient) tableName (query : EntityQuery<'T>) = 531 | async { return! Task.fromTableAsync client tableName query |> Async.AwaitTask } 532 | 533 | let fromTable client tableName query = 534 | fromTableAsync client tableName query |> syncOverAsync 535 | 536 | let fromTableSegmented client tableName continuationToken query = 537 | fromTableSegmentedAsync client tableName continuationToken query |> syncOverAsync 538 | -------------------------------------------------------------------------------- /src/FSharp.Azure.Storage/Utilities.fs: -------------------------------------------------------------------------------- 1 | namespace FSharp.Azure.Storage 2 | 3 | open System; 4 | 5 | module internal String = 6 | let inline join (separator : string) (strs : string seq) = String.Join (separator, strs) 7 | 8 | module internal Async = 9 | let Raise (e : #exn) = 10 | Async.FromContinuations(fun (_,econt,_) -> econt e) 11 | 12 | let UnwrapAggregateException a = 13 | async { 14 | let! result = Async.Catch a 15 | return! 16 | match result with 17 | | Choice1Of2 x -> async.Return x 18 | | Choice2Of2 (:? AggregateException as ex) -> 19 | let inners = ex.Flatten().InnerExceptions 20 | if inners.Count > 1 21 | then Raise ex 22 | else Raise inners.[0] 23 | | Choice2Of2 ex -> Raise ex 24 | } 25 | 26 | module internal Seq = 27 | let split n = 28 | Seq.fold (fun (count, currSeq, seqs) item -> 29 | if count = n 30 | then 1, item |> Seq.singleton, currSeq |> Seq.singleton |> Seq.append seqs 31 | else count + 1, item |> Seq.singleton |> Seq.append currSeq, seqs) 32 | (0, Seq.empty, Seq.empty) 33 | >> (fun (_, currSeq, seqs) -> currSeq |> Seq.singleton |> Seq.append seqs) 34 | 35 | module internal Utilities = 36 | 37 | open System.Reflection 38 | 39 | let inline (|?) (lhs: 'a option) rhs = (if lhs.IsSome then lhs.Value else rhs) 40 | let inline toNullable (opt: 'a option) = if opt.IsSome then Nullable(opt.Value) else Nullable() 41 | let inline toNullRef (opt: 'a option) = if opt.IsSome then opt.Value else null 42 | let inline toOption o = match o with | null -> None | _ -> Some(o) 43 | let inline isNull o = match o with | null -> true | _ -> false 44 | let inline isNotNull o = match o with | null -> false | _ -> true 45 | 46 | let notImplemented() = 47 | raise (NotImplementedException()) 48 | Unchecked.defaultof<_> 49 | 50 | let runtimeGetUncheckedDefault (t : Type) = 51 | if t.IsValueType && Nullable.GetUnderlyingType t |> isNull then 52 | Activator.CreateInstance t 53 | else 54 | null 55 | 56 | let tryGet key (dict : System.Collections.Generic.IDictionary<_,_>) = 57 | let mutable value = null; 58 | match dict.TryGetValue (key, &value) with 59 | | true -> Some value 60 | | false -> None 61 | 62 | let getPropertyByAttribute<'T, 'TAttr, 'TReturn when 'TAttr :> Attribute and 'TAttr : null>() = 63 | let properties = 64 | typeof<'T>.GetProperties(BindingFlags.Public ||| BindingFlags.NonPublic ||| BindingFlags.Instance ||| BindingFlags.Static) 65 | |> Seq.where (fun p -> p.CanRead) 66 | |> Seq.where (fun p -> p.GetCustomAttribute<'TAttr>() |> isNotNull) 67 | |> Seq.toList 68 | 69 | match properties with 70 | | [h] when h.PropertyType = typeof<'TReturn> -> h 71 | | [h] -> failwithf "The property %s on type %s that is marked with %s is not of type %s" h.Name typeof<'T>.Name typeof<'TAttr>.Name typeof<'TReturn>.Name 72 | | _ :: _ -> failwithf "The type %s contains more than one property with %s" typeof<'T>.Name typeof<'TAttr>.Name 73 | | [] -> failwithf "The type %s does not contain a property with %s" typeof<'T>.Name typeof<'TAttr>.Name 74 | 75 | 76 | let inline private isOptionType (t : Type) = 77 | t.IsGenericType && t.GetGenericTypeDefinition() = typedefof 78 | 79 | let getUnderlyingTypeIfOption (t : Type) = 80 | if t |> isOptionType then 81 | t.GetGenericArguments().[0] 82 | else 83 | t 84 | 85 | let unwrapIfOption (o : obj) = 86 | match o with 87 | | null -> null 88 | | :? (string option) as opt -> opt.Value :> obj 89 | | :? (byte[] option) as opt -> opt.Value :> obj 90 | | :? (bool option) as opt -> opt.Value :> obj 91 | | :? (DateTimeOffset option) as opt -> opt.Value :> obj 92 | | :? (DateTime option) as opt -> opt.Value :> obj 93 | | :? (double option) as opt -> opt.Value :> obj 94 | | :? (Guid option) as opt -> opt.Value :> obj 95 | | :? (int option) as opt -> opt.Value :> obj 96 | | :? (int64 option) as opt -> opt.Value :> obj 97 | | :? (Uri option) as opt -> opt.Value :> obj 98 | | other -> other 99 | 100 | let wrapIfOption (t : Type) (o : obj) = 101 | if t |> isOptionType then 102 | match o with 103 | | null -> null 104 | | :? string as v -> Some v :> obj 105 | | :? (byte[]) as v -> Some v :> obj 106 | | :? bool as v -> Some v :> obj 107 | | :? DateTimeOffset as v -> Some v :> obj 108 | | :? DateTime as v -> Some v :> obj 109 | | :? double as v -> Some v :> obj 110 | | :? Guid as v -> Some v :> obj 111 | | :? int as v -> Some v :> obj 112 | | :? int64 as v -> Some v :> obj 113 | | :? Uri as v -> Some v :> obj 114 | | other -> other 115 | else o -------------------------------------------------------------------------------- /src/FSharp.Azure.Storage/paket.references: -------------------------------------------------------------------------------- 1 | FSharp.Control.AsyncSeq 2 | FSharp.Core 3 | Microsoft.Azure.Cosmos.Table 4 | TaskBuilder.fs 5 | Unquote 6 | -------------------------------------------------------------------------------- /src/FSharp.Azure.Storage/paket.template: -------------------------------------------------------------------------------- 1 | type project 2 | id FSharp.Azure.Storage 3 | description 4 | A library that provides an idiomatic F# API for Microsoft Azure services. 5 | authors 6 | Daniel Chambers & Contributors 7 | owners 8 | Daniel Chambers & Contributors 9 | copyright 10 | Copyright 2022 11 | summary 12 | A library that provides an idiomatic F# API for Microsoft Azure services. 13 | licenseurl https://github.com/fsprojects/FSharp.Azure.Storage/blob/master/LICENCE.txt 14 | projecturl https://github.com/fsprojects/FSharp.Azure.Storage 15 | iconurl https://raw.githubusercontent.com/fsprojects/FSharp.Azure.Storage/master/media/Logo.128.png 16 | tags 17 | fsharp, f#, microsoft, azure, table, storage -------------------------------------------------------------------------------- /test/FSharp.Azure.Storage.Tests/FSharp.Azure.Storage.Tests.fsproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Exe 5 | netcoreapp5.0 6 | false 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /test/FSharp.Azure.Storage.Tests/Program.fs: -------------------------------------------------------------------------------- 1 | module FSharp.Azure.Storage.Tests.Program 2 | 3 | open System 4 | open Expecto 5 | 6 | [] 7 | let main argv = 8 | let connStrEnvVar = Environment.GetEnvironmentVariable "FSHARP_AZURE_STORAGE_CONNECTION_STRING" 9 | let connectionString = 10 | if String.IsNullOrWhiteSpace connStrEnvVar 11 | then 12 | do printfn "Using Development Storage for tests" 13 | "UseDevelopmentStorage=true;" 14 | else 15 | do printfn "Using connection string from FSHARP_AZURE_STORAGE_CONNECTION_STRING env var for tests" 16 | connStrEnvVar 17 | 18 | runTestsWithArgs defaultConfig argv <| 19 | testList "FSharp.Azure.Storage" [ 20 | Table.QueryExpressionTests.tests 21 | Table.DataModificationTests.tests connectionString 22 | Table.DataQueryTests.tests connectionString 23 | ] 24 | -------------------------------------------------------------------------------- /test/FSharp.Azure.Storage.Tests/Table/DataModificationTests.fs: -------------------------------------------------------------------------------- 1 | module FSharp.Azure.Storage.Tests.Table.DataModificationTests 2 | 3 | open System 4 | open FSharp.Azure.Storage.Table 5 | open Expecto 6 | open Expecto.Flip 7 | open FSharp.Azure.Storage.Tests 8 | open Microsoft.Azure.Cosmos.Table 9 | 10 | type GameWithOptions = 11 | { [] Name: string 12 | Platform: string option 13 | [] Developer : string 14 | HasMultiplayer: bool option } 15 | 16 | type GameSummary = 17 | { Name: string 18 | Platform: string 19 | Developer : string } 20 | interface IEntityIdentifiable with 21 | member g.GetIdentifier() = 22 | { PartitionKey = g.Developer; RowKey = g.Name } 23 | 24 | type Game = 25 | { [] Name: string 26 | Platform: string 27 | [] Developer : string 28 | HasMultiplayer: bool } 29 | 30 | type PureRecord = 31 | { SuchPure : string 32 | VeryClean : string 33 | Wow : bool } 34 | 35 | let getPureIdentifier p = { PartitionKey = p.SuchPure; RowKey = p.VeryClean } 36 | 37 | type TypeWithSystemProps = 38 | { [] PartitionKey : string; 39 | [] RowKey : string; 40 | Timestamp : DateTimeOffset } 41 | 42 | type TypeWithSystemPropsAttributes = 43 | { [] PartitionKey : string; 44 | [] RowKey : string; 45 | [] Modified: DateTimeOffset option; 46 | [] tag : string option } 47 | 48 | type GameTableEntity() = 49 | inherit TableEntity() 50 | member val Name : string = null with get,set 51 | member val Platform : string = null with get,set 52 | member val Developer : string = null with get,set 53 | member val HasMultiplayer : bool = false with get,set 54 | 55 | type NonTableEntityClass() = 56 | member val Name : string = null with get,set 57 | 58 | type internal InternalRecord = 59 | { [] MuchInternal: string 60 | [] VeryWow: string } 61 | 62 | type UnionProperty = A | B | C 63 | type TypeWithUnionProperty = 64 | { [] PartitionKey : string; 65 | [] RowKey : string; 66 | UnionProp: UnionProperty; } 67 | 68 | type EnumProperty = A = 1 | B = 2 | C = 3 69 | type TypeWithEnumProperty = 70 | { [] PartitionKey : string; 71 | [] RowKey : string; 72 | EnumProp: EnumProperty; } 73 | 74 | type UnionWithFieldProperty = 75 | | X of string 76 | | Y 77 | type TypeWithUnionWithFieldProperty = 78 | { [] PartitionKey : string; 79 | [] RowKey : string; 80 | UnionWithFieldProp: UnionWithFieldProperty; } 81 | 82 | type GameTempTable (tableClient) = 83 | inherit Storage.TempTable (tableClient) 84 | 85 | member this.InGameTable e = inTable tableClient this.Name e 86 | member this.InGameTableAsync e = inTableAsync tableClient this.Name e 87 | member this.InGameTableAsBatch e = inTableAsBatch tableClient this.Name e 88 | member this.InGameTableAsBatchAsync e = inTableAsBatchAsync tableClient this.Name e 89 | member this.VerifyGame game = 90 | let result = TableOperation.Retrieve(game.Developer, game.Name) |> this.Table.ExecuteAsync |> Async.AwaitTask |> Async.RunSynchronously 91 | let entity = result.Result :?> DynamicTableEntity 92 | 93 | entity.PartitionKey |> Expect.equal "PartitionKey is developer" game.Developer 94 | entity.RowKey |> Expect.equal "RowKey is name" game.Name 95 | entity.Properties.["Name"].StringValue |> Expect.equal "Name property is correct" game.Name 96 | entity.Properties.["Platform"].StringValue |> Expect.equal "Platform property is correct" game.Platform 97 | entity.Properties.["Developer"].StringValue |> Expect.equal "Developer property is correct" game.Developer 98 | entity.Properties.["HasMultiplayer"].BooleanValue |> Expect.equal "HasMultiplayer property is correct" (Nullable game.HasMultiplayer) 99 | member this.VerifyGameTableEntity (game : GameTableEntity) = 100 | let result = TableOperation.Retrieve(game.Developer, game.Name) |> this.Table.ExecuteAsync |> Async.AwaitTask |> Async.RunSynchronously 101 | let entity = result.Result :?> DynamicTableEntity 102 | 103 | entity.PartitionKey |> Expect.equal "PartitionKey is developer" game.Developer 104 | entity.RowKey |> Expect.equal "RowKey is name" game.Name 105 | entity.Properties.["Name"].StringValue |> Expect.equal "Name property is correct" game.Name 106 | entity.Properties.["Platform"].StringValue |> Expect.equal "Platform property is correct" game.Platform 107 | entity.Properties.["Developer"].StringValue |> Expect.equal "Developer property is correct" game.Developer 108 | entity.Properties.["HasMultiplayer"].BooleanValue |> Expect.equal "HasMultiplayer property is correct" (Nullable game.HasMultiplayer) 109 | 110 | member this.VerifyGameDynamicTableEntity (game : DynamicTableEntity) = 111 | let result = TableOperation.Retrieve(game.PartitionKey, game.RowKey) |> this.Table.ExecuteAsync |> Async.AwaitTask |> Async.RunSynchronously 112 | let entity = result.Result :?> DynamicTableEntity 113 | 114 | entity.PartitionKey |> Expect.equal "PartitionKey is developer" game.PartitionKey 115 | entity.RowKey |> Expect.equal "RowKey is name" game.RowKey 116 | entity.Properties.["Name"] |> Expect.equal "Name property is correct" game.Properties.["Name"] 117 | entity.Properties.["Platform"] |> Expect.equal "Platform property is correct" game.Properties.["Platform"] 118 | entity.Properties.["Developer"] |> Expect.equal "Developer property is correct" game.Properties.["Developer"] 119 | entity.Properties.["HasMultiplayer"] |> Expect.equal "HasMultiplayer property is correct" game.Properties.["HasMultiplayer"] 120 | 121 | let tests connectionString = 122 | EntityIdentiferReader.GetIdentifier <- getPureIdentifier 123 | 124 | let account = CloudStorageAccount.Parse connectionString 125 | let tableClient = account.CreateCloudTableClient() 126 | 127 | let gameTestCase name testFn = 128 | testCase name (fun () -> 129 | use tempTable = new GameTempTable (tableClient) 130 | do testFn tempTable 131 | ) 132 | 133 | let gameTestCaseAsync name testFn = 134 | testCaseAsync name (async { 135 | use tempTable = new GameTempTable (tableClient) 136 | do! testFn tempTable 137 | }) 138 | 139 | testList "Data Modification Tests" [ 140 | gameTestCase "can insert a new record" <| fun ts -> 141 | let game = 142 | { Name = "Halo 4" 143 | Platform = "Xbox 360" 144 | Developer = "343 Industries" 145 | HasMultiplayer = true } 146 | 147 | let result = game |> Insert |> ts.InGameTable 148 | 149 | result.HttpStatusCode |> Expect.equal "Status code equals 204" 204 150 | ts.VerifyGame game 151 | 152 | gameTestCaseAsync "can insert a new record asynchronously" <| fun ts -> async { 153 | let game = 154 | { Name = "Halo 4" 155 | Platform = "Xbox 360" 156 | Developer = "343 Industries" 157 | HasMultiplayer = true } 158 | 159 | let! result = game |> Insert |> ts.InGameTableAsync 160 | 161 | result.HttpStatusCode |> Expect.equal "Status code equals 204" 204 162 | ts.VerifyGame game 163 | } 164 | 165 | gameTestCase "fails when inserting a record that already exists" <| fun ts -> 166 | let game = 167 | { Name = "Halo 4" 168 | Platform = "Xbox 360" 169 | Developer = "343 Industries" 170 | HasMultiplayer = true } 171 | 172 | game |> Insert |> ts.InGameTable |> ignore 173 | (fun () -> game |> Insert |> ts.InGameTable |> ignore) 174 | |> Expect.throwsT "Throws StorageException" 175 | 176 | gameTestCase "can insert or replace a record" <| fun ts -> 177 | let game = 178 | { Name = "Halo 4" 179 | Platform = "Xbox 360" 180 | Developer = "343 Industries" 181 | HasMultiplayer = true } 182 | 183 | game |> Insert |> ts.InGameTable |> ignore 184 | 185 | let gameChanged = 186 | { game with 187 | Platform = "PC" 188 | HasMultiplayer = false } 189 | 190 | let result = gameChanged |> InsertOrReplace |> ts.InGameTable 191 | result.HttpStatusCode |> Expect.equal "Status code equals 204" 204 192 | ts.VerifyGame gameChanged 193 | 194 | gameTestCase "can force replace a record" <| fun ts -> 195 | let game = 196 | { Name = "Halo 4" 197 | Platform = "Xbox 360" 198 | Developer = "343 Industries" 199 | HasMultiplayer = true } 200 | 201 | game |> Insert |> ts.InGameTable |> ignore 202 | 203 | let gameChanged = 204 | { game with 205 | Platform = "PC" 206 | HasMultiplayer = false } 207 | 208 | let result = gameChanged |> ForceReplace |> ts.InGameTable 209 | result.HttpStatusCode |> Expect.equal "Status code equals 204" 204 210 | ts.VerifyGame gameChanged 211 | 212 | gameTestCase "can replace a record" <| fun ts -> 213 | let game = 214 | { Name = "Halo 4" 215 | Platform = "Xbox 360" 216 | Developer = "343 Industries" 217 | HasMultiplayer = true } 218 | 219 | let originalResult = game |> Insert |> ts.InGameTable 220 | 221 | let gameChanged = 222 | { game with 223 | Platform = "PC" 224 | HasMultiplayer = false } 225 | 226 | let result = (gameChanged, originalResult.Etag) |> Replace |> ts.InGameTable 227 | result.HttpStatusCode |> Expect.equal "Status code equals 204" 204 228 | ts.VerifyGame gameChanged 229 | 230 | gameTestCase "fails when replacing but etag is out of date" <| fun ts -> 231 | let game = 232 | { Name = "Halo 4" 233 | Platform = "Xbox 360" 234 | Developer = "343 Industries" 235 | HasMultiplayer = true } 236 | 237 | game |> Insert |> ts.InGameTable |> ignore 238 | 239 | let gameChanged = 240 | { game with 241 | Platform = "PC" 242 | HasMultiplayer = false } 243 | 244 | (fun () -> (gameChanged, "bogus") |> Replace |> ts.InGameTable |> ignore) 245 | |> Expect.throwsT "Throws StorageException" 246 | 247 | gameTestCase "can insert or merge a record" <| fun ts -> 248 | let game = 249 | { Name = "Halo 4" 250 | Platform = "Xbox 360" 251 | Developer = "343 Industries" 252 | HasMultiplayer = true } 253 | 254 | game |> Insert |> ts.InGameTable |> ignore 255 | 256 | let gameSummary = 257 | { GameSummary.Name = game.Name 258 | Platform = "PC" 259 | Developer = game.Developer } 260 | 261 | let result = gameSummary |> InsertOrMerge |> ts.InGameTable 262 | result.HttpStatusCode |> Expect.equal "Status code equals 204" 204 263 | ts.VerifyGame { game with Platform = "PC" } 264 | 265 | gameTestCase "can force merge a record" <| fun ts -> 266 | let game = 267 | { Name = "Halo 4" 268 | Platform = "Xbox 360" 269 | Developer = "343 Industries" 270 | HasMultiplayer = true } 271 | 272 | game |> Insert |> ts.InGameTable |> ignore 273 | 274 | let gameSummary = 275 | { GameSummary.Name = game.Name 276 | Platform = "PC" 277 | Developer = game.Developer } 278 | 279 | let result = gameSummary |> ForceMerge |> ts.InGameTable 280 | result.HttpStatusCode |> Expect.equal "Status code equals 204" 204 281 | ts.VerifyGame { game with Platform = "PC" } 282 | 283 | gameTestCase "can merge a record" <| fun ts -> 284 | let game = 285 | { Name = "Halo 4" 286 | Platform = "Xbox 360" 287 | Developer = "343 Industries" 288 | HasMultiplayer = true } 289 | 290 | let originalResult = game |> Insert |> ts.InGameTable 291 | 292 | let gameSummary = 293 | { GameSummary.Name = game.Name 294 | Platform = "PC" 295 | Developer = game.Developer } 296 | 297 | let result = (gameSummary, originalResult.Etag) |> Merge |> ts.InGameTable 298 | result.HttpStatusCode |> Expect.equal "Status code equals 204" 204 299 | ts.VerifyGame { game with Platform = "PC" } 300 | 301 | gameTestCase "fails when merging but etag is out of date" <| fun ts -> 302 | let game = 303 | { Name = "Halo 4" 304 | Platform = "Xbox 360" 305 | Developer = "343 Industries" 306 | HasMultiplayer = true } 307 | 308 | game |> Insert |> ts.InGameTable |> ignore 309 | 310 | let gameSummary = 311 | { GameSummary.Name = game.Name 312 | Platform = "PC" 313 | Developer = game.Developer } 314 | 315 | (fun () -> (gameSummary, "bogus") |> Merge |> ts.InGameTable |> ignore) 316 | |> Expect.throwsT "Throws StorageException" 317 | 318 | gameTestCase "works when inserting a type that has properties that are system properties" <| fun ts -> 319 | //Note that Timestamp will be ignored by table storage 320 | let record = { PartitionKey = "TestPK"; RowKey = "TestRK"; Timestamp = DateTimeOffset.Now } 321 | 322 | let result = record |> Insert |> ts.InGameTable 323 | 324 | result.HttpStatusCode |> Expect.equal "Status code equals 204" 204 325 | 326 | gameTestCase "can delete a record" <| fun ts -> 327 | let game = 328 | { Name = "Halo 4" 329 | Platform = "Xbox 360" 330 | Developer = "343 Industries" 331 | HasMultiplayer = true } 332 | 333 | let originalResult = game |> Insert |> ts.InGameTable 334 | 335 | let result = (game, originalResult.Etag) |> Delete |> ts.InGameTable 336 | result.HttpStatusCode |> Expect.equal "Status code equals 204" 204 337 | 338 | Query.all 339 | |> Query.where <@ fun _ s -> s.PartitionKey = game.Developer && s.RowKey = game.Name @> 340 | |> fromTable tableClient ts.Name 341 | |> Expect.isEmpty "Deleted row should not be found" 342 | 343 | gameTestCase "can force delete a record" <| fun ts -> 344 | let game = 345 | { Name = "Halo 4" 346 | Platform = "Xbox 360" 347 | Developer = "343 Industries" 348 | HasMultiplayer = true } 349 | 350 | game |> Insert |> ts.InGameTable |> ignore 351 | 352 | let result = game |> ForceDelete |> ts.InGameTable 353 | result.HttpStatusCode |> Expect.equal "Status code equals 204" 204 354 | 355 | Query.all 356 | |> Query.where <@ fun _ s -> s.PartitionKey = game.Developer && s.RowKey = game.Name @> 357 | |> fromTable tableClient ts.Name 358 | |> Expect.isEmpty "Deleted row should not be found" 359 | 360 | gameTestCase "can delete a record using only PK and RK" <| fun ts -> 361 | let game = 362 | { Name = "Halo 4" 363 | Platform = "Xbox 360" 364 | Developer = "343 Industries" 365 | HasMultiplayer = true } 366 | 367 | game |> Insert |> ts.InGameTable |> ignore 368 | 369 | let result = 370 | { EntityIdentifier.PartitionKey = game.Developer; RowKey = game.Name } 371 | |> ForceDelete 372 | |> ts.InGameTable 373 | 374 | result.HttpStatusCode |> Expect.equal "Status code equals 204" 204 375 | 376 | Query.all 377 | |> Query.where <@ fun _ s -> s.PartitionKey = game.Developer && s.RowKey = game.Name @> 378 | |> fromTable tableClient ts.Name 379 | |> Expect.isEmpty "Deleted row should not be found" 380 | 381 | gameTestCase "can insert a new table entity" <| fun ts -> 382 | let game = 383 | GameTableEntity (Name = "Halo 4", 384 | Platform = "Xbox 360", 385 | Developer = "343 Industries", 386 | HasMultiplayer = true, 387 | PartitionKey = "343 Industries", 388 | RowKey = "Halo 4") 389 | 390 | let result = game |> Insert |> ts.InGameTable 391 | 392 | result.HttpStatusCode |> Expect.equal "Status code equals 204" 204 393 | ts.VerifyGameTableEntity game 394 | 395 | gameTestCase "can replace a table entity" <| fun ts -> 396 | let game = 397 | GameTableEntity (Name = "Halo 4", 398 | Platform = "Xbox 360", 399 | Developer = "343 Industries", 400 | HasMultiplayer = true, 401 | PartitionKey = "343 Industries", 402 | RowKey = "Halo 4") 403 | 404 | let originalResult = game |> Insert |> ts.InGameTable 405 | 406 | do game.Platform <- "PC" 407 | do game.HasMultiplayer <- false 408 | do game.ETag <- null //This is to prove that replace will respect the etag you pass to it (below) 409 | 410 | let result = (game, originalResult.Etag) |> Replace |> ts.InGameTable 411 | result.HttpStatusCode |> Expect.equal "Status code equals 204" 204 412 | ts.VerifyGameTableEntity game 413 | 414 | gameTestCase "inserting with types that aren't records or implement ITableEntity fails" <| fun ts -> 415 | (fun () -> NonTableEntityClass() |> Insert |> ts.InGameTable |> ignore) 416 | |> Expect.throwsT "Throws Exception" 417 | 418 | gameTestCase "inserting many entities using autobatching works" <| fun ts -> 419 | let games = 420 | [seq { for i in 1 .. 120 -> 421 | { Developer = "Valve"; Name = sprintf "Portal %i" i; Platform = "PC"; HasMultiplayer = true } }; 422 | seq { for i in 1 .. 150 -> 423 | { Developer = "343 Industries"; Name = sprintf "Halo %i" i; Platform = "PC"; HasMultiplayer = true } }] 424 | |> Seq.concat 425 | |> Seq.toList 426 | 427 | let batches = games |> Seq.map Insert |> autobatch 428 | 429 | batches.Length |> Expect.equal "Correct number of batches" 4 430 | batches |> Seq.head |> List.length |> Expect.equal "First batch size correct" MaxBatchSize 431 | batches |> Seq.skip 1 |> Seq.head |> List.length |> Expect.equal "Second batch size correct" 20 432 | batches |> Seq.skip 2 |> Seq.head |> List.length |> Expect.equal "Third batch size correct" MaxBatchSize 433 | batches |> Seq.skip 3 |> Seq.head |> List.length |> Expect.equal "Fourth batch size correct" 50 434 | 435 | let results = batches |> List.map ts.InGameTableAsBatch 436 | 437 | results |> Seq.concat |> Seq.map (fun r -> r.HttpStatusCode) |> Expect.allEqual "All status codes equal 204" 204 438 | let readGames = 439 | Query.all 440 | |> fromTable tableClient ts.Name 441 | |> Seq.map fst 442 | |> Seq.toList 443 | readGames |> Expect.containsAll "All games were inserted" games 444 | readGames.Length |> Expect.equal "Number of games read equals inserted games" games.Length 445 | 446 | gameTestCaseAsync "inserting many entities asynchronously using autobatching works" <| fun ts -> async { 447 | let games = 448 | [seq { for i in 1 .. 120 -> 449 | { Developer = "Valve"; Name = sprintf "Portal %i" i; Platform = "PC"; HasMultiplayer = true } }; 450 | seq { for i in 1 .. 150 -> 451 | { Developer = "343 Industries"; Name = sprintf "Halo %i" i; Platform = "PC"; HasMultiplayer = true } }] 452 | |> Seq.concat 453 | |> Seq.toList 454 | 455 | let batches = games |> Seq.map Insert |> autobatch 456 | 457 | batches.Length |> Expect.equal "Correct number of batches" 4 458 | batches |> Seq.head |> List.length |> Expect.equal "First batch size correct" MaxBatchSize 459 | batches |> Seq.skip 1 |> Seq.head |> List.length |> Expect.equal "Second batch size correct" 20 460 | batches |> Seq.skip 2 |> Seq.head |> List.length |> Expect.equal "Third batch size correct" MaxBatchSize 461 | batches |> Seq.skip 3 |> Seq.head |> List.length |> Expect.equal "Fourth batch size correct" 50 462 | 463 | let! results = 464 | batches 465 | |> List.map ts.InGameTableAsBatchAsync 466 | |> Async.Parallel 467 | 468 | results |> Seq.concat |> Seq.map (fun r -> r.HttpStatusCode) |> Expect.allEqual "All status codes equal 204" 204 469 | let readGames = 470 | Query.all 471 | |> fromTable tableClient ts.Name 472 | |> Seq.map fst 473 | |> Seq.toList 474 | readGames |> Expect.containsAll "All games were inserted" games 475 | readGames.Length |> Expect.equal "Number of games read equals inserted games" games.Length 476 | } 477 | 478 | testCase "inserting two of the same entity with autobatching fails" <| fun () -> 479 | let games = [ 480 | { Developer = "Valve"; Name = sprintf "Portal"; Platform = "PC"; HasMultiplayer = true } 481 | { Developer = "Valve"; Name = sprintf "Portal"; Platform = "PC"; HasMultiplayer = false } 482 | ] 483 | 484 | (fun () -> games |> Seq.map Insert |> autobatch |> ignore) 485 | |> Expect.throwsT "Throws Exception" 486 | 487 | gameTestCase "performing an operation on a type that uses a custom EntityIdentiferReader function works" <| fun ts -> 488 | let doge = 489 | { SuchPure = "MuchWin" 490 | VeryClean = "SoShiny" 491 | Wow = true } 492 | 493 | let result = doge |> Insert |> ts.InGameTable 494 | 495 | result.HttpStatusCode |> Expect.equal "Status code equals 204" 204 496 | 497 | gameTestCase "inserting a record with option type fields works" <| fun ts -> 498 | let game = 499 | { GameWithOptions.Name = "Halo 4" 500 | Platform = None 501 | Developer = "343 Industries" 502 | HasMultiplayer = None } 503 | 504 | let result = game |> Insert |> ts.InGameTable 505 | 506 | result.HttpStatusCode |> Expect.equal "Status code equals 204" 204 507 | 508 | gameTestCase "inserting an internal record type works" <| fun ts -> 509 | let internalType = 510 | { MuchInternal = "SuchPrivates" 511 | VeryWow = "Amaze" } 512 | 513 | let result = internalType |> Insert |> ts.InGameTable 514 | 515 | result.HttpStatusCode |> Expect.equal "Status code equals 204" 204 516 | 517 | gameTestCase "inserting an record type with system attributes and no values works" <| fun ts -> 518 | let data = 519 | { TypeWithSystemPropsAttributes.PartitionKey = "Some Nice Partition Key" 520 | RowKey = "Yup this is a row key" 521 | Modified = None 522 | tag = None } 523 | let result = data |> Insert |> ts.InGameTable 524 | 525 | result.HttpStatusCode |> Expect.equal "Status code equals 204" 204 526 | 527 | gameTestCase "inserting an record type with system attributes and values works" <| fun ts -> 528 | let data = 529 | { TypeWithSystemPropsAttributes.PartitionKey = "Some Nice Partition Key" 530 | RowKey = "Yup this is a row key" 531 | Modified = Some DateTimeOffset.Now 532 | tag = Some "Some tag" } 533 | let result = data |> Insert |> ts.InGameTable 534 | 535 | result.HttpStatusCode |> Expect.equal "Status code equals 204" 204 536 | 537 | testCase "inserting a record type with union property works" <| fun () -> 538 | use ts = new Storage.TempTable (tableClient) 539 | let data = { PartitionKey = "PK"; RowKey = "RK"; UnionProp = A } 540 | 541 | let result = data |> Insert |> inTable tableClient ts.Name 542 | 543 | result.HttpStatusCode |> Expect.equal "Status code equals 204" 204 544 | 545 | testCase "inserting a record type with enum property works" <| fun () -> 546 | use ts = new Storage.TempTable (tableClient) 547 | let data = { PartitionKey = "PK"; RowKey = "RK"; EnumProp = EnumProperty.A } 548 | 549 | let result = data |> Insert |> inTable tableClient ts.Name 550 | 551 | result.HttpStatusCode |> Expect.equal "Status code equals 204" 204 552 | 553 | testCase "inserting a record type with union that has fields property fails" <| fun () -> 554 | use ts = new Storage.TempTable (tableClient) 555 | let data = { PartitionKey = "PK"; RowKey = "RK"; UnionWithFieldProp = X("x") } 556 | 557 | (fun () -> data |> Insert |> inTable tableClient ts.Name |> ignore) 558 | |> Expect.throws "Union with field" 559 | 560 | gameTestCase "can insert a new record using DynamicTableEntity" <| fun ts -> 561 | let properties = 562 | dict [ 563 | "Name", EntityProperty.GeneratePropertyForString "Halo 4" 564 | "Platform", EntityProperty.GeneratePropertyForString "Xbox 360" 565 | "Developer", EntityProperty.GeneratePropertyForString "343 Industries" 566 | "HasMultiplayer", EntityProperty.GeneratePropertyForBool true 567 | ] 568 | let game = DynamicTableEntity("343 Industries", "Halo 4", null, properties) 569 | 570 | let result = game |> Insert |> ts.InGameTable 571 | 572 | result.HttpStatusCode |> Expect.equal "Status code equals 204" 204 573 | ts.VerifyGameDynamicTableEntity game 574 | ] 575 | -------------------------------------------------------------------------------- /test/FSharp.Azure.Storage.Tests/Table/DataQueryTests.fs: -------------------------------------------------------------------------------- 1 | module FSharp.Azure.Storage.Tests.Table.DataQueryTests 2 | 3 | open System 4 | open System.Collections.Generic 5 | open FSharp.Azure.Storage.Table 6 | open Expecto 7 | open Expecto.Flip 8 | open FSharp.Control 9 | open FSharp.Azure.Storage.Tests 10 | open Microsoft.Azure.Cosmos.Table 11 | 12 | type GameWithDateTime = 13 | { [] Name: string 14 | DevelopmentDate: DateTime 15 | DevelopmentDateAsOffset: DateTimeOffset 16 | [] Developer : string } 17 | 18 | type internal InternalGame = 19 | { [] Name: string 20 | [] Developer: string } 21 | 22 | type GameWithOptions = 23 | { Name: string 24 | Platform: string 25 | Developer : string 26 | HasMultiplayer: bool option 27 | Notes : string option } 28 | 29 | interface IEntityIdentifiable with 30 | member g.GetIdentifier() = 31 | { PartitionKey = g.Developer; RowKey = g.Name + "-" + g.Platform } 32 | 33 | type GameWithUri = 34 | { Name: string 35 | Developer : string 36 | HasMultiplayer: bool 37 | Website : Uri } 38 | 39 | interface IEntityIdentifiable with 40 | member g.GetIdentifier() = 41 | { PartitionKey = g.Developer; RowKey = g.Name} 42 | type GameWithUriOptions = 43 | { Name: string 44 | Developer : string 45 | Website: Uri option} 46 | 47 | interface IEntityIdentifiable with 48 | member g.GetIdentifier() = 49 | { PartitionKey = g.Developer; RowKey = g.Name } 50 | type Game = 51 | { Name: string 52 | Platform: string 53 | Developer : string 54 | HasMultiplayer: bool } 55 | 56 | interface IEntityIdentifiable with 57 | member g.GetIdentifier() = 58 | { PartitionKey = g.Developer; RowKey = g.Name + "-" + g.Platform } 59 | 60 | type TypeWithSystemProps = 61 | { [] PartitionKey : string; 62 | [] RowKey : string; 63 | Timestamp : DateTimeOffset } 64 | 65 | 66 | type TypeWithSystemPropsAttributes = 67 | { [] PartitionKey : string; 68 | [] RowKey : string; 69 | [] Modified: DateTimeOffset option; 70 | [] tag : string option } 71 | 72 | type Simple = { [] PK : string; [] RK : string } 73 | 74 | type NonTableEntityClass() = 75 | member val Name : string = null with get,set 76 | 77 | type GameTableEntity() = 78 | inherit TableEntity() 79 | member val Name : string = null with get,set 80 | member val Platform : string = null with get,set 81 | member val Developer : string = null with get,set 82 | member val HasMultiplayer : bool = false with get,set 83 | 84 | override this.Equals other = 85 | match other with 86 | | :? Game as game -> 87 | this.Name = game.Name && this.Platform = game.Platform && 88 | this.Developer = game.Developer && this.HasMultiplayer = game.HasMultiplayer 89 | | :? GameTableEntity as game -> 90 | this.Name = game.Name && this.Platform = game.Platform && 91 | this.Developer = game.Developer && this.HasMultiplayer = game.HasMultiplayer && 92 | this.PartitionKey = game.PartitionKey && this.RowKey = game.RowKey && 93 | this.Timestamp = game.Timestamp && this.ETag = game.ETag 94 | | _ -> false 95 | 96 | override this.GetHashCode() = 97 | [box this.Name; box this.Platform; box this.Developer 98 | box this.HasMultiplayer; box this.PartitionKey; 99 | box this.RowKey; box this.Timestamp; box this.HasMultiplayer ] 100 | |> Seq.choose (fun o -> match o with | null -> None | o -> Some (o.GetHashCode())) 101 | |> Seq.reduce (^^^) 102 | 103 | type GameTableEntityWithIgnoredProperty() = 104 | inherit TableEntity() 105 | member val Name : string = null with get,set 106 | member val Platform : string = null with get,set 107 | member val Developer : string = null with get,set 108 | [] 109 | member val HasMultiplayer : bool = false with get,set 110 | 111 | override this.Equals other = 112 | match other with 113 | | :? Game as game -> 114 | this.Name = game.Name && this.Platform = game.Platform && 115 | this.Developer = game.Developer //Don't compare HasMultiplayer because we're expecting it to not be read back from table storage 116 | | :? GameTableEntity as game -> 117 | this.Name = game.Name && this.Platform = game.Platform && 118 | this.Developer = game.Developer && this.HasMultiplayer = game.HasMultiplayer && 119 | this.PartitionKey = game.PartitionKey && this.RowKey = game.RowKey && 120 | this.Timestamp = game.Timestamp && this.ETag = game.ETag 121 | | _ -> false 122 | 123 | override this.GetHashCode() = 124 | [box this.Name; box this.Platform; box this.Developer 125 | box this.HasMultiplayer; box this.PartitionKey; 126 | box this.RowKey; box this.Timestamp; box this.HasMultiplayer ] 127 | |> Seq.choose (fun o -> match o with | null -> None | o -> Some (o.GetHashCode())) 128 | |> Seq.reduce (^^^) 129 | 130 | type UnionProperty = A | B | C 131 | type TypeWithUnionProperty = 132 | { [] PartitionKey : string; 133 | [] RowKey : string; 134 | UnionProp: UnionProperty; } 135 | 136 | type EnumProperty = A = 1 | B = 2 | C = 3 137 | type TypeWithEnumProperty = 138 | { [] PartitionKey : string; 139 | [] RowKey : string; 140 | EnumProp: EnumProperty; } 141 | 142 | type UnionWithFieldProperty = 143 | | X of string 144 | | Y 145 | type TypeWithUnionWithFieldProperty = 146 | { [] PartitionKey : string; 147 | [] RowKey : string; 148 | UnionWithFieldProp: UnionWithFieldProperty; } 149 | 150 | 151 | let private processInParallel tableClient tableName operation = 152 | Seq.map operation 153 | >> autobatch 154 | >> Seq.map (inTableAsBatchAsync tableClient tableName) 155 | >> Async.ParallelByDegree 4 156 | >> Async.RunSynchronously 157 | >> Seq.concat 158 | 159 | let private insertInParallelAndCheckSuccess tableClient tableName = 160 | processInParallel tableClient tableName Insert 161 | >> Seq.map (fun r -> r.HttpStatusCode) 162 | >> Expect.allEqual "All inserts have status code 204" 204 163 | 164 | let private verifyMetadata metadata = 165 | metadata |> Seq.iter (fun (_, m) -> 166 | m.Etag |> Expect.isNotEmpty "Etag should not be null or empty" 167 | m.Timestamp |> Expect.notEqual "Timestamp should not be default value" (DateTimeOffset()) 168 | ) 169 | 170 | let verifyRecords expected actual = 171 | actual |> Array.length |> Expect.equal "Record count should match" (expected |> Array.length) 172 | let actualRecords = actual |> Seq.map fst 173 | actualRecords |> Expect.all "All records match expected records" (fun a -> expected |> Seq.exists (fun e -> a.Equals(e))) 174 | 175 | type GameTempTable (tableClient) = 176 | inherit Storage.TempTable (tableClient) 177 | 178 | static let data = [ 179 | { Developer = "343 Industries"; Name = "Halo 4"; Platform = "Xbox 360"; HasMultiplayer = true } 180 | { Developer = "Bungie"; Name = "Halo 3"; Platform = "Xbox 360"; HasMultiplayer = true } 181 | { Developer = "Bungie"; Name = "Halo 2"; Platform = "Xbox 360"; HasMultiplayer = true } 182 | { Developer = "Bungie"; Name = "Halo 2"; Platform = "PC"; HasMultiplayer = true } 183 | { Developer = "Bungie"; Name = "Halo 1"; Platform = "Xbox 360"; HasMultiplayer = true } 184 | { Developer = "Bungie"; Name = "Halo 1"; Platform = "PC"; HasMultiplayer = true } 185 | { Developer = "Valve"; Name = "Half-Life 2"; Platform = "PC"; HasMultiplayer = true } 186 | { Developer = "Valve"; Name = "Portal"; Platform = "PC"; HasMultiplayer = false } 187 | { Developer = "Valve"; Name = "Portal 2"; Platform = "PC"; HasMultiplayer = false } 188 | { Developer = "Crystal Dynamics"; Name = "Tomb Raider"; Platform = "PC"; HasMultiplayer = true } 189 | { Developer = "Crystal Dynamics"; Name = "Tomb Raider"; Platform = "Xbox 360"; HasMultiplayer = true } 190 | { Developer = "Crystal Dynamics"; Name = "Tomb Raider"; Platform = "PS3"; HasMultiplayer = true } 191 | { Developer = "Crystal Dynamics"; Name = "Tomb Raider"; Platform = "Xbox One"; HasMultiplayer = true } 192 | { Developer = "Crystal Dynamics"; Name = "Tomb Raider"; Platform = "PS4"; HasMultiplayer = true } 193 | ] 194 | 195 | member this.FromGameTable q = fromTable tableClient this.Name q 196 | member this.FromGameTableAsync q = fromTableAsync tableClient this.Name q 197 | member this.InsertTestData () = do data |> insertInParallelAndCheckSuccess tableClient this.Name 198 | 199 | type SimpleTempTable (tableClient) = 200 | inherit Storage.TempTable (tableClient) 201 | 202 | member this.InsertTestData () = 203 | //Storage emulator segments the data after 1000 rows, so generate 1200 rows 204 | let rows = 205 | seq { 206 | for partition in 1..12 do 207 | for row in 1..100 do 208 | yield { PK = "PK" + partition.ToString(); RK = "RK" + row.ToString() } 209 | } 210 | |> Array.ofSeq 211 | do rows |> insertInParallelAndCheckSuccess tableClient this.Name 212 | rows 213 | 214 | let tests connectionString = 215 | let account = CloudStorageAccount.Parse connectionString 216 | let tableClient = account.CreateCloudTableClient() 217 | 218 | let gameTestCase name testFn = 219 | testCase name (fun () -> 220 | use tempTable = new GameTempTable (tableClient) 221 | do tempTable.InsertTestData() 222 | do testFn tempTable 223 | ) 224 | 225 | let gameTestCaseAsync name testFn = 226 | testCaseAsync name (async { 227 | use tempTable = new GameTempTable (tableClient) 228 | do tempTable.InsertTestData() 229 | do! testFn tempTable 230 | }) 231 | 232 | testList "Data Query Tests" [ 233 | gameTestCase "query by specific instance" <| fun ts -> 234 | let halo4 = 235 | Query.all 236 | |> Query.where <@ fun _ s -> s.PartitionKey = "343 Industries" && s.RowKey = "Halo 4-Xbox 360" @> 237 | |> ts.FromGameTable 238 | |> Seq.toArray 239 | 240 | halo4 |> verifyRecords [| 241 | { Developer = "343 Industries"; Name = "Halo 4"; Platform = "Xbox 360"; HasMultiplayer = true } 242 | |] 243 | 244 | halo4 |> verifyMetadata 245 | 246 | gameTestCase "query by partition key" <| fun ts -> 247 | let valveGames = 248 | Query.all 249 | |> Query.where <@ fun _ s -> s.PartitionKey = "Valve" @> 250 | |> ts.FromGameTable 251 | |> Seq.toArray 252 | 253 | valveGames |> verifyRecords [| 254 | { Developer = "Valve"; Name = "Half-Life 2"; Platform = "PC"; HasMultiplayer = true } 255 | { Developer = "Valve"; Name = "Portal"; Platform = "PC"; HasMultiplayer = false } 256 | { Developer = "Valve"; Name = "Portal 2"; Platform = "PC"; HasMultiplayer = false } 257 | |] 258 | 259 | valveGames |> verifyMetadata 260 | 261 | 262 | gameTestCase "query by properties" <| fun ts -> 263 | let valveGames = 264 | Query.all 265 | |> Query.where <@ fun g _ -> (g.Platform = "Xbox 360" || g.Platform = "PC") && not (g.Developer = "Bungie") @> 266 | |> ts.FromGameTable 267 | |> Seq.toArray 268 | 269 | valveGames |> verifyRecords [| 270 | { Developer = "343 Industries"; Name = "Halo 4"; Platform = "Xbox 360"; HasMultiplayer = true } 271 | { Developer = "Valve"; Name = "Half-Life 2"; Platform = "PC"; HasMultiplayer = true } 272 | { Developer = "Valve"; Name = "Portal"; Platform = "PC"; HasMultiplayer = false } 273 | { Developer = "Valve"; Name = "Portal 2"; Platform = "PC"; HasMultiplayer = false } 274 | { Developer = "Crystal Dynamics"; Name = "Tomb Raider"; Platform = "PC"; HasMultiplayer = true } 275 | { Developer = "Crystal Dynamics"; Name = "Tomb Raider"; Platform = "Xbox 360"; HasMultiplayer = true } 276 | |] 277 | 278 | valveGames |> verifyMetadata 279 | 280 | gameTestCase "query with take" <| fun ts -> 281 | let valveGames = 282 | Query.all 283 | |> Query.where <@ fun _ s -> s.PartitionKey = "Valve" @> 284 | |> Query.take 2 285 | |> ts.FromGameTable 286 | |> Seq.toArray 287 | 288 | valveGames |> verifyRecords [| 289 | { Developer = "Valve"; Name = "Half-Life 2"; Platform = "PC"; HasMultiplayer = true } 290 | { Developer = "Valve"; Name = "Portal 2"; Platform = "PC"; HasMultiplayer = false } 291 | |] 292 | 293 | valveGames |> verifyMetadata 294 | 295 | gameTestCaseAsync "async query" <| fun ts -> async { 296 | let! valveGamesSeq = 297 | Query.all 298 | |> Query.where <@ fun _ s -> s.PartitionKey = "Valve" @> 299 | |> ts.FromGameTableAsync 300 | 301 | let valveGames = valveGamesSeq |> Seq.toArray 302 | 303 | valveGames |> verifyRecords [| 304 | { Developer = "Valve"; Name = "Half-Life 2"; Platform = "PC"; HasMultiplayer = true } 305 | { Developer = "Valve"; Name = "Portal"; Platform = "PC"; HasMultiplayer = false } 306 | { Developer = "Valve"; Name = "Portal 2"; Platform = "PC"; HasMultiplayer = false } 307 | |] 308 | 309 | valveGames |> verifyMetadata 310 | } 311 | 312 | testCase "segmented query" <| fun () -> 313 | use ts = new SimpleTempTable (tableClient) 314 | let rows = ts.InsertTestData() 315 | let (simples1, segmentToken1) = 316 | Query.all 317 | |> fromTableSegmented tableClient ts.Name None 318 | 319 | segmentToken1 |> Expect.isSome "SegmentToken1 should be Some" 320 | 321 | let (simples2, segmentToken2) = 322 | Query.all 323 | |> fromTableSegmented tableClient ts.Name segmentToken1 324 | 325 | segmentToken2 |> Expect.isNone "SegmentToken1 should be None" 326 | 327 | let allSimples = [simples1; simples2] |> Seq.concat |> Seq.toArray 328 | allSimples |> verifyRecords rows 329 | allSimples |> verifyMetadata 330 | 331 | testCaseAsync "async segmented query" <| async { 332 | use ts = new SimpleTempTable (tableClient) 333 | let rows = ts.InsertTestData() 334 | let! (simples1, segmentToken1) = 335 | Query.all 336 | |> fromTableSegmentedAsync tableClient ts.Name None 337 | 338 | segmentToken1 |> Expect.isSome "SegmentToken1 should be Some" 339 | 340 | let! (simples2, segmentToken2) = 341 | Query.all 342 | |> fromTableSegmentedAsync tableClient ts.Name segmentToken1 343 | 344 | segmentToken2 |> Expect.isNone "SegmentToken1 should be None" 345 | 346 | let allSimples = [simples1; simples2] |> Seq.concat |> Seq.toArray 347 | allSimples |> verifyRecords rows 348 | allSimples |> verifyMetadata 349 | } 350 | 351 | testCaseAsync "AsyncSeq segmented query" <| async { 352 | use ts = new SimpleTempTable (tableClient) 353 | let rows = ts.InsertTestData() 354 | let! allSimples = 355 | Query.all 356 | |> fromTableSegmentedAsyncSeq tableClient ts.Name 357 | |> AsyncSeq.concatSeq 358 | |> AsyncSeq.toArrayAsync 359 | 360 | allSimples |> verifyRecords rows 361 | allSimples |> verifyMetadata 362 | } 363 | 364 | testCaseAsync "async query that crosses segments with a take that is greater than the segment size" <| async { 365 | use ts = new SimpleTempTable (tableClient) 366 | do ts.InsertTestData() |> ignore 367 | let! results = 368 | Query.all 369 | |> Query.take 1100 370 | |> fromTableAsync tableClient ts.Name 371 | 372 | results |> Seq.length |> Expect.equal "Should return the number of rows taken" 1100 373 | } 374 | 375 | gameTestCase "query with a type that has system properties on it" <| fun ts -> 376 | let valveGames = 377 | Query.all 378 | |> Query.where <@ fun _ s -> s.PartitionKey = "Valve" @> 379 | |> fromTable tableClient ts.Name 380 | |> Seq.toArray 381 | 382 | valveGames |> Seq.map (fun (g, _) -> g.PartitionKey) |> Expect.allEqual "All PKs should be Valve" "Valve" 383 | valveGames |> Seq.map (fun (g, _) -> g.RowKey) |> Expect.contains "Games should contain Half-Life 2" "Half-Life 2-PC" 384 | valveGames |> Seq.map (fun (g, _) -> g.RowKey) |> Expect.contains "Games should contain Portal" "Portal-PC" 385 | valveGames |> Seq.map (fun (g, _) -> g.RowKey) |> Expect.contains "Games should contain Portal 2" "Portal 2-PC" 386 | valveGames |> Seq.map (fun (g, _) -> g.Timestamp) |> Seq.filter (fun ts -> ts = (DateTimeOffset())) |> Expect.isEmpty "All timestamps should be non-default" 387 | 388 | valveGames |> verifyMetadata 389 | 390 | 391 | gameTestCase "query with a type that has system properties annotations on it" <| fun ts -> 392 | let valveGames = 393 | Query.all 394 | |> Query.where <@ fun _ s -> s.PartitionKey = "Valve" @> 395 | |> fromTable tableClient ts.Name 396 | |> Seq.toArray 397 | 398 | valveGames |> Seq.map (fun (g, _) -> g.PartitionKey) |> Expect.allEqual "All PKs should be Valve" "Valve" 399 | valveGames |> Seq.map (fun (g, _) -> g.RowKey) |> Expect.contains "Games should contain Half-Life 2" "Half-Life 2-PC" 400 | valveGames |> Seq.map (fun (g, _) -> g.RowKey) |> Expect.contains "Games should contain Portal" "Portal-PC" 401 | valveGames |> Seq.map (fun (g, _) -> g.RowKey) |> Expect.contains "Games should contain Portal 2" "Portal 2-PC" 402 | valveGames |> Seq.map (fun (g, _) -> g.Modified) |> Seq.choose id |> Seq.filter (fun ts -> ts = (DateTimeOffset())) |> Expect.isEmpty "All timestamps should be non-default" 403 | valveGames |> Seq.map (fun (g, _) -> g.tag) |> Seq.choose id |> Seq.filter (fun tag -> tag |> String.IsNullOrEmpty) |> Expect.isEmpty "All etags should be non-default" 404 | 405 | valveGames |> verifyMetadata 406 | 407 | gameTestCase "query with a table entity type" <| fun ts -> 408 | let valveGames = 409 | Query.all 410 | |> Query.where <@ fun _ s -> s.PartitionKey = "Valve" @> 411 | |> fromTable tableClient ts.Name 412 | |> Seq.toArray 413 | 414 | valveGames |> verifyRecords [| 415 | { Developer = "Valve"; Name = "Half-Life 2"; Platform = "PC"; HasMultiplayer = true } 416 | { Developer = "Valve"; Name = "Portal"; Platform = "PC"; HasMultiplayer = false } 417 | { Developer = "Valve"; Name = "Portal 2"; Platform = "PC"; HasMultiplayer = false } 418 | |] 419 | 420 | valveGames |> Seq.map (fun (g, _) -> g.PartitionKey) |> Expect.allEqual "All PKs should be Valve" "Valve" 421 | valveGames |> Seq.map (fun (g, _) -> g.RowKey) |> Expect.contains "Games should contain Half-Life 2" "Half-Life 2-PC" 422 | valveGames |> Seq.map (fun (g, _) -> g.RowKey) |> Expect.contains "Games should contain Portal" "Portal-PC" 423 | valveGames |> Seq.map (fun (g, _) -> g.RowKey) |> Expect.contains "Games should contain Portal 2" "Portal 2-PC" 424 | valveGames |> Seq.map (fun (g, _) -> g.Timestamp) |> Seq.filter (fun ts -> ts = (DateTimeOffset())) |> Expect.isEmpty "All timestamps should be non-default" 425 | 426 | valveGames |> verifyMetadata 427 | 428 | gameTestCase "query with a table entity type that has an ignored property" <| fun ts -> 429 | let valveGames = 430 | Query.all 431 | |> Query.where <@ fun _ s -> s.PartitionKey = "Valve" @> 432 | |> fromTable tableClient ts.Name 433 | |> Seq.toArray 434 | 435 | valveGames |> verifyRecords [| 436 | { Developer = "Valve"; Name = "Half-Life 2"; Platform = "PC"; HasMultiplayer = true } 437 | { Developer = "Valve"; Name = "Portal"; Platform = "PC"; HasMultiplayer = false } 438 | { Developer = "Valve"; Name = "Portal 2"; Platform = "PC"; HasMultiplayer = false } 439 | |] 440 | 441 | valveGames |> Seq.map (fun (g, _) -> g.PartitionKey) |> Expect.allEqual "All PKs should be Valve" "Valve" 442 | valveGames |> Seq.map (fun (g, _) -> g.RowKey) |> Expect.contains "Games should contain Half-Life 2" "Half-Life 2-PC" 443 | valveGames |> Seq.map (fun (g, _) -> g.RowKey) |> Expect.contains "Games should contain Portal" "Portal-PC" 444 | valveGames |> Seq.map (fun (g, _) -> g.RowKey) |> Expect.contains "Games should contain Portal 2" "Portal 2-PC" 445 | valveGames |> Seq.map (fun (g, _) -> g.Timestamp) |> Seq.filter (fun ts -> ts = (DateTimeOffset())) |> Expect.isEmpty "All timestamps should be non-default" 446 | 447 | //Check that all HasMultiplayers are false as they should have not been populated as they are ignored 448 | valveGames |> Seq.map (fun (g, _) -> g.HasMultiplayer) |> Expect.allEqual "All HasMultiplayers are false" false 449 | 450 | valveGames |> verifyMetadata 451 | 452 | gameTestCase "querying with types that aren't records or implement ITableEntity fails" <| fun ts -> 453 | (fun () -> Query.all |> fromTable tableClient ts.Name |> ignore) 454 | |> Expect.throwsT "Throws exception" 455 | 456 | gameTestCase "querying for a record that has option type fields works" <| fun ts -> 457 | let game = 458 | { Name = "Transistor" 459 | Platform = "PC" 460 | Developer = "Supergiant Games" 461 | HasMultiplayer = Some false 462 | Notes = None } 463 | 464 | let result = game |> Insert |> inTable tableClient ts.Name 465 | result.HttpStatusCode |> Expect.equal "Status code should be 204" 204 466 | 467 | let retrievedGame = 468 | Query.all 469 | |> Query.where <@ fun _ s -> s.PartitionKey = game.Developer && s.RowKey = game.Name + "-" + game.Platform @> 470 | |> ts.FromGameTable 471 | |> Seq.head 472 | |> fst 473 | 474 | retrievedGame |> Expect.equal "Retrieved game should be correct" game 475 | 476 | gameTestCase "querying for a record that has option type fields works when filtering by the option-types properties" <| fun ts -> 477 | let game = 478 | { Name = "Transistor" 479 | Platform = "PC" 480 | Developer = "Supergiant Games" 481 | HasMultiplayer = Some false 482 | Notes = Some "From the same studio that made Bastion" } 483 | 484 | let result = game |> Insert |> inTable tableClient ts.Name 485 | result.HttpStatusCode |> Expect.equal "Status code should be 204" 204 486 | 487 | let retrievedGame = 488 | Query.all 489 | |> Query.where <@ fun g _ -> g.HasMultiplayer = Some false && g.Notes = game.Notes @> 490 | |> ts.FromGameTable 491 | |> Seq.head 492 | |> fst 493 | 494 | retrievedGame |> Expect.equal "Retrieved game should be correct" game 495 | 496 | gameTestCase "querying for a record and filtering using a None option value is not supported by table storage" <| fun ts -> 497 | let game = 498 | { Name = "Transistor" 499 | Platform = "PC" 500 | Developer = "Supergiant Games" 501 | HasMultiplayer = None 502 | Notes = Some "From the same studio that made Bastion" } 503 | 504 | let result = game |> Insert |> inTable tableClient ts.Name 505 | result.HttpStatusCode |> Expect.equal "Status code should be 204" 204 506 | 507 | (fun () -> Query.all 508 | |> Query.where <@ fun g _ -> g.HasMultiplayer = None && g.Notes = game.Notes @> 509 | |> ignore) 510 | |> Expect.throwsT "Throws exception" 511 | 512 | gameTestCase "querying for a record type that is internal works" <| fun ts -> 513 | let game = 514 | { InternalGame.Name = "Transistor" 515 | Developer = "Supergiant Games" } 516 | 517 | let result = game |> Insert |> inTable tableClient ts.Name 518 | result.HttpStatusCode |> Expect.equal "Status code should be 204" 204 519 | 520 | let retrievedGame = 521 | Query.all 522 | |> Query.where <@ fun _ s -> s.PartitionKey = game.Developer && s.RowKey = game.Name @> 523 | |> ts.FromGameTable 524 | |> Seq.head 525 | |> fst 526 | 527 | retrievedGame |> Expect.equal "Retrieved game should be correct" game 528 | 529 | gameTestCase "querying for a record type that uses a DateTime and DateTimeOffset property work" <| fun ts -> 530 | let game = 531 | { Name = "Transistor" 532 | Developer = "Supergiant Games" 533 | DevelopmentDate = DateTime(2014, 5, 20, 0, 0, 0, DateTimeKind.Utc) 534 | DevelopmentDateAsOffset = DateTimeOffset(2014, 5, 20, 0, 0, 0, TimeSpan.Zero) } 535 | 536 | let result = game |> Insert |> inTable tableClient ts.Name 537 | result.HttpStatusCode |> Expect.equal "Status code should be 204" 204 538 | 539 | let retrievedGame = 540 | Query.all 541 | |> Query.where <@ fun g _ -> g.DevelopmentDate = game.DevelopmentDate && g.DevelopmentDateAsOffset = game.DevelopmentDateAsOffset @> 542 | |> ts.FromGameTable 543 | |> Seq.head 544 | |> fst 545 | 546 | retrievedGame |> Expect.equal "Retrieved game should be correct" game 547 | 548 | gameTestCase "querying for a record type with URI works" <| fun ts -> 549 | let game = 550 | { GameWithUri.Name = "Transistor" 551 | Developer = "Supergiant Games" 552 | HasMultiplayer = true 553 | Website = Uri ("https://example.org")} 554 | 555 | let result = game |> Insert |> inTable tableClient ts.Name 556 | result.HttpStatusCode |> Expect.equal "Status code should be 204" 204 557 | 558 | let retrievedGame = 559 | Query.all 560 | |> Query.where <@ fun _ s -> s.PartitionKey = game.Developer && s.RowKey = game.Name @> 561 | |> ts.FromGameTable 562 | |> Seq.head 563 | |> fst 564 | 565 | retrievedGame |> Expect.equal "Retrieved game should be correct" game 566 | 567 | gameTestCase "querying for a record type with URI option works" <| fun ts -> 568 | let game = 569 | { GameWithUriOptions.Name = "Transistor" 570 | Developer = "Supergiant Games" 571 | Website = Some (Uri ("https://example.org"))} 572 | 573 | let result = game |> Insert |> inTable tableClient ts.Name 574 | result.HttpStatusCode |> Expect.equal "Status code should be 204" 204 575 | 576 | let retrievedGame = 577 | Query.all 578 | |> Query.where <@ fun _ s -> s.PartitionKey = game.Developer && s.RowKey = game.Name @> 579 | |> ts.FromGameTable 580 | |> Seq.head 581 | |> fst 582 | 583 | retrievedGame |> Expect.equal "Retrieved game should be correct" game 584 | 585 | testCase "querying for a record type with union works" <| fun () -> 586 | use ts = new SimpleTempTable (tableClient) 587 | let data = { PartitionKey = "PK"; RowKey = "RK"; UnionProp = A } 588 | 589 | data |> Insert |> inTable tableClient ts.Name |> ignore 590 | 591 | let (output, _) = 592 | Query.all 593 | |> fromTable tableClient ts.Name 594 | |> Seq.head 595 | output.UnionProp |> Expect.equal "Retrieved union property correctly" A 596 | 597 | testCase "querying for a record type with union with fields fails" <| fun () -> 598 | use ts = new SimpleTempTable (tableClient) 599 | let data = { PartitionKey = "PK"; RowKey = "RK"; UnionProp = A } 600 | 601 | data |> Insert |> inTable tableClient ts.Name |> ignore 602 | 603 | (fun () -> 604 | Query.all 605 | |> fromTable tableClient ts.Name 606 | |> Seq.head 607 | |> ignore ) 608 | |> Expect.throws "Union with field" 609 | 610 | testCase "querying for a record type with enum works" <| fun () -> 611 | use ts = new SimpleTempTable (tableClient) 612 | let data = { PartitionKey = "PK"; RowKey = "RK"; EnumProp = EnumProperty.C } 613 | 614 | data |> Insert |> inTable tableClient ts.Name |> ignore 615 | 616 | let (output, _) = 617 | Query.all 618 | |> fromTable tableClient ts.Name 619 | |> Seq.head 620 | output.EnumProp |> Expect.equal "Retrieved enum property correctly" EnumProperty.C 621 | 622 | gameTestCase "query using a DynamicTableEntity" <| fun ts -> 623 | let halo4 = 624 | Query.all 625 | |> Query.where <@ fun _ s -> s.PartitionKey = "343 Industries" && s.RowKey = "Halo 4-Xbox 360" @> 626 | |> ts.FromGameTable 627 | |> Seq.toArray 628 | 629 | Expect.hasLength halo4 1 "One record returned" 630 | let (record, _metadata) = halo4.[0] 631 | 632 | record.PartitionKey |> Expect.equal "PartitionKey is correct" "343 Industries" 633 | record.RowKey |> Expect.equal "RowKey is correct" "Halo 4-Xbox 360" 634 | record.Properties |> Seq.sortBy (fun kvp -> kvp.Key) |> Expect.sequenceEqual "Correct properties" ([ 635 | KeyValuePair("Developer", EntityProperty.GeneratePropertyForString("343 Industries")) 636 | KeyValuePair("HasMultiplayer", EntityProperty.GeneratePropertyForBool(true)) 637 | KeyValuePair("Name", EntityProperty.GeneratePropertyForString("Halo 4")) 638 | KeyValuePair("Platform", EntityProperty.GeneratePropertyForString("Xbox 360")) 639 | ]) 640 | 641 | halo4 |> verifyMetadata 642 | ] -------------------------------------------------------------------------------- /test/FSharp.Azure.Storage.Tests/Table/QueryExpressionTests.fs: -------------------------------------------------------------------------------- 1 | module FSharp.Azure.Storage.Tests.Table.QueryExpressionTests 2 | 3 | open System 4 | open FSharp.Azure.Storage.Table 5 | open Expecto 6 | open Expecto.Flip 7 | 8 | type GameWithOptions = 9 | { Name: string 10 | Platform: string option 11 | Developer : string 12 | HasMultiplayer: bool option } 13 | 14 | let gameTypePropertySet = ["Name"; "Platform"; "Developer"; "HasMultiplayer"] |> Set.ofList 15 | 16 | type Game = 17 | { Name: string 18 | Platform: string 19 | Developer : string 20 | HasMultiplayer: bool } 21 | 22 | let gameFilterTestCase testName whereExpr expectedFilterExpr = 23 | testCase testName <| fun () -> 24 | let query = Query.all |> Query.where whereExpr 25 | 26 | query.Filter |> Expect.equal "Correct filter expression" expectedFilterExpr 27 | query.TakeCount.IsNone |> Expect.isTrue "No take count" 28 | query.SelectColumns |> Expect.equal "All columns should be selected" gameTypePropertySet 29 | 30 | let tests = 31 | testList "Query Expression Tests" [ 32 | testCase "all returns empty query" <| fun () -> 33 | let query = Query.all 34 | 35 | query |> Expect.equal "Query should be empty query" (EntityQuery.get_Zero ()) 36 | query.SelectColumns |> Expect.equal "All columns should be selected" gameTypePropertySet 37 | 38 | gameFilterTestCase "where query with equals against value" 39 | <@ fun g _ -> g.Name = "Half-Life 2" @> 40 | "Name eq 'Half-Life 2'" 41 | 42 | gameFilterTestCase "where query with not equals against value" 43 | <@ fun g _ -> g.Name <> "Half-Life 2" @> 44 | "Name ne 'Half-Life 2'" 45 | 46 | gameFilterTestCase "where query with greater than against value" 47 | <@ fun g _ -> g.Name > "Half-Life 2" @> 48 | "Name gt 'Half-Life 2'" 49 | 50 | gameFilterTestCase "where query with greater than or equals against value" 51 | <@ fun g _ -> g.Name >= "Half-Life 2" @> 52 | "Name ge 'Half-Life 2'" 53 | 54 | gameFilterTestCase "where query with less than against value" 55 | <@ fun g _ -> g.Name < "Half-Life 2" @> 56 | "Name lt 'Half-Life 2'" 57 | 58 | gameFilterTestCase "where query with less than or equals against value" 59 | <@ fun g _ -> g.Name <= "Half-Life 2" @> 60 | "Name le 'Half-Life 2'" 61 | 62 | gameFilterTestCase "where query with logical and operator" 63 | <@ fun g _ -> g.Name = "Half-Life 2" && g.Developer = "Valve" @> 64 | "(Name eq 'Half-Life 2') and (Developer eq 'Valve')" 65 | 66 | gameFilterTestCase "where query with logical or operator" 67 | <@ fun g _ -> g.Name = "Half-Life 2" || g.Developer = "Valve" @> 68 | "(Name eq 'Half-Life 2') or (Developer eq 'Valve')" 69 | 70 | gameFilterTestCase "where query with logical or and and operators uses correct operator precedence" 71 | <@ fun g _ -> g.Name = "Half-Life 2" || g.Developer = "Crystal Dynamics" && g.Name = "Tomb Raider" @> 72 | "(Name eq 'Half-Life 2') or ((Developer eq 'Crystal Dynamics') and (Name eq 'Tomb Raider'))" 73 | 74 | gameFilterTestCase "where query with logical or and and operators and parentheses" 75 | <@ fun g _ -> (g.Name = "Half-Life 2" || g.Name = "Portal") && g.Developer = "Valve" @> 76 | "((Name eq 'Half-Life 2') or (Name eq 'Portal')) and (Developer eq 'Valve')" 77 | 78 | gameFilterTestCase "where query with comparison value coming from outside let binding" 79 | (let gameName = "Half-Life 2" in <@ fun g _ -> g.Name = gameName @>) 80 | "Name eq 'Half-Life 2'" 81 | 82 | gameFilterTestCase "where query with comparison value coming from outside object" 83 | (let game = { Name = "Half-Life 2"; Platform = "PC"; Developer = "Valve"; HasMultiplayer = true } 84 | <@ fun g _ -> g.Name = game.Name @>) 85 | "Name eq 'Half-Life 2'" 86 | 87 | gameFilterTestCase "where query allows commutative comparison with equals" 88 | <@ fun g _ -> "Halo" = g.Name @> 89 | "Name eq 'Halo'" 90 | 91 | gameFilterTestCase "where query allows commutative comparison with not equals" 92 | <@ fun g _ -> "Halo" <> g.Name @> 93 | "Name ne 'Halo'" 94 | 95 | gameFilterTestCase "where query allows commutative comparison with greater than" 96 | <@ fun g _ -> "Halo" > g.Name @> 97 | "Name lt 'Halo'" 98 | 99 | gameFilterTestCase "where query allows commutative comparison with greater than or equals" 100 | <@ fun g _ -> "Halo" >= g.Name @> 101 | "Name le 'Halo'" 102 | 103 | gameFilterTestCase "where query allows commutative comparison with less than" 104 | <@ fun g _ -> "Halo" < g.Name @> 105 | "Name gt 'Halo'" 106 | 107 | gameFilterTestCase "where query allows commutative comparison with less than or equals" 108 | <@ fun g _ -> "Halo" <= g.Name @> 109 | "Name ge 'Halo'" 110 | 111 | gameFilterTestCase "where query allows bool properties to be treated as comparison against true" 112 | <@ fun g _ -> g.HasMultiplayer @> 113 | "HasMultiplayer eq true" 114 | 115 | gameFilterTestCase "where query allows notted bool properties to be treated as comparison against false" 116 | <@ fun g _ -> not g.HasMultiplayer @> 117 | "HasMultiplayer eq false" 118 | 119 | gameFilterTestCase "where query allows bool properties to be combined with other comparisons" 120 | <@ fun g _ -> g.HasMultiplayer || g.Developer = "Valve" @> 121 | "(HasMultiplayer eq true) or (Developer eq 'Valve')" 122 | 123 | gameFilterTestCase "where query allows not to be used against property comparison" 124 | <@ fun g _ -> not (g.Developer = "Valve") @> 125 | "not (Developer eq 'Valve')" 126 | 127 | gameFilterTestCase "where query allows not to be used against boolean expressions" 128 | <@ fun g _ -> not (g.Developer = "Valve" && g.Name = "Portal") @> 129 | "not ((Developer eq 'Valve') and (Name eq 'Portal'))" 130 | 131 | testCase "where query allows comparison against option types" <| fun () -> 132 | let query = 133 | Query.all 134 | |> Query.where <@ fun g _ -> g.Platform = Some "Valve" && g.HasMultiplayer = Some true @> 135 | 136 | query.Filter |> Expect.equal "Correct filter expression" "(Platform eq 'Valve') and (HasMultiplayer eq true)" 137 | query.TakeCount.IsNone |> Expect.isTrue "No take count" 138 | query.SelectColumns |> Expect.equal "All columns should be selected" gameTypePropertySet 139 | 140 | testCase "where query does not allow comparison against option types with None" <| fun () -> 141 | (fun () -> 142 | Query.all 143 | |> Query.where <@ fun g _ -> (g.Platform = None && g.HasMultiplayer = None) @> 144 | |> ignore) 145 | |> Expect.throwsT "Throws exception" 146 | 147 | testCase "multiple where queries are anded together" <| fun () -> 148 | let query = 149 | Query.all 150 | |> Query.where <@ fun g _ -> g.Name = "Halo 4" @> 151 | |> Query.where <@ fun g _ -> g.Developer = "343 Studios" @> 152 | 153 | query.Filter |> Expect.equal "Correct filter expression" "(Name eq 'Halo 4') and (Developer eq '343 Studios')" 154 | query.TakeCount.IsNone |> Expect.isTrue "No take count" 155 | query.SelectColumns |> Expect.equal "All columns should be selected" gameTypePropertySet 156 | 157 | gameFilterTestCase "partition key where query comparison" 158 | <@ (fun _ s -> s.PartitionKey = "Valve") @> 159 | "PartitionKey eq 'Valve'" 160 | 161 | gameFilterTestCase "row key where query comparison" 162 | <@ (fun _ s -> s.RowKey <> "PS4") @> 163 | "RowKey ne 'PS4'" 164 | 165 | gameFilterTestCase "timestamp where query comparison" 166 | (let datetime = DateTimeOffset(2014, 4, 1, 12, 0, 0, TimeSpan.FromHours(11.0)) 167 | <@ (fun _ s -> s.Timestamp > datetime) @>) 168 | "Timestamp gt datetime'2014-04-01T01:00:00.0000000Z'" 169 | 170 | gameFilterTestCase "where query allows system and entity properties to be all used together" 171 | (let datetime = DateTimeOffset(2014, 4, 1, 12, 0, 0, TimeSpan.FromHours(11.0)) 172 | <@ fun g s -> 173 | g.Name >= "Halo" && g.Name < "I" && 174 | s.PartitionKey = "Bungie" && s.RowKey = "Xbox 360" && 175 | s.Timestamp < datetime @>) 176 | "((((Name ge 'Halo') and (Name lt 'I')) and (PartitionKey eq 'Bungie')) and (RowKey eq 'Xbox 360')) and (Timestamp lt datetime'2014-04-01T01:00:00.0000000Z')" 177 | 178 | testCase "take sets take count on query" <| fun () -> 179 | let query = Query.all |> Query.take 10 180 | 181 | query.Filter |> Expect.equal "Empty filter expression" "" 182 | query.TakeCount |> Expect.equal "Correct take count" (Some 10) 183 | query.SelectColumns |> Expect.equal "All columns should be selected" gameTypePropertySet 184 | 185 | testCase "multiple takes uses the smallest take count on query" <| fun () -> 186 | let query = Query.all |> Query.take 30 |> Query.take 10 |> Query.take 20 187 | 188 | query.Filter |> Expect.equal "Empty filter expression" "" 189 | query.TakeCount |> Expect.equal "Correct take count" (Some 10) 190 | query.SelectColumns |> Expect.equal "All columns should be selected" gameTypePropertySet 191 | 192 | testCase "take combines with where query" <| fun () -> 193 | let query = 194 | Query.all 195 | |> Query.where <@ fun _ s -> s.PartitionKey = "Blizzard" @> 196 | |> Query.take 5 197 | 198 | query.Filter |> Expect.equal "Correct filter expression" "PartitionKey eq 'Blizzard'" 199 | query.TakeCount |> Expect.equal "Correct take count" (Some 5) 200 | query.SelectColumns |> Expect.equal "All columns should be selected" gameTypePropertySet 201 | 202 | testCase "manual modification of query select columns is respected by subsequent query transformations" <| fun () -> 203 | let properties = [ "Name"; "Platform" ] |> Set.ofList 204 | let query = 205 | Query.all 206 | |> (fun q -> { q with SelectColumns = properties }) 207 | |> Query.where <@ fun g s -> s.PartitionKey = "Blizzard" @> 208 | |> Query.take 5 209 | 210 | query.Filter |> Expect.equal "Correct filter expression" "PartitionKey eq 'Blizzard'" 211 | query.TakeCount |> Expect.equal "Correct take count" (Some 5) 212 | query.SelectColumns |> Expect.equal "Correct columns selected" properties 213 | ] -------------------------------------------------------------------------------- /test/FSharp.Azure.Storage.Tests/Utilities.fs: -------------------------------------------------------------------------------- 1 | namespace FSharp.Azure.Storage.Tests 2 | 3 | module Storage = 4 | 5 | open System 6 | open Microsoft.Azure.Cosmos.Table 7 | 8 | type TempTable (client : CloudTableClient) = 9 | let name = sprintf "TestTable%s" (Guid.NewGuid().ToString("N")) 10 | let table = client.GetTableReference name 11 | do table.CreateIfNotExistsAsync () |> Async.AwaitTask |> Async.Ignore |> Async.RunSynchronously 12 | 13 | member val Name = name 14 | member val Table = table 15 | 16 | interface IDisposable with 17 | member __.Dispose() = 18 | table.DeleteIfExistsAsync() |> Async.AwaitTask |> Async.Ignore |> Async.RunSynchronously 19 | 20 | module Async = 21 | 22 | open FSharp.Azure.Storage 23 | 24 | let Sequential computations = 25 | let rec innerSequential results computations = 26 | async { 27 | match computations with 28 | | current :: rest -> 29 | let! result = current 30 | return! rest |> innerSequential (result :: results) 31 | | [] -> 32 | return results 33 | } 34 | 35 | async { 36 | let! results = innerSequential [] computations 37 | return results |> List.rev 38 | } 39 | 40 | let ParallelByDegree degree computations = 41 | if degree < 1 then invalidArg "degree" "degree must be 1 or greater" 42 | let parallelWork = 43 | computations 44 | |> Seq.split degree 45 | |> Seq.map (Seq.toList >> Sequential) 46 | |> Async.Parallel 47 | 48 | async { 49 | let! completedParallels = parallelWork 50 | return completedParallels |> Seq.concat |> Seq.toArray 51 | } 52 | -------------------------------------------------------------------------------- /test/FSharp.Azure.Storage.Tests/paket.references: -------------------------------------------------------------------------------- 1 | Expecto 2 | FSharp.Core 3 | Microsoft.Azure.Cosmos.Table --------------------------------------------------------------------------------