{"id":22770981,"url":"https://github.com/misterkaiou/fsharp-constellation","last_synced_at":"2025-09-04T19:23:59.699Z","repository":{"id":137123746,"uuid":"408017231","full_name":"MisterKaiou/fsharp-constellation","owner":"MisterKaiou","description":"A simple wrapper around Cosmos SDK for F#","archived":false,"fork":false,"pushed_at":"2022-05-09T16:31:58.000Z","size":139,"stargazers_count":1,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-08-01T02:28:44.414Z","etag":null,"topics":["cosmos","cosmos-sdk","cosmosdb","fsharp","wrapper"],"latest_commit_sha":null,"homepage":"","language":"F#","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/MisterKaiou.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2021-09-19T02:57:17.000Z","updated_at":"2022-01-11T00:49:30.000Z","dependencies_parsed_at":"2023-08-10T20:32:40.735Z","dependency_job_id":null,"html_url":"https://github.com/MisterKaiou/fsharp-constellation","commit_stats":null,"previous_names":[],"tags_count":3,"template":false,"template_full_name":null,"purl":"pkg:github/MisterKaiou/fsharp-constellation","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/MisterKaiou%2Ffsharp-constellation","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/MisterKaiou%2Ffsharp-constellation/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/MisterKaiou%2Ffsharp-constellation/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/MisterKaiou%2Ffsharp-constellation/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/MisterKaiou","download_url":"https://codeload.github.com/MisterKaiou/fsharp-constellation/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/MisterKaiou%2Ffsharp-constellation/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":273659029,"owners_count":25145386,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-09-04T02:00:08.968Z","response_time":61,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["cosmos","cosmos-sdk","cosmosdb","fsharp","wrapper"],"created_at":"2024-12-11T16:11:32.434Z","updated_at":"2025-09-04T19:23:59.636Z","avatar_url":"https://github.com/MisterKaiou.png","language":"F#","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Constellation\n\n![Build Status](https://img.shields.io/github/workflow/status/MisterKaiou/fsharp-constellation/Build/main?style=for-the-badge)\n![Downloads](https://img.shields.io/nuget/dt/FSharp.Constellation?style=for-the-badge)\n![Nuget Version](https://img.shields.io/nuget/v/FSharp.Constellation?style=for-the-badge)\n## What is Constellation?\n\nConstellation is a wrapper library around parts of the CosmosDB SDK, mainly CRUD operations, to make use with F# more friendly and straight-forward.\n\n## Motivation\n\nThe reason why I decided to make this package came from [FSharp.CosmosDb](https://github.com/aaronpowell/FSharp.CosmosDb) (a package by [@aaronpowell](https://github.com/aaronpowell)), [this section](https://docs.microsoft.com/en-us/azure/cosmos-db/sql/performance-tips-dotnet-sdk-v3-sql#sdk-usage) of the Microsoft Performance Tips for CosmosDB, a little bit of the context syntax sugar from Entity Framework, and a personal project that I am working on at the date of writing.\n\nThe library that Aaron created is great, but I needed something a little too specific for my use case. So I've decided to create something that can be a little more friendly with DI Frameworks, like ASP.NET's default one, and that does not instantiate multiple instances of the CosmosClient, as suggested in the Tips from Microsoft.\n\n## Goal\n\nThe goal is to provide F# syntax to CosmosDB SDK while also allowing to use all the options provided by the SDK.\n\n## How does it work?\n\nIt has two wrappers around CosmosDB SDK v3, since v4 is still preview. One around CosmosClient, named `Constellation.CosmosContext` and another one around the Container named `Constellation.Container`. These wrappers are just a proxy to provide you with a more F# friendly syntax to communicate with CosmosDB.\n\n### Attributes\n\nThere are only three attributes you might need to use when interacting with this library.\nThey are on the module `Constellation.Attributes`\n\n#### Id\nThis attribute marks the property as an id field and will be used to represent an id when needed. Keep in mind that the id field must be a string.\n````f#\nopen Constellation.Attributes\n\ntype User =\n  { [\u003cId\u003e] Id: string }\n````\n\n#### PartitionKey\nFor almost all operations the PartitionKey is needed. Fields marked with this attribute can only be of type **string, bool, double or float**:\n\n````f#\nopen Constellation.Attributes\n\ntype User =\n  { [\u003cId\u003e] Id: string\n    [\u003cPartitionKey\u003e] Username: string }\n````\n\nOr your PartitionKey can be inside a property on your root class. The example below sets\nthe PartitionKey to one level below (I.e.: In a reference type property of the root class)\nbut it can be as deep as you need it to. Just keep in mind that reflection is used to\nnavigate into these properties and get the value; if performance is a concern you might not\nwant to use this convenience.\n\n````f#\nopen Constellation.Attributes\n\ntype ContactInfo =\n  { [\u003cPartitionKey\u003e] Email: string }\n\ntype User =\n  { [\u003cId\u003e] Id: string\n    Username: string\n    ContactInformation: ContactInfo }\n````\n\nIf your data is partitioned by id, for example, a user is it's own partition, then you can do:\n\n````f#\nopen Constellation.Attributes\n\ntype User =\n  { [\u003cId; PartitionKey\u003e] Id: string\n    Username: string }\n````\n\n#### Container\n\nThis attributes allow you the specify the id of the container right in the model.\n\n````f#\nopen Constellation.Attributes\n\n[\u003cContainer(\"Users\")\u003e]\ntype User =\n  { [\u003cId; PartitionKey\u003e] Id: string\n    Username: string }\n````\n\n\u003cbr\u003e\n\n### Builders\n\nThis library exposes _some_ builders for the SDK option models. For the sake of brevity, I won't be going through all the builder methods (there is plenty since the models have lots of properties as well), but be sure that you'll be able to find practically all the method you need by being following the naming convention; that is, every method name corresponds to a option model's property but with words separated by underscores (snake_case), I.e. If a model has a property `SomeProperty` the builder exposes the method `some_property` that takes an input of the same type as the property.\n\nThere are currently builder for the following SDK option models:\n\n- `ChangeFeedRequestOptions` with name `ChangeFeedRequestOptionsBuilder`; instantiate with `changeFeedRequestOptions`\n- `ContainerRequestOptions` with name `ContainerRequestOptionsBuilder`; instantiate with `containerRequestOptions`\n- `CosmosClientOptions` with name `CosmosClientOptionsBuilder`; instantiate with `cosmosClientOptions`\n- `CosmosSerializationOptions` with name `CosmosSerializationOptionsBuilder`; instantiate with `cosmosSerializationOptions`\n- `ItemRequestOptions` with name `ItemRequestOptionsBuilder`; instantiate with `itemRequestOptions`\n- `PatchItemRequestOptions` with `PatchItemRequestOptionsBuilder`; instantiate with `patchItemRequestOptions`\n- `QueryRequestOptions` with name `QueryRequestOptionsBuilder`; instantiate with `queryRequestOptions`\n- `ReadManyRequestOptions` with name `ReadManyRequestOptionsBuilder`; instantiate with `readManyRequestOptions`\n- `RequestOptions` with name `RequestOptionsBuilder`; instantiate with `requestOptions`\n- `StorageProcedureRequestOptions` with name `StorageProcedureRequestOptionsBuilder`; instantiate with `storageProcedureRequestOptions`\n- `TransactionalBatchItemRequestOptions` with name `TransactionalBatchItemRequestOptionsBuilder`; instantiate with `transactionalBatchItemRequestOptions`\n- `TransactionalBatchPatchItemRequestOptions` with name `TransactionalBatchPatchItemRequestOptionsBuilder`; instantiate with `transactionalBatchPatchItemRequestOptions`\n- `TransactionalBatchRequestOptions` with name `TransactionalBatchRequestOptionsBuilder`; instantiate with `transactionalBatchRequestOptions`\n\n#### Example Usage\n\n````f#\nopen Constellation.TypeBuilders\n\nlet itemOption =\n  itemRequestOptions {\n    if_match_etag \"SomeTag\"\n    enable_content_response_on_write\n  }\n\n//Is the same as doing\n\nlet itemOption =\n  ItemRequestOptions(\n    IfMatchEtag = \"SomeTag\",\n    EnableContentResponseOnWrite = true\n  )\n````\n\n\u003cbr\u003e\n\n### CosmosContext\n\nThe wrapper around `CosmosClient`, holds a reference to a `CosmosClient` instance.\n\nBy default, this library replaces the default SDK JSON Serializer, for something that better handles F# types; [FSharp.SystemTextJson](https://github.com/Tarmil/FSharp.SystemTextJson), is used with union encoding set to `JsonUnionEncoding.FSharpLuLike ||| JsonUnionEncoding.NamedFields`. Please, refer to that project if you have any questions about how it works.\n\nTo override this behavior, on the constructor send the optional third parameter with your custom serializer set, or send `clientOptions=null` and you'll use the default SDK serializer (Not really recommended with F#).\n\n\n#### Instantiating the Context - Using the base class\n````f#\nopen Constellation.Context\n\n\nlet context = new CosmosContext(\"connectionString\", \"databaseId\") //Disposable\n\n//Or\n\nlet endpointInfo = { Endpoint = \"End/point\"; AccountKey = \"Key\" }\nlet context = new CosmosContext(endpointInfo, \"databaseId\") //Disposable\n\n(* Gets the underlying SDK client if you need to do something not yet covered by this library *)\nlet client = context.Client\n````\n\n\u003cbr\u003e\n\n#### Using the Context - Inheriting from the base class\n\n````f#\nopen Constellation.Attributes\n\n[\u003cContainer(\"Users\")\u003e]\ntype User = \n  { Name: string }\n\ntype MyContext(ConnString: string, DatabaseId: string) =\n  inherit CosmosContext(ConnString, DatabaseId)\n  \n  member this.Users = this.GetContainer\u003cUser\u003e()\n  \n//Or\n\ntype MyContext(ConnString: string, DatabaseId: string) =\n  inherit CosmosContext(ConnString, DatabaseId)\n  \n  member this.Users = this.GetContainer\u003cUser\u003e \"Users\"\n````\nFrom this example, we see we can achieve some kind of \"DbContext\". Register this class in your DI container or Composition Root and you should be good to go.\n\nNotice that, the type `CosmosContext` exposes a method `GetContainer\u003c'from\u003e` where you can either send the target ContainerId or a type that uses the attribute `Container(string)`. But note that the type is necessary since it is used on that `ConstellationContainer` instance to map from/to the type specified during interactions with the database.\n\n\u003cbr\u003e\n\n### ConstellationContainer\n\nThis is actually just a wrapper around the SDK `Container` class. As of now, it exposes the main CRUD operations in fluent syntax. Also, the methods exposes a version accepting a SDK option model, that has the suffix _WithOption_, for example, the method `getItem` has a version `getItemWithOptions`. The methods that accepts the models options also accepts a `CancellationToken`. So basically, their signatures are: `OptionModel -\u003e CancellationToken -\u003e arg1 -\u003e argN -\u003e ConstellationContainer\u003c'a\u003e -\u003e PendingOperation\u003c'a\u003e`.\n\n#### What is `PendingOperation\u003c'a\u003e`?\n\nAs the name might suggest it is an operation yet to be executed, it represent a function that returns an `AsyncSeq\u003cCosmosResponse\u003c'a\u003e\u003e` where `CosmosReponse\u003c'a\u003e` is an union type that maps on the SDK to either a default `Response\u003cT\u003e` or `FeedResponse\u003cT\u003e` in the case of queries or operations that supports continuation, for example, the insert operation returns the type `ItemResponse\u003c'a\u003e`; you can see it in more detail in the SDK documentation.\n\nThis module `Constellation.Container` exposes two methods for dealing with `PendingOperation\u003c'a\u003e`.\n\n- `Container.execAsync` with signature `PendingOperation\u003c'a\u003e -\u003e AsyncSeq\u003c'a\u003e`\n- `Container.execAsyncWrapped` with signature `PendingOperation\u003c'a\u003e -\u003e AsyncSeq\u003cCosmosResponse\u003c'a\u003e\u003e`\n\nAs said before, every operation returns `AsyncSeq\u003c'WrapperType\u003e`, if you want to you can iter through each item as it is used with `AsyncSeq.iter`, or do `AsyncSeq.toListSynchronously` to get list of the objects used in the operation. See below usage examples.\n\n#### KeyParam\n\nSome methods exposed by this library contain a parameter of type `KeyParam`, it is an union case of:\n- `SameIdPartition`: that is a convenience for when you container is partitioned by the documents id. \n- `IdAndKey`: That represents an id that is different from the partition key.\n\n`IdAndKey` Take a `string * PartitionKeys` where PartitionKeys is an union type of:\n- `StringKey of string`: A partition key of string type.\n- `BooleanKey of bool`: A partition key of a boolean value.\n- `NumericKey of double`: A partition key of numeric value.\n- `NoKey`: Maps to `PartitionKey.None` and represents a key that allows for operations providing no partition key.\n- `Null`: Maps to `PartitionKey.Null` and represents a key that allows for operations providing a null partition key.\n\n#### Get\n\nReturns a single result. Even though its return type is an `AsyncSeq`, this sequence will only contain either one item or nothing in it.\n\n````f#\n//Gets the container from a model with the attribute Container\nlet container = ctx |\u003e Context.getContainer\u003cUser\u003e\n\nlet getResults = //AsyncSeq\u003cUser\u003e\n  container\n  |\u003e Container.getItem (SameIdPartition \"itemId\")\n  |\u003e Container.execAsync\n  \n// or specifying a different response resource type.  \n\nlet getResults = //AsyncSeq\u003cAdminUser\u003e\n  container\n  |\u003e Container.getItemAs\u003c_, AdminUser\u003e (SameIdPartition \"itemId\")\n  |\u003e Container.execAsync\n\n// or wrapped\n  \nlet getResults = //AsyncSeq\u003cCosmosResponse\u003cUser\u003e\u003e\n  container \n  |\u003e Container.getItem (SameIdPartition \"itemId\")\n  |\u003e Container.execAsyncWrapped\n````\n\n#### Query\n\nReturns a collection of results of the container type that may contain none, one or more items.\n\n````f#\n//Gets the container from a model with the attribute Container\nlet container = ctx |\u003e Context.getContainer\u003cUser\u003e\n\nlet queryResult = //AsyncSeq\u003cUser\u003e\n  container\n  |\u003e Container.query \"SELECT u.Id, u.UserName FROM User u WHERE u.Id LIKE @someId\"\n  |\u003e Container.withParameters [ (\"@someId\", \"userId\") ]\n  |\u003e Container.execAsync\n  \n//Or parameterless\n\nlet queryResult = //AsyncSeq\u003cUser\u003e\n  container\n  |\u003e Container.parameterlessQuery \"SELECT u.Id, u.UserName FROM User u\"\n  |\u003e Container.execAsync\n  \n// Or wrapped\n  \nlet queryResult = //AsyncSeq\u003cCosmosResponse\u003cUser\u003e\u003e\n  container\n  |\u003e Container.query \"SELECT u.Id, u.UserName FROM User u WHERE u.Id LIKE @someId\"\n  |\u003e Container.withParameters [ (\"@someId\", box \u003c| \"userId\") ]\n  |\u003e Container.execQueryWrapped\n````\n\n#### Insert\n\nInserts the given collection in the database.\n\n````f#\n//Gets the container from a model with the attribute Container\nlet container = ctx |\u003e Context.getContainer\u003cUser\u003e\n\nlet user = { Id = \"SomeId\"; Username = \"User_Name\" }\n\nlet insertResult = //AsyncSeq\u003cUser\u003e\n  container\n  |\u003e Container.insertItem [ user ]\n  |\u003e Container.execAsync\n  \n// Or wrapped\n  \nlet insertResult = //AsyncSeq\u003cItemResponse\u003cUser\u003e\u003e\n  container\n  |\u003e Container.insertItem [ user ]\n  |\u003e Container.execAsyncWrapped\n````\n\n#### Update (Patch)\n\nUpdates (patches) the documents with the specified keys by applying the provided operations to them.\n\nSupported operations are described [here](https://docs.microsoft.com/en-us/azure/cosmos-db/partial-document-update#supported-operations).\nDescription is also provided on code documentation.\n\n```f#\n//Gets the container from a model with the attribute Container\nlet container = ctx |\u003e Context.getContainer\u003cUser\u003e\n\nlet update = {| Permission = \"Write\" |}\n\ncontainer\n|\u003e Container.updateItems\n  [ Add \u003c@@ update.Permission @@\u003e ]\n  [ SameIdPartition \"userId\" ]\n|\u003e Container.execAsync\n```\n\n#### Replace\n\nReplaces an item with the same Id and PartitionKey from the Database\n\n```f#\n//Gets the container from a model with the attribute Container\nlet container = ctx |\u003e Context.getContainer\u003cUser\u003e\n\nlet replaceForThis = { Id = \"SomeId\"; Username = \"UpdatedUserName\" }\n\nlet updateResult = //AsyncSeq\u003cUser\u003e\n  container\n  |\u003e Container.changeItem replaceForThis\n  |\u003e Container.execAsync\n  \n//Or\n\nlet updateResult = //AsyncSeq\u003cItemResponse\u003cUser\u003e\u003e\n  container\n  |\u003e Container.changeItem replaceForThis\n  |\u003e Container.execAsyncWrapped\n```\n\n#### Delete\n\nRemoves an item from the database.\nNote that, if you want to get the resource from the wrapped response, you can't; it is not returned by the database since it has been deleted.\n```f#\n//Gets the container from a model with the attribute Container\nlet container = ctx |\u003e Context.getContainer\u003cUser\u003e\n\nlet deleteThis = { Id = \"SomeId\"; Username = \"UpdatedUserName\" }\n\nlet deleteResult = //AsyncSeq\u003cUser\u003e\n  container\n  |\u003e Container.deleteItem [ deleteThis ] \n  |\u003e Container.execAsync\n  \n//Or\n\nlet deleteResult = //AsyncSeq\u003cItemResponse\u003cUser\u003e\u003e\n  container\n  |\u003e Container.deleteItem [ deleteThis ]\n  |\u003e Container.execAsyncWrapped\n```\n\u003cbr\u003e\n\n#### **Note:** \n\nWhen available, all methods above also provide a version with suffix `Of` and type parameters `\u003c'a, 'b\u003e` whose usage is similar to how it was showcases in the **Get** section above. Usually, you'll want to keep the first parameter the inferred default in the first argument but specifying the expected resource type with an actual type, i.e `\u003c_, AdminUser\u003e`.\n\nBe mindful that `'b` is the types used for deserialization, so if a field is expected but is missing from the response, depending on how you configured your serializer to handle those cases, an exception might be thrown.  \n\n### Request Logging\n\nIf need to log the requests as they go you can add a request handler, like below:\n\n```f#\nlet loggingHandler =\n  { new RequestHandler() with\n    member this.SendAsync(request: RequestMessage, token: CancellationToken) =\n      let content =\n        match request.Content |\u003e Option.ofObj with\n        | None -\u003e Array.zeroCreate 0\n        | Some c -\u003e c.AsyncRead (int request.Content.Length) |\u003e Async.RunSynchronously\n        \n      printfn $\"Sending {request.Method.Method}: %A{System.Text.Encoding.UTF8.GetString(content)}\"\n      this.InnerHandler.SendAsync(request, token)}\n\nlet cosmosOptions = cosmosClientOptions {\n  serializer defaultCosmosSerializer\n  custom_handler loggingHandler\n}\n\ntype PlanetContext(connString: string, databaseId: string, options: CosmosClientOptions Option) =\n  inherit CosmosContext(connString, databaseId, Option.toObj options)\n```\n\nIn this example we log every single request content body, if any.\n\n## Planned Future Features\n\n- Give support to Transactional Batches\n- Give support to Batch operations (Insert, Change, Delete, etc...)\n- Update to CosmosDb SDK v4 when it's released","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmisterkaiou%2Ffsharp-constellation","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fmisterkaiou%2Ffsharp-constellation","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmisterkaiou%2Ffsharp-constellation/lists"}