From fdbe1eba6f8c54e1174a3730046ef5e609a7126a Mon Sep 17 00:00:00 2001 From: Mark Brown Date: Thu, 18 Jul 2024 06:43:01 -0700 Subject: [PATCH 1/2] Refresh all samples --- .gitignore | 8 ++++---- .../source/website/appsettings.Development.json | 9 --------- .../source/website/appsettings.Development.json | 9 --------- 3 files changed, 4 insertions(+), 22 deletions(-) delete mode 100644 document-versioning/source/website/appsettings.Development.json delete mode 100644 schema-versioning/source/website/appsettings.Development.json diff --git a/.gitignore b/.gitignore index 89bc1a5..a07fd32 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,7 @@ appsettings.development.json appsettings.production.json appsettings.*.json +local.settings.json # User-specific files *.rsuser @@ -356,7 +357,6 @@ MigrationBackup/ # Ionide (cross platform F# VS Code tools) working folder .ionide/ -/distributed-lock/source/consoleapp/appsettings.development.json -/distributed-counter/source/Visualizer/appsettings.Development.json -/distributed-counter/source/ConsumerApp/appsettings.Development.json -/data-binning/source/appsettings.development.json + +/document-versioning/source/website/appsettings.Development.json +/schema-versioning/source/website/appsettings.Development.json diff --git a/document-versioning/source/website/appsettings.Development.json b/document-versioning/source/website/appsettings.Development.json deleted file mode 100644 index 770d3e9..0000000 --- a/document-versioning/source/website/appsettings.Development.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "DetailedErrors": true, - "Logging": { - "LogLevel": { - "Default": "Information", - "Microsoft.AspNetCore": "Warning" - } - } -} diff --git a/schema-versioning/source/website/appsettings.Development.json b/schema-versioning/source/website/appsettings.Development.json deleted file mode 100644 index 770d3e9..0000000 --- a/schema-versioning/source/website/appsettings.Development.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "DetailedErrors": true, - "Logging": { - "LogLevel": { - "Default": "Information", - "Microsoft.AspNetCore": "Warning" - } - } -} From d6a59e9e6cdc3fee1abb2149e1878ce3ac0e5f54 Mon Sep 17 00:00:00 2001 From: Mark Brown Date: Thu, 18 Jul 2024 06:43:28 -0700 Subject: [PATCH 2/2] Refresh all --- .devcontainer/data-binning/devcontainer.json | 2 +- .../distributed-counter/devcontainer.json | 2 +- .../distributed-lock/devcontainer.json | 2 +- .../event-sourcing/devcontainer.json | 2 +- .devcontainer/preallocation/devcontainer.json | 2 +- README.md | 35 ++- attribute-array/README.md | 49 ++-- attribute-array/source/Program.cs | 5 +- azuredeploy.json | 42 +++ data-binning/README.md | 47 ++- data-binning/source/.gitignore | 267 ------------------ data-binning/source/DataBinning.csproj | 2 +- data-binning/source/Program.cs | 11 +- distributed-counter/README.md | 86 ++++-- .../DistributedCounterConsumerApp.csproj | 2 + .../source/ConsumerApp/Program.cs | 10 +- .../source/ConsumerApp/appsettings.json | 3 - .../source/Counter/CosmosService.cs | 9 +- ...utedCounter.sln => DistributedCounter.sln} | 0 distributed-counter/source/SETUP.md | 65 ----- .../source/Visualizer/appsettings.json | 2 - distributed-counter/source/azuredeploy.json | 115 -------- distributed-lock/README.md | 55 ++-- .../source/{consoleapp => }/CosmosService.cs | 3 +- .../DistributedLockService.cs | 0 .../source/{consoleapp => }/GlobalLock.csproj | 3 + .../source/{consoleapp => }/LockManager.cs | 0 .../source/{consoleapp => }/LockTest.cs | 0 .../source/{consoleapp => }/Program.cs | 2 +- .../Properties/launchSettings.json | 4 +- distributed-lock/source/SETUP.md | 55 ---- distributed-lock/source/appsettings.json | 7 + .../consoleapp/Cosmos_Patterns_GlobalLock.sln | 25 -- .../source/consoleapp/appsettings.json | 17 -- document-versioning/README.md | 93 +++--- document-versioning/source/azuredeploy.json | 158 ----------- .../DocumentVersioningProcessor.cs | 27 +- .../source/function-app/function-app.csproj | 2 +- document-versioning/source/setup.md | 182 ------------ .../website/Controllers/StatusController.cs | 29 +- .../source/website/Models/Order.cs | 2 +- .../source/website/Models/OrderItem.cs | 2 +- .../source/website/Models/VersionedOrder.cs | 2 +- .../source/website/Options/CosmosDb.cs | 13 + .../source/website/Pages/Index.cshtml.cs | 15 +- document-versioning/source/website/Program.cs | 62 +++- .../website/Properties/launchSettings.json | 13 +- .../source/website/Services/CosmosDb.cs | 39 +++ .../source/website/Services/OrderHelper.cs | 69 ++--- .../source/website/appsettings.json | 10 +- .../source/website/website.csproj | 6 + event-sourcing/README.md | 40 ++- event-sourcing/source/.gitignore | 267 ------------------ event-sourcing/source/CartEvent.cs | 4 +- event-sourcing/source/CartItem.cs | 2 +- .../Cosmos_Patterns_EventSourcing.csproj | 22 -- ...rcingExample.cs => EventSourceFunction.cs} | 39 ++- event-sourcing/source/EventSourcing.csproj | 32 +++ event-sourcing/source/Program.cs | 53 +++- .../source/Properties/launchSettings.json | 4 +- event-sourcing/source/azuredeploy.json | 115 -------- event-sourcing/source/host.json | 29 +- event-sourcing/source/setup.md | 145 ---------- materialized-view/README.md | 96 +++---- materialized-view/source/azuredeploy.json | 158 ----------- .../source/data-generator/Options/Cosmos.cs | 9 + .../source/data-generator/Program.cs | 54 ++-- .../source/data-generator/appsettings.json | 4 + .../data-generator/data-generator.csproj | 13 + .../function-app/MaterializeViews.csproj | 2 +- .../function-app/MaterializedViewProcessor.cs | 24 +- materialized-view/source/setup.md | 200 ------------- preallocation/README.md | 82 +++--- .../source/Cosmos_Patterns_Preallocation.sln | 25 -- preallocation/source/Hotel.cs | 165 ++++++----- preallocation/source/Options/Cosmos.cs | 17 ++ ...allocation.csproj => Preallocation.csproj} | 6 +- preallocation/source/Program.cs | 234 +++++++-------- .../source/Properties/launchSettings.json | 2 +- preallocation/source/Reservation.cs | 2 +- preallocation/source/Room.cs | 23 +- preallocation/source/SETUP.md | 85 ------ preallocation/source/appsettings.json | 3 +- preallocation/source/azuredeploy.json | 115 -------- schema-versioning/README.md | 85 ++++-- schema-versioning/source/azuredeploy.json | 115 -------- .../source/data-generator/Options/Cosmos.cs | 11 + .../source/data-generator/Program.cs | 64 +++-- .../source/data-generator/appsettings.json | 7 + .../data-generator/data-generator.csproj | 14 +- schema-versioning/source/setup.md | 139 --------- .../source/website/Models/Cart.cs | 8 +- .../source/website/Models/CartItem.cs | 2 +- .../Models/CartItemWithSpecialOrder.cs | 2 +- .../source/website/Options/CosmosDb.cs | 12 + .../source/website/Pages/Index.cshtml | 3 +- .../source/website/Pages/Index.cshtml.cs | 12 +- schema-versioning/source/website/Program.cs | 61 +++- .../website/Properties/launchSettings.json | 11 +- .../source/website/Services/CartHelper.cs | 28 -- .../source/website/Services/CartService.cs | 37 +++ .../website/Services/CosmosDbService.cs | 33 +++ .../source/website/Services/CosmosHelper.cs | 41 --- .../source/website/appsettings.json | 11 +- .../source/website/website.csproj | 14 +- 105 files changed, 1324 insertions(+), 3112 deletions(-) create mode 100644 azuredeploy.json delete mode 100644 data-binning/source/.gitignore rename distributed-counter/source/{Cosmos_Patterns_DistributedCounter.sln => DistributedCounter.sln} (100%) delete mode 100644 distributed-counter/source/SETUP.md delete mode 100644 distributed-counter/source/azuredeploy.json rename distributed-lock/source/{consoleapp => }/CosmosService.cs (97%) rename distributed-lock/source/{consoleapp => }/DistributedLockService.cs (100%) rename distributed-lock/source/{consoleapp => }/GlobalLock.csproj (89%) rename distributed-lock/source/{consoleapp => }/LockManager.cs (100%) rename distributed-lock/source/{consoleapp => }/LockTest.cs (100%) rename distributed-lock/source/{consoleapp => }/Program.cs (96%) rename distributed-lock/source/{consoleapp => }/Properties/launchSettings.json (57%) delete mode 100644 distributed-lock/source/SETUP.md create mode 100644 distributed-lock/source/appsettings.json delete mode 100644 distributed-lock/source/consoleapp/Cosmos_Patterns_GlobalLock.sln delete mode 100644 distributed-lock/source/consoleapp/appsettings.json delete mode 100644 document-versioning/source/azuredeploy.json delete mode 100644 document-versioning/source/setup.md create mode 100644 document-versioning/source/website/Options/CosmosDb.cs create mode 100644 document-versioning/source/website/Services/CosmosDb.cs delete mode 100644 event-sourcing/source/.gitignore delete mode 100644 event-sourcing/source/Cosmos_Patterns_EventSourcing.csproj rename event-sourcing/source/{CosmosPatternsEventSourcingExample.cs => EventSourceFunction.cs} (57%) create mode 100644 event-sourcing/source/EventSourcing.csproj delete mode 100644 event-sourcing/source/azuredeploy.json delete mode 100644 event-sourcing/source/setup.md delete mode 100644 materialized-view/source/azuredeploy.json create mode 100644 materialized-view/source/data-generator/Options/Cosmos.cs create mode 100644 materialized-view/source/data-generator/appsettings.json delete mode 100644 materialized-view/source/setup.md delete mode 100644 preallocation/source/Cosmos_Patterns_Preallocation.sln create mode 100644 preallocation/source/Options/Cosmos.cs rename preallocation/source/{Cosmos_Patterns_Preallocation.csproj => Preallocation.csproj} (71%) delete mode 100644 preallocation/source/SETUP.md delete mode 100644 preallocation/source/azuredeploy.json delete mode 100644 schema-versioning/source/azuredeploy.json create mode 100644 schema-versioning/source/data-generator/Options/Cosmos.cs create mode 100644 schema-versioning/source/data-generator/appsettings.json delete mode 100644 schema-versioning/source/setup.md create mode 100644 schema-versioning/source/website/Options/CosmosDb.cs delete mode 100644 schema-versioning/source/website/Services/CartHelper.cs create mode 100644 schema-versioning/source/website/Services/CartService.cs create mode 100644 schema-versioning/source/website/Services/CosmosDbService.cs delete mode 100644 schema-versioning/source/website/Services/CosmosHelper.cs diff --git a/.devcontainer/data-binning/devcontainer.json b/.devcontainer/data-binning/devcontainer.json index ca64124..53a42d3 100644 --- a/.devcontainer/data-binning/devcontainer.json +++ b/.devcontainer/data-binning/devcontainer.json @@ -3,7 +3,7 @@ "image": "ghcr.io/azure-samples/cosmos-db-design-patterns/devcontainer-base:latest", "workspaceMount": "source=${localWorkspaceFolder},target=/workspace,type=bind", "workspaceFolder": "/workspace/data-binning/source/", - "postAttachCommand": "dotnet build Cosmos_Patterns_Bucketing.csproj", + "postAttachCommand": "dotnet build DataBinning.csproj", "customizations": { "codespaces": { "openFiles": [ diff --git a/.devcontainer/distributed-counter/devcontainer.json b/.devcontainer/distributed-counter/devcontainer.json index 9f1edeb..53d265a 100644 --- a/.devcontainer/distributed-counter/devcontainer.json +++ b/.devcontainer/distributed-counter/devcontainer.json @@ -3,7 +3,7 @@ "image": "ghcr.io/azure-samples/cosmos-db-design-patterns/devcontainer-base:latest", "workspaceMount": "source=${localWorkspaceFolder},target=/workspace,type=bind", "workspaceFolder": "/workspace/distributed-counter/source/", - "postAttachCommand": "dotnet build Cosmos_Patterns_DistributedCounter.sln", + "postAttachCommand": "dotnet build DistributedCounter.sln", "customizations": { "codespaces": { "openFiles": [ diff --git a/.devcontainer/distributed-lock/devcontainer.json b/.devcontainer/distributed-lock/devcontainer.json index b13486b..92375f5 100644 --- a/.devcontainer/distributed-lock/devcontainer.json +++ b/.devcontainer/distributed-lock/devcontainer.json @@ -3,7 +3,7 @@ "image": "ghcr.io/azure-samples/cosmos-db-design-patterns/devcontainer-base:latest", "workspaceMount": "source=${localWorkspaceFolder},target=/workspace,type=bind", "workspaceFolder": "/workspace/distributed-lock/source/", - "postAttachCommand": "dotnet build consoleapp/Cosmos_Patterns_GlobalLock.sln", + "postAttachCommand": "dotnet build GlobalLock.csproj", "customizations": { "codespaces": { "openFiles": [ diff --git a/.devcontainer/event-sourcing/devcontainer.json b/.devcontainer/event-sourcing/devcontainer.json index 2595075..701c912 100644 --- a/.devcontainer/event-sourcing/devcontainer.json +++ b/.devcontainer/event-sourcing/devcontainer.json @@ -3,7 +3,7 @@ "image": "ghcr.io/azure-samples/cosmos-db-design-patterns/devcontainer-base:latest", "workspaceMount": "source=${localWorkspaceFolder},target=/workspace,type=bind", "workspaceFolder": "/workspace/event-sourcing/source/", - "postAttachCommand": "dotnet build Cosmos_Patterns_EventSourcing.csproj", + "postAttachCommand": "dotnet build EventSourcing.csproj", "customizations": { "codespaces": { "openFiles": [ diff --git a/.devcontainer/preallocation/devcontainer.json b/.devcontainer/preallocation/devcontainer.json index 8c27aa7..bfb0d49 100644 --- a/.devcontainer/preallocation/devcontainer.json +++ b/.devcontainer/preallocation/devcontainer.json @@ -3,7 +3,7 @@ "image": "ghcr.io/azure-samples/cosmos-db-design-patterns/devcontainer-base:latest", "workspaceMount": "source=${localWorkspaceFolder},target=/workspace,type=bind", "workspaceFolder": "/workspace/preallocation/source/", - "postAttachCommand": "dotnet build Cosmos_Patterns_Preallocation.csproj", + "postAttachCommand": "dotnet build Preallocation.csproj", "customizations": { "codespaces": { "openFiles": [ diff --git a/README.md b/README.md index ed03fb0..11ac370 100644 --- a/README.md +++ b/README.md @@ -11,23 +11,14 @@ Design patterns play a crucial role in building robust applications and modeling ### Key Benefits of Using Design Patterns - **Efficiency and Best Practices**: Design patterns encapsulate proven solutions, saving you time and effort by leveraging established best practices. - - **Scalability and Performance**: Many patterns are optimized for scalability, ensuring your application can handle growth without compromising performance. - - **Consistency and Maintainability**: Patterns promote consistent architecture, making codebases easier to understand, maintain, and extend. - - **Reliability and Resilience**: Patterns address fault tolerance and error handling, resulting in applications that gracefully recover from failures. - - **Flexibility and Adaptability**: Patterns facilitate changes, enabling your application to evolve and adapt to new requirements seamlessly. - - **Reusability and Accelerated Development**: Patterns encourage reusable components, speeding up development and reducing the risk of bugs. - - **Effective Data Modeling**: In NoSQL databases like Azure Cosmos DB, choosing the right pattern ensures efficient data modeling for enhanced performance. - - **Documentation and Communication**: Patterns provide a shared vocabulary, aiding communication and collaboration among team members. - - **Adherence to Best Practices**: Design patterns ensure applications adhere to security, data integrity, and maintainability best practices. - - **Reduced Learning Curve**: Developers familiar with patterns quickly understand and contribute to projects, reducing onboarding time. @@ -74,15 +65,31 @@ Dive into the `schema-versioning` folder to learn how to manage changes to your ## Getting Started -Navigate to the individual folders of each design pattern for a dedicated `README.md` file that provides step-by-step instructions on how to implement and work with the pattern in your applications. +### Using the Terminal: +- Open the terminal on your computer. +- Navigate to the directory where you want to clone the repository. +- Type `git clone https://github.com/Azure-Samples/cosmos-db-design-patterns.git` and press enter. +- The repository will be cloned to your local machine. + +### Using Visual Studio Code: +- Open Visual Studio Code. +- Click on the **Source Control** icon in the left sidebar. +- Click on the **Clone Repository** button at the top of the Source Control panel. +- Paste `https://github.com/Azure-Samples/cosmos-db-design-patterns.git` into the text field and press enter. +- Select a directory where you want to clone the repository. +- The repository will be cloned to your local machine. + +### Using GitHub Codespaces + +Nearly all of these samples are configured to run from [GitHub Codespaces](https://docs.github.com/codespaces/overview). -### Trying Out the Design Patterns with Azure Cosmos DB for Free +Navigate to the individual folders of each design pattern for a dedicated `README.md` file and look for the GitHub Codespaced badge. -You can try out these design patterns using a **free Azure Cosmos DB account**, making it easy to experiment with Azure Cosmos DB before making a commitment. No credit card is required to get started, and your account is free for 30 days. After the initial 30-day period, you can create a new sandbox account. Additionally, you have the option to extend the trial beyond 30 days for an additional 24 hours. If you decide to upgrade, you can do so at any time during the 30-day trial period. +### Setting up Azure Cosmos DB -**Sign up for your free Azure Cosmos DB account at [aka.ms/trycosmosdb](https://aka.ms/trycosmosdb).** +All of these design patterns are built to run from a single Serverless Azure Cosmos DB account. Before running any of the samples, click the Deploy to Azure button below to create a Serverless Azure Cosmos DB account. You will need the URI Primary Key and Connection String for these. Keep those handy as you prepare each sample to run. -This opportunity provides a risk-free environment to explore these design patterns and see how Azure Cosmos DB can enhance your application development and data modeling efforts. Whether you're an experienced developer or just getting started, the free trial allows you to discover the benefits firsthand. +[![Deploy to Azure](https://aka.ms/deploytoazurebutton)](https://portal.azure.com/#create/Microsoft.Template/uri/https%3A%2F%2Fgithub.com%2FAzureCosmosDB%2Fdesign-patterns%2Ftree%2Fmain%2Fazuredeploy.json) Happy coding with Azure Cosmos DB and these powerful design patterns! diff --git a/attribute-array/README.md b/attribute-array/README.md index 5be4a65..e1616af 100644 --- a/attribute-array/README.md +++ b/attribute-array/README.md @@ -133,7 +133,7 @@ If a user adds new sizes or even removes them. The same query will run unmodifie In order to run the demos, you will need: -- [.NET 6.0 Runtime](https://dotnet.microsoft.com/download/dotnet/6.0) +- [.NET 8.0 Runtime](https://dotnet.microsoft.com/download/) - [Azure Functions Core Tools v4](https://learn.microsoft.com/azure/azure-functions/functions-run-local#install-the-azure-functions-core-tools) ## Confirm required tools are installed @@ -176,38 +176,19 @@ You can try out this implementation by running the code in [GitHub Codespaces](h [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/azure-samples/cosmos-db-design-patterns?quickstart=1&devcontainer_path=.devcontainer%2Fattribute-array%2Fdevcontainer.json) -## Create an Azure Cosmos DB for NoSQL account +## Set up application configuration files -1. Create a free Azure Cosmos DB for NoSQL account: () +You need to configure **two** application configuration files to run these demos. -1. In the Data Explorer, create a new database named **CosmosPatterns** with shared autoscale throughput: +1. Go to your resource group. - | | Value | - | --- | --- | - | **Database name** | `CosmosPatterns` | - | **Throughput** | `1000` (*Autoscale*) | +1. Select the Serverless Azure Cosmos DB for NoSQL account that you created for this repository. -**Note:** We are using shared database throughput because it can scale down to 100 RU/s when not running. This is the most cost efficient if running in a paid subscription and not using Free Tier. +1. From the navigation, under **Settings**, select **Keys**. The values you need for the application settings for the demo are here. -1. Create a container **AttributeArrays** with the following values: +While on the Keys blade, make note of the `URI` and `PRIMARY KEY`. You will need these for the sections below. - | | Value | - | --- | --- | - | **Database name** | `CosmosPatterns` | - | **Container name** | `AttributeArrays` | - | **Partition key path** | `/productId` | - -## Get Azure Cosmos DB connection information - -You will need connection details for the Azure Cosmos DB account. - -1. Select the new Azure Cosmos DB for NoSQL account. - -1. Open the Keys blade, click the Eye icon to view the `PRIMARY KEY`. Keep this and the `URI` handy. You will need these for the next step. - -## Prepare the app configuration - -1. Open the application code, create an **appsettings.Development.json** file in the **/source** folder. In the file, create a JSON object with **CosmosUri** and **CosmosKey** properties. Copy and paste the values for `URI` and `PRIMARY KEY` from the previous step: +1. Open the attribute-array project and add a new **appsettings.development.json** file with the following contents: ```json { @@ -216,6 +197,20 @@ You will need connection details for the Azure Cosmos DB account. } ``` +1. Replace the `CosmosURI` and `CosmosKey` with the values from the Keys blade in the Azure Portal. +1. Modify the **Copy to Output Directory** to **Copy Always** (For VS Code add the XML below to the csproj file) +1. Save the file. + + ```xml + + + Always + + + ``` + +## Run the app + 1. Open a terminal and run the application: ```bash diff --git a/attribute-array/source/Program.cs b/attribute-array/source/Program.cs index 8126bfb..94ca31e 100644 --- a/attribute-array/source/Program.cs +++ b/attribute-array/source/Program.cs @@ -9,7 +9,7 @@ IConfigurationBuilder configuration = new ConfigurationBuilder() .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true) - .AddJsonFile($"appsettings.Development.json", optional: true); + .AddJsonFile($"appsettings.development.json", optional: true); Cosmos? config = configuration .Build() @@ -37,8 +37,7 @@ ); Database database = await client.CreateDatabaseIfNotExistsAsync( - id: "CosmosPatterns", - throughputProperties: ThroughputProperties.CreateAutoscaleThroughput(1000) + id: "AttributeArrayDB" ); Console.Write( diff --git a/azuredeploy.json b/azuredeploy.json new file mode 100644 index 0000000..61f7a3d --- /dev/null +++ b/azuredeploy.json @@ -0,0 +1,42 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "location": { + "type": "string", + "defaultValue": "[resourceGroup().location]", + "metadata": { + "description": "Location for all resources." + } + }, + "accountName": { + "type": "string", + "metadata": { + "description": "Azure Cosmos DB account name, max length 44 characters" + } + } + }, + "resources": [ + { + "type": "Microsoft.DocumentDB/databaseAccounts", + "apiVersion": "2022-05-15", + "name": "[toLower(parameters('accountName'))]", + "kind": "GlobalDocumentDB", + "location": "[parameters('location')]", + "properties": { + "databaseAccountOfferType": "Standard", + "locations": [ + { + "locationName": "[parameters('location')]", + "failoverPriority": 0 + } + ], + "capabilities": [ + { + "name": "EnableServerless" + } + ] + } + } + ] +} \ No newline at end of file diff --git a/data-binning/README.md b/data-binning/README.md index 103f615..7bbcb10 100644 --- a/data-binning/README.md +++ b/data-binning/README.md @@ -132,7 +132,7 @@ Note: In the demo application, aggregated events are collected based on system t In order to run the demos, you will need: -- [.NET 6.0 Runtime](https://dotnet.microsoft.com/download/dotnet/6.0) +- [.NET 8.0 Runtime](https://dotnet.microsoft.com/download/) ## Confirm required tools are installed @@ -174,38 +174,19 @@ You can try out this implementation by running the code in [GitHub Codespaces](h [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/azure-samples/cosmos-db-design-patterns?quickstart=1&devcontainer_path=.devcontainer%2Fdata-binning%2Fdevcontainer.json) -## Create an Azure Cosmos DB for NoSQL account +## Set up application configuration files -1. Create a free Azure Cosmos DB for NoSQL account: () +You need to configure an application configuration file to run this app. -1. In the Data Explorer, create a new database named **CosmosPatterns** with shared autoscale throughput: +1. Go to your resource group. - | | Value | - | --- | --- | - | **Database name** | `CosmosPatterns` | - | **Throughput** | `1000` (*Autoscale*) | +1. Select the Serverless Azure Cosmos DB for NoSQL account that you created for this repository. -**Note:** We are using shared database throughput because it can scale down to 100 RU/s when not running. This is the most cost efficient when running at very small scale. +1. From the navigation, under **Settings**, select **Keys**. The values you need for the application settings for the demo are here. -1. Create a container **DataBinning** with the following values: +While on the Keys blade, make note of the `URI` and `PRIMARY KEY`. You will need these for the sections below. - | | Value | - | --- | --- | - | **Database name** | `CosmosPatterns` | - | **Container name** | `DataBinning` | - | **Partition key path** | `/DeviceId` | - -## Get Azure Cosmos DB connection information - -You will need connection details for the Azure Cosmos DB account. - -1. Select the new Azure Cosmos DB for NoSQL account. - -1. Open the Keys blade, click the Eye icon to view the `PRIMARY KEY`. Keep this and the `URI` handy. You will need these for the next step. - -## Prepare the app configuration - -1. Open the application code, create an **appsettings.Development.json** file in the **/source** folder. In the file, create a JSON object with **CosmosUri** and **CosmosKey** properties. Copy and paste the values for `URI` and `PRIMARY KEY` from the previous step: +1. Open the data-binning project and add a new **appsettings.development.json** file with the following contents: ```json { @@ -214,6 +195,18 @@ You will need connection details for the Azure Cosmos DB account. } ``` +1. Replace the `CosmosURI` and `CosmosKey` with the values from the Keys blade in the Azure Portal. +1. Modify the **Copy to Output Directory** to **Copy Always** (For VS Code add the XML below to the csproj file) +1. Save the file. + + ```xml + + + Always + + + ``` + ## Run the demo Open a new terminal and run the included Console App (Program.cs) which generates events, saves them bucketed by device and minute: diff --git a/data-binning/source/.gitignore b/data-binning/source/.gitignore deleted file mode 100644 index ceb7d4f..0000000 --- a/data-binning/source/.gitignore +++ /dev/null @@ -1,267 +0,0 @@ -## Ignore Visual Studio temporary files, build results, and -## files generated by popular Visual Studio add-ons. - -# Azure Functions localsettings file -local.settings.json - -# User-specific files -*.suo -*.user -*.userosscache -*.sln.docstates - -# User-specific files (MonoDevelop/Xamarin Studio) -*.userprefs - -# Build results -[Dd]ebug/ -[Dd]ebugPublic/ -[Rr]elease/ -[Rr]eleases/ -x64/ -x86/ -bld/ -[Bb]in/ -[Oo]bj/ -[Ll]og/ - -# VS Code -.vscode/ - -# Visual Studio 2015 cache/options directory -.vs/ -# Uncomment if you have tasks that create the project's static files in wwwroot -#wwwroot/ - -# MSTest test Results -[Tt]est[Rr]esult*/ -[Bb]uild[Ll]og.* - -# NUNIT -*.VisualState.xml -TestResult.xml - -# Build Results of an ATL Project -[Dd]ebugPS/ -[Rr]eleasePS/ -dlldata.c - -# DNX -project.lock.json -project.fragment.lock.json -artifacts/ - -*_i.c -*_p.c -*_i.h -*.ilk -*.meta -*.obj -*.pch -*.pdb -*.pgc -*.pgd -*.rsp -*.sbr -*.tlb -*.tli -*.tlh -*.tmp -*.tmp_proj -*.log -*.vspscc -*.vssscc -.builds -*.pidb -*.svclog -*.scc - -# Chutzpah Test files -_Chutzpah* - -# Visual C++ cache files -ipch/ -*.aps -*.ncb -*.opendb -*.opensdf -*.sdf -*.cachefile -*.VC.db -*.VC.VC.opendb - -# Visual Studio profiler -*.psess -*.vsp -*.vspx -*.sap - -# TFS 2012 Local Workspace -$tf/ - -# Guidance Automation Toolkit -*.gpState - -# ReSharper is a .NET coding add-in -_ReSharper*/ -*.[Rr]e[Ss]harper -*.DotSettings.user - -# JustCode is a .NET coding add-in -.JustCode - -# TeamCity is a build add-in -_TeamCity* - -# DotCover is a Code Coverage Tool -*.dotCover - -# NCrunch -_NCrunch_* -.*crunch*.local.xml -nCrunchTemp_* - -# MightyMoose -*.mm.* -AutoTest.Net/ - -# Web workbench (sass) -.sass-cache/ - -# Installshield output folder -[Ee]xpress/ - -# DocProject is a documentation generator add-in -DocProject/buildhelp/ -DocProject/Help/*.HxT -DocProject/Help/*.HxC -DocProject/Help/*.hhc -DocProject/Help/*.hhk -DocProject/Help/*.hhp -DocProject/Help/Html2 -DocProject/Help/html - -# Click-Once directory -publish/ - -# Publish Web Output -*.[Pp]ublish.xml -*.azurePubxml -# TODO: Comment the next line if you want to checkin your web deploy settings -# but database connection strings (with potential passwords) will be unencrypted -#*.pubxml -*.publishproj - -# Microsoft Azure Web App publish settings. Comment the next line if you want to -# checkin your Azure Web App publish settings, but sensitive information contained -# in these scripts will be unencrypted -PublishScripts/ - -# NuGet Packages -*.nupkg -# The packages folder can be ignored because of Package Restore -**/packages/* -# except build/, which is used as an MSBuild target. -!**/packages/build/ -# Uncomment if necessary however generally it will be regenerated when needed -#!**/packages/repositories.config -# NuGet v3's project.json files produces more ignoreable files -*.nuget.props -*.nuget.targets - -# Microsoft Azure Build Output -csx/ -*.build.csdef - -# Microsoft Azure Emulator -ecf/ -rcf/ - -# Windows Store app package directories and files -AppPackages/ -BundleArtifacts/ -Package.StoreAssociation.xml -_pkginfo.txt - -# Visual Studio cache files -# files ending in .cache can be ignored -*.[Cc]ache -# but keep track of directories ending in .cache -!*.[Cc]ache/ - -# Others -ClientBin/ -~$* -*~ -*.dbmdl -*.dbproj.schemaview -*.jfm -*.pfx -*.publishsettings -node_modules/ -orleans.codegen.cs - -# Since there are multiple workflows, uncomment next line to ignore bower_components -# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) -#bower_components/ - -# RIA/Silverlight projects -Generated_Code/ - -# Backup & report files from converting an old project file -# to a newer Visual Studio version. Backup files are not needed, -# because we have git ;-) -_UpgradeReport_Files/ -Backup*/ -UpgradeLog*.XML -UpgradeLog*.htm - -# SQL Server files -*.mdf -*.ldf - -# Business Intelligence projects -*.rdl.data -*.bim.layout -*.bim_*.settings - -# Microsoft Fakes -FakesAssemblies/ - -# GhostDoc plugin setting file -*.GhostDoc.xml - -# Node.js Tools for Visual Studio -.ntvs_analysis.dat - -# Visual Studio 6 build log -*.plg - -# Visual Studio 6 workspace options file -*.opt - -# Visual Studio LightSwitch build output -**/*.HTMLClient/GeneratedArtifacts -**/*.DesktopClient/GeneratedArtifacts -**/*.DesktopClient/ModelManifest.xml -**/*.Server/GeneratedArtifacts -**/*.Server/ModelManifest.xml -_Pvt_Extensions - -# Paket dependency manager -.paket/paket.exe -paket-files/ - -# FAKE - F# Make -.fake/ - -# JetBrains Rider -.idea/ -*.sln.iml - -# CodeRush -.cr/ - -# Python Tools for Visual Studio (PTVS) -__pycache__/ -*.pyc diff --git a/data-binning/source/DataBinning.csproj b/data-binning/source/DataBinning.csproj index 4c21ca6..f2c946e 100644 --- a/data-binning/source/DataBinning.csproj +++ b/data-binning/source/DataBinning.csproj @@ -13,7 +13,7 @@ - + Always diff --git a/data-binning/source/Program.cs b/data-binning/source/Program.cs index 2470f9d..0928016 100644 --- a/data-binning/source/Program.cs +++ b/data-binning/source/Program.cs @@ -54,19 +54,18 @@ static async Task Main(string[] args) //wait till all threads complete. await Task.Delay((durationSec +2) * 1000); - System.Console.WriteLine($"Completed generation events for {deviceCount} devices"); + Console.WriteLine($"Completed generation events for {deviceCount} devices"); Console.WriteLine($"Check DataBinning Container for sensor events"); } private async static Task createCosmosDBArtifactsAsync() { - string partitionKeyPath = "/DeviceId"; - + Console.WriteLine($"Please wait while Cosmos DB database and container is created."); var configuration = new ConfigurationBuilder() .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true) - .AddJsonFile($"appsettings.Development.json", optional: true); + .AddJsonFile($"appsettings.development.json", optional: true); var config = configuration.Build(); @@ -76,10 +75,10 @@ private async static Task createCosmosDBArtifactsAsync() CosmosClient client = new(accountEndpoint: uri, authKeyOrResourceToken: key); Database database = await client.CreateDatabaseIfNotExistsAsync( - id: "CosmosPatterns", - throughputProperties: ThroughputProperties.CreateAutoscaleThroughput(1000) + id: "DataBinningDB" ); + string partitionKeyPath = "/DeviceId"; Container container = await database.CreateContainerIfNotExistsAsync( id: "DataBinning", partitionKeyPath: partitionKeyPath diff --git a/distributed-counter/README.md b/distributed-counter/README.md index 0333396..9d0b441 100644 --- a/distributed-counter/README.md +++ b/distributed-counter/README.md @@ -38,17 +38,13 @@ This sample is implemented as a C#/.NET application with three projects. The thr - **Counter** class library: - This class library implements the distributed counter pattern using two services. - - The `DistributedCounterManagementService` creates the counter and manages on-demand splitting or merging. - - The `DistributedCounterOperationalService` updates the counters in a high traffic workload. This service picks a random distributed counter from the pool of available counters and updates the counter using a partial document update (or HTTP PATCH request). This service's implementation ensures that there are no conflicts to updating the counter and each counter update is an atomic transaction. - **Visualizer** web application: - This Blazor web application renders a visual interface for the distributed counters. - - The web application uses graphical charts to illustrate how the counters are performing in real-time. - - The web application polls the `DistributedCounterManagementService` for the data rendered in the chart. ![Screenshot of the Blazor web application with a chart visualizing the various distributed counters.](media/distributed-counter-chart-visualization.png) @@ -56,9 +52,7 @@ This sample is implemented as a C#/.NET application with three projects. The thr - **Consumers** console application: - This console application mimics a high traffic workload. - - The console application creates multiple concurrent threads. Each thread runs in a loop to update the distributed counters quickly. - - The console application uses the `DistributedCounterOperationalService` to update the counters. ```output @@ -74,7 +68,7 @@ This sample is implemented as a C#/.NET application with three projects. The thr In order to run the demos, you will need: -- [.NET 6.0 Runtime](https://dotnet.microsoft.com/download/dotnet/6.0) +- [.NET 8.0 Runtime](https://dotnet.microsoft.com/download/) - [Azure Functions Core Tools](https://learn.microsoft.com/azure/azure-functions/functions-run-local#install-the-azure-functions-core-tools) ## Confirm required tools are installed @@ -87,7 +81,7 @@ First, check the .NET runtime with this command: dotnet --list-runtimes ``` -As you may have multiple versions of the runtime installed, make sure that .NET components with versions that start with 6.0 appear as part of the output. +As you may have multiple versions of the runtime installed, make sure that .NET components with versions that start with 8.0 appear as part of the output. Next, check the version of Azure Functions Core Tools with this command: @@ -125,42 +119,78 @@ You can try out this implementation by running the code in [GitHub Codespaces](h [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/azure-samples/cosmos-db-design-patterns?quickstart=1&devcontainer_path=.devcontainer%2Fdistributed-counter%2Fdevcontainer.json) -## Create an Azure Cosmos DB for NoSQL account - -1. Create a free Azure Cosmos DB for NoSQL account: () +## Set up application configuration files -1. In the Data Explorer, create a new database and container with the following values with shared autoscale throughput: +You need to configure **two** application configuration files to run this app. - | | Value | - | --- | --- | - | **Database name** | `CounterDB` | - | **Container name** | `Counters` | - | **Partition key path** | `/pk` | - | **Throughput** | `1000` (_AutoScale_) | +1. Go to your resource group. -**Note:** We are using shared database throughput because it can scale down to 100 RU/s when not running. This is the most cost efficient if running in a paid subscription and not using Free Tier. +1. Select the Serverless Azure Cosmos DB for NoSQL account that you created for this repository. -## Get Azure Cosmos DB connection information +1. From the navigation, under **Settings**, select **Keys**. The values you need for the application settings for the demo are here. -You will need a connection string for the Azure Cosmos DB account. +While on the Keys blade, make note of the `URI` and `PRIMARY KEY`. You will need these for the sections below. -1. Go to resource group +1. Open the Visualizer project and add a new **appsettings.development.json** file with the following contents: -1. Select the new Azure Cosmos DB for NoSQL account. + ```json + { + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + }, + "AllowedHosts": "*", + "CosmosUri": "", + "CosmosKey": "", + "CosmosDatabase": "CounterDB", + "CosmosContainer": "Counters", + "DetailedErrors": true + } + ``` -1. Open the Keys blade, click the Eye icon to view the `PRIMARY KEY`. Keep this and the `URI` handy. You will need these for the next step. +1. Replace the `CosmosURI` and `CosmosKey` with the values from the Keys blade in the Azure Portal. +1. Modify the **Copy to Output Directory** to **Copy Always** (For VS Code add the XML below to the csproj file) +1. Save the file. -## Prepare the app configuration +Next move to the other project. -1. Open the code, create an **appsettings.Development.json** file in both the **/Visualizer** and **/ConsumerApp** folders. In each of the files, create a JSON object with **CosmosUri** and **CosmosKey** properties. Copy and paste the values for `URI` and `PRIMARY KEY` from the previous step: +1. Open the ConsumerApp project and add a new **appsettings.development.json** file with the following contents: ```json { - "CosmosUri": "", - "CosmosKey": "" + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + }, + "AllowedHosts": "*", + "CosmosUri": "", + "CosmosKey": "", + "CosmosDatabase": "CounterDB", + "CosmosContainer": "Counters", + "DetailedErrors": true } ``` +1. Replace the `CosmosURI` and `CosmosKey` with the values from the Keys blade in the Azure Portal. +1. Modify the **Copy to Output Directory** to **Copy Always** (For VS Code add the XML below to the csproj file) +1. Save the file. + + ```xml + + + Always + + + ``` + +## Run the demo locally + 1. Open a terminal and run the web application. The web application opens in a new browser window. ```bash diff --git a/distributed-counter/source/ConsumerApp/DistributedCounterConsumerApp.csproj b/distributed-counter/source/ConsumerApp/DistributedCounterConsumerApp.csproj index cd12c58..e501bf3 100644 --- a/distributed-counter/source/ConsumerApp/DistributedCounterConsumerApp.csproj +++ b/distributed-counter/source/ConsumerApp/DistributedCounterConsumerApp.csproj @@ -13,6 +13,8 @@ true PreserveNewest + + PreserveNewest true diff --git a/distributed-counter/source/ConsumerApp/Program.cs b/distributed-counter/source/ConsumerApp/Program.cs index 591ba09..787ee65 100644 --- a/distributed-counter/source/ConsumerApp/Program.cs +++ b/distributed-counter/source/ConsumerApp/Program.cs @@ -17,14 +17,14 @@ static async Task Main(string[] args) var configuration = new ConfigurationBuilder() .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true) - .AddJsonFile($"appsettings.{Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT")}.json", optional: true); + .AddJsonFile($"appsettings.development.json", optional: true); var config = configuration.Build(); - string endpoint = config["CosmosUri"]; - string key = config["CosmosKey"]; - string databaseName = config["CosmosDatabase"]; - string containerName = config["CosmosContainer"]; + string endpoint = config["CosmosUri"]!; + string key = config["CosmosKey"]!; + string databaseName = config["CosmosDatabase"]!; + string containerName = config["CosmosContainer"]!; dcos = new DistributedCounterOperationalService(endpoint, key, databaseName, containerName); diff --git a/distributed-counter/source/ConsumerApp/appsettings.json b/distributed-counter/source/ConsumerApp/appsettings.json index 8a2a437..42d58eb 100644 --- a/distributed-counter/source/ConsumerApp/appsettings.json +++ b/distributed-counter/source/ConsumerApp/appsettings.json @@ -11,7 +11,4 @@ "CosmosKey": "", "CosmosDatabase": "CounterDB", "CosmosContainer": "Counters" - - - } diff --git a/distributed-counter/source/Counter/CosmosService.cs b/distributed-counter/source/Counter/CosmosService.cs index ecb52b0..97da9c1 100644 --- a/distributed-counter/source/Counter/CosmosService.cs +++ b/distributed-counter/source/Counter/CosmosService.cs @@ -4,6 +4,7 @@ using System.ComponentModel; using Microsoft.Azure.Cosmos.Linq; using Newtonsoft.Json.Linq; +using Container = Microsoft.Azure.Cosmos.Container; namespace CosmosDistributedLock.Services { @@ -12,7 +13,7 @@ public class CosmosService { private readonly CosmosClient client; private readonly Database db; - private readonly Microsoft.Azure.Cosmos.Container container; + private readonly Container container; public CosmosService(string CosmosUri, string CosmosKey, string CosmosDatabase, string CosmosContainer) @@ -22,9 +23,9 @@ public CosmosService(string CosmosUri, string CosmosKey, string CosmosDatabase, accountEndpoint: CosmosUri, authKeyOrResourceToken: CosmosKey); - db = client.GetDatabase(CosmosDatabase); - - container = db.GetContainer(CosmosContainer); + + db = client.CreateDatabaseIfNotExistsAsync(CosmosDatabase).Result; + container = db.CreateContainerIfNotExistsAsync(CosmosContainer, "/pk").Result; } diff --git a/distributed-counter/source/Cosmos_Patterns_DistributedCounter.sln b/distributed-counter/source/DistributedCounter.sln similarity index 100% rename from distributed-counter/source/Cosmos_Patterns_DistributedCounter.sln rename to distributed-counter/source/DistributedCounter.sln diff --git a/distributed-counter/source/SETUP.md b/distributed-counter/source/SETUP.md deleted file mode 100644 index 6da75a5..0000000 --- a/distributed-counter/source/SETUP.md +++ /dev/null @@ -1,65 +0,0 @@ -# Setup - -This template will create an Azure Cosmos DB for NoSQL account with a database named `CounterDB` with a container named `Counters`. - -The suggested account name includes 'YOUR_SUFFIX'. Change that to a suffix to make your account name unique. - -The Azure Cosmos DB for NoSQL account will automatically be created with the region of the selected resource group. - ---- - -**This link will work if this is a public repo.** - -[![Deploy to Azure](https://aka.ms/deploytoazurebutton)](https://portal.azure.com/#create/Microsoft.Template/uri/https%3A%2F%2Fgithub.com%2FAzureCosmosDB%2Fdesign-patterns%2Ftree%2Fmain%2Fdistributed-counter%2Fsource%2Fazuredeploy.json) - -**For the private repo** - -1. [Create a custom template deployment](https://portal.azure.com/#create/Microsoft.Template/). -2. Select **Build your own template in the editor**. -3. Copy the contents from [this template](azuredeploy.json) into the editor. -4. Select **Save**. - ---- - -Once the template is loaded, populate the values: - -- **Subscription** - Choose a subscription. -- **Resource group** - Choose a resource group. -- **Region** - Select a region for the instance. -- **Location** - Enter a location for the Azure Cosmos DB for NoSQL account. **Note**: By default, it is set to use the location of the resource group. If you need to change this value, you can find the supported regions for your subscription via: - - [Azure CLI](https://learn.microsoft.com/cli/azure/account?view=azure-cli-latest#az-account-list-locations) - - PowerShell: `Get-AzLocation | Where-Object {$_.Providers -contains "Microsoft.DocumentDB"} | Select location` -- **Account Name** - Replace `YOUR_SUFFIX` with a suffix to make your Azure Cosmos DB account name unique. -- **Database Name** - Set to the default **CounterDB**. -- **Container Name** - Set to default **Counters**, is partitioned by `/pk`. -- **Throughput** - Set to the default **400**. -- **Enable Free Tier** - This defaults to `false`. Set it to **true** if you want to use it as [the free tier account](https://learn.microsoft.com/azure/cosmos-db/free-tier). - -Once those settings are set, select **Review + create**, then **Create**. - -## Updating Azure Cosmos DB URI and Key in Code - -1. Once the template deployment is complete, select **Go to resource group**. -2. Select the new Azure Cosmos DB for NoSQL account. -3. From the navigation, under **Settings**, select **Keys**. - -Update the following in the **ConsumerApp/appsettings.json** and **Visualizer/appsettings.json** before you run the code: - -- `CosmosUri`: Set to the `URI` value on the Azure Cosmos DB account Keys blade. -- `CosmosKey`: Set to the Read-Write `PRIMARY KEY` for the Azure Cosmos DB for NoSQL account - -## Run the demo - -1. In Visual Studio load the **DistributeCounter.sln** -1. Press **F5** . It will to run 2 projects simultaneously. - 1. Visualizer web app - 1. ConsumerApp console application -1. In the **Visualizer** web app - 1. Select 'Create Counter' to create the distributed counters. - ![Screenshot showing the Create Counter in Visualizer](media/createcounter.png) - 1. Copy the Distributed Counter Id when the counters are ready. - ![Screenshot showing the Counter Id in Visualizer](media/copycounterid.png) -1. In the **ConsumerApp** console application, paste the value of Distributed Counter Id copied in previous step. Provide the number of worker threads you want for update the counters. -![Screenshot showing the Console App](media/consoleapp.png) -1. Switch back to the **Visualizer** web app, you should see your counters values change as they get updated by the worker threads of **ConsumerApp** . -![Screenshot showing the Visualizer](media/visualizer.png) diff --git a/distributed-counter/source/Visualizer/appsettings.json b/distributed-counter/source/Visualizer/appsettings.json index 33dfd3a..4810af5 100644 --- a/distributed-counter/source/Visualizer/appsettings.json +++ b/distributed-counter/source/Visualizer/appsettings.json @@ -12,6 +12,4 @@ "CosmosDatabase": "CounterDB", "CosmosContainer": "Counters", "DetailedErrors": true - - } diff --git a/distributed-counter/source/azuredeploy.json b/distributed-counter/source/azuredeploy.json deleted file mode 100644 index cb31f72..0000000 --- a/distributed-counter/source/azuredeploy.json +++ /dev/null @@ -1,115 +0,0 @@ -{ - "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", - "contentVersion": "1.0.0.0", - "parameters": { - "location": { - "type": "string", - "defaultValue": "[resourceGroup().location]", - "metadata": { - "description": "Location for all resources." - } - }, - "accountName": { - "type": "string", - "defaultValue": "distributedcounter-your_suffix", - "metadata": { - "description": "Azure Cosmos DB account name, max length 44 characters" - } - }, - "databaseName": { - "type": "string", - "defaultValue": "CounterDB", - "metadata": { - "description": "The name for the database" - } - }, - "containerName": { - "type": "string", - "defaultValue": "Counters", - "metadata": { - "description": "The name for the container" - } - }, - "throughput": { - "type": "int", - "defaultValue": 400, - "metadata": { - "description": "The throughput for the container" - }, - "maxValue": 1000000, - "minValue": 400 - }, - "enableFreeTier": { - "type": "bool", - "defaultValue": false - } - }, - "resources": [ - { - "type": "Microsoft.DocumentDB/databaseAccounts", - "apiVersion": "2022-05-15", - "name": "[toLower(parameters('accountName'))]", - "kind": "GlobalDocumentDB", - "location": "[parameters('location')]", - "properties": { - "databaseAccountOfferType": "Standard", - "enableFreeTier": "[parameters('enableFreeTier')]", - "locations": [ - { - "locationName": "[parameters('location')]", - "failoverPriority": 0 - } - ] - } - }, - { - "type": "Microsoft.DocumentDB/databaseAccounts/sqlDatabases", - "apiVersion": "2022-05-15", - "name": "[format('{0}/{1}', toLower(parameters('accountName')), parameters('databaseName'))]", - "properties": { - "resource": { - "id": "[parameters('databaseName')]" - } - }, - "dependsOn": [ - "[resourceId('Microsoft.DocumentDB/databaseAccounts', toLower(parameters('accountName')))]" - ] - }, - { - "type": "Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers", - "apiVersion": "2022-05-15", - "name": "[format('{0}/{1}', format('{0}/{1}', toLower(parameters('accountName')), parameters('databaseName')), parameters('containerName'))]", - "properties": { - "resource": { - "id": "[parameters('containerName')]", - "partitionKey": { - "paths": [ - "/pk" - ], - "kind": "Hash" - }, - "indexingPolicy": { - "indexingMode": "consistent", - "includedPaths": [ - { - "path": "/*" - } - ], - "excludedPaths": [ - { - "path": "/_etag/?" - } - ] - }, - "defaultTtl": 86400 - }, - "options": { - "throughput": "[parameters('throughput')]" - } - }, - "dependsOn": [ - "[resourceId('Microsoft.DocumentDB/databaseAccounts/sqlDatabases', split(format('{0}/{1}', toLower(parameters('accountName')), parameters('databaseName')), '/')[0], split(format('{0}/{1}', toLower(parameters('accountName')), parameters('databaseName')), '/')[1])]" - ] - } - ] -} \ No newline at end of file diff --git a/distributed-lock/README.md b/distributed-lock/README.md index 945ddac..4745c5c 100644 --- a/distributed-lock/README.md +++ b/distributed-lock/README.md @@ -46,7 +46,7 @@ The TTL feature is used to automatically get rid of a lease object rather than h In order to run the demos, you will need: -- [.NET 6.0 Runtime](https://dotnet.microsoft.com/download/dotnet/6.0) +- [.NET 8.0 Runtime](https://dotnet.microsoft.com/) ## Confirm required tools are installed @@ -86,34 +86,21 @@ You can try out this implementation by running the code in [GitHub Codespaces](h - Open the application code in a GitHub Codespace: - [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/azure-samples/cosmos-db-design-patterns?quickstart=1&devcontainer_path=.devcontainer%2Fschema-versioning%2Fdevcontainer.json) + [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/azure-samples/cosmos-db-design-patterns?quickstart=1&devcontainer_path=.devcontainer%2Fdistributed-lock%2Fdevcontainer.json) -## Create an Azure Cosmos DB for NoSQL account +## Set up application configuration files -1. Create a free Azure Cosmos DB for NoSQL account: () +You need to configure an application configuration file to run this app. -1. In the Data Explorer, create a new databased named **LockDB** with with shared autoscale throughput: +1. Go to your resource group. - | | Value | - | --- | --- | - | **Database name** | `LockDB` | - | **Throughput** | `1000` (*Autoscale*) | +1. Select the Serverless Azure Cosmos DB for NoSQL account that you created for this repository. -**Note:** We are using shared database throughput because it can scale down to 100 RU/s when not running. This is the most cost efficient if running in a paid subscription and not using Free Tier. +1. From the navigation, under **Settings**, select **Keys**. The values you need for the application settings for the demo are here. -1. Create a container named **Locks** container with the following values: +While on the Keys blade, make note of the `URI` and `PRIMARY KEY`. You will need these for the sections below. - | | Value | - | --- | --- | - | **Database name** | `LockDB` | - | **Container name** | `Locks` | - | **Partition key path** | `/id` | - -1. Open the Keys blade, click the Eye icon to view the `PRIMARY KEY`. Keep this and the `URI` handy. You will need these for the next step. - -## Configure the application - -1. Open the application code, create an **appsettings.Development.json** file in the **/source** folder. In the file, create a JSON object with **CosmosUri** and **CosmosKey** properties. Copy and paste the values for `URI` and `PRIMARY KEY` from the previous step: +1. Open the distributed-lock project and add a new **appsettings.development.json** file with the following contents: ```json { @@ -134,14 +121,28 @@ You can try out this implementation by running the code in [GitHub Codespaces](h } ``` -1. In the codespace, open a terminal and run the application: +1. Replace the `CosmosURI` and `CosmosKey` with the values from the Keys blade in the Azure Portal. +1. Modify the **Copy to Output Directory** to **Copy Always** (For VS Code add the XML below to the csproj file) +1. Save the file. -```bash -dotnet run -``` + ```xml + + + Always + + + ``` + +## Run the demo locally + +1. At a command prompt or VS Code Terminal, switch to the `source` folder and run the app with: + + ```dotnetcli + dotnet run + ``` 1. When prompted, enter the values for the lock name and the default TTL ## Summary -Azure Cosmos DB makes implementing global lock leases fairly simple by utilizing the `TTL` and 'ETag' features. +Azure Cosmos DB makes implementing a global lock fairly simple by utilizing the `TTL` and 'ETag' features. diff --git a/distributed-lock/source/consoleapp/CosmosService.cs b/distributed-lock/source/CosmosService.cs similarity index 97% rename from distributed-lock/source/consoleapp/CosmosService.cs rename to distributed-lock/source/CosmosService.cs index 5d6add5..ac677e5 100644 --- a/distributed-lock/source/consoleapp/CosmosService.cs +++ b/distributed-lock/source/CosmosService.cs @@ -37,9 +37,8 @@ public async Task InitDatabaseAsync() PartitionKeyPath = "/id", DefaultTimeToLive = 60 //seconds }; - ThroughputProperties throughputProperties = ThroughputProperties.CreateManualThroughput(400); - _container = await database.CreateContainerIfNotExistsAsync(containerProperties, throughputProperties); + _container = await database.CreateContainerIfNotExistsAsync(containerProperties); } public async Task CreateUpdateLeaseAsync(string ownerId, int leaseDuration) diff --git a/distributed-lock/source/consoleapp/DistributedLockService.cs b/distributed-lock/source/DistributedLockService.cs similarity index 100% rename from distributed-lock/source/consoleapp/DistributedLockService.cs rename to distributed-lock/source/DistributedLockService.cs diff --git a/distributed-lock/source/consoleapp/GlobalLock.csproj b/distributed-lock/source/GlobalLock.csproj similarity index 89% rename from distributed-lock/source/consoleapp/GlobalLock.csproj rename to distributed-lock/source/GlobalLock.csproj index f998d16..d1eb4e0 100644 --- a/distributed-lock/source/consoleapp/GlobalLock.csproj +++ b/distributed-lock/source/GlobalLock.csproj @@ -22,6 +22,9 @@ + + Always + Always diff --git a/distributed-lock/source/consoleapp/LockManager.cs b/distributed-lock/source/LockManager.cs similarity index 100% rename from distributed-lock/source/consoleapp/LockManager.cs rename to distributed-lock/source/LockManager.cs diff --git a/distributed-lock/source/consoleapp/LockTest.cs b/distributed-lock/source/LockTest.cs similarity index 100% rename from distributed-lock/source/consoleapp/LockTest.cs rename to distributed-lock/source/LockTest.cs diff --git a/distributed-lock/source/consoleapp/Program.cs b/distributed-lock/source/Program.cs similarity index 96% rename from distributed-lock/source/consoleapp/Program.cs rename to distributed-lock/source/Program.cs index 64032ba..a22fffa 100644 --- a/distributed-lock/source/consoleapp/Program.cs +++ b/distributed-lock/source/Program.cs @@ -16,7 +16,7 @@ static async Task Main(string[] args) var configuration = new ConfigurationBuilder() .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true) - .AddJsonFile($"appsettings.{Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT")}.json", optional: true); + .AddJsonFile($"appsettings.development.json", optional: true); var config = configuration.Build(); diff --git a/distributed-lock/source/consoleapp/Properties/launchSettings.json b/distributed-lock/source/Properties/launchSettings.json similarity index 57% rename from distributed-lock/source/consoleapp/Properties/launchSettings.json rename to distributed-lock/source/Properties/launchSettings.json index c143ad1..2886411 100644 --- a/distributed-lock/source/consoleapp/Properties/launchSettings.json +++ b/distributed-lock/source/Properties/launchSettings.json @@ -1,9 +1,9 @@ { "profiles": { - "Cosomos_Patterns": { + "GlobalLock": { "commandName": "Project", "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development" + "ASPNETCORE_ENVIRONMENT": "development" } } } diff --git a/distributed-lock/source/SETUP.md b/distributed-lock/source/SETUP.md deleted file mode 100644 index d613926..0000000 --- a/distributed-lock/source/SETUP.md +++ /dev/null @@ -1,55 +0,0 @@ -# Setup - -This template will create an Azure Cosmos DB for NoSQL account with a database named `LockDB` with a container named `Locks`. - -The suggested account name includes 'YOUR_SUFFIX'. Change that to a suffix to make your account name unique. - -The Azure Cosmos DB for NoSQL account will automatically be created with the region of the selected resource group. - ---- - -**This link will work if this is a public repo.** - -[![Deploy to Azure](https://aka.ms/deploytoazurebutton)](https://portal.azure.com/#create/Microsoft.Template/uri/https%3A%2F%2Fgithub.com%2FAzureCosmosDB%2Fdesign-patterns%2Ftree%2Fmain%2Fdistributed-lock%2Fsource%2Fazuredeploy.json) - -**For the private repo** - -1. [Create a custom template deployment](https://portal.azure.com/#create/Microsoft.Template/). -2. Select **Build your own template in the editor**. -3. Copy the contents from [this template](azuredeploy.json) into the editor. -4. Select **Save**. - ---- - -Once the template is loaded, populate the values: - -- **Subscription** - Choose a subscription. -- **Resource group** - Choose a resource group. -- **Region** - Select a region for the instance. -- **Location** - Enter a location for the Azure Cosmos DB for NoSQL account. **Note**: By default, it is set to use the location of the resource group. If you need to change this value, you can find the supported regions for your subscription via: - - [Azure CLI](https://learn.microsoft.com/cli/azure/account?view=azure-cli-latest#az-account-list-locations) - - PowerShell: `Get-AzLocation | Where-Object {$_.Providers -contains "Microsoft.DocumentDB"} | Select location` -- **Account Name** - Replace `YOUR_SUFFIX` with a suffix to make your Azure Cosmos DB account name unique. -- **Database Name** - Set to the default **LockDB**. -- **Container Name** - Set to the default **Locks**, it is partitioned by `/id`. -- **Throughput** - Set to the default **400**. -- **Enable Free Tier** - This defaults to `false`. Set it to **true** if you want to use it as [the free tier account](https://learn.microsoft.com/azure/cosmos-db/free-tier). - -Once those settings are set, select **Review + create**, then **Create**. - -## Set up environment variables - -1. Once the template deployment is complete, select **Go to resource group**. -2. Select the new Azure Cosmos DB for NoSQL account. -3. From the navigation, under **Settings**, select **Keys**. - -Update the following in the appsettings.json** before you run the code: - -- `CosmosUri`: Set to the `URI` value on the Azure Cosmos DB account Keys blade. -- `CosmosKey`: Set to the Read-Write `PRIMARY KEY` for the Azure Cosmos DB for NoSQL account - -## Run the demo - -1. In Visual Studio load the **Cosmos_Patterns_GlobalLock.sln** -2. Press **F5** to run 2 the project. -3. When prompted, enter the values for the lock name and the default TTL diff --git a/distributed-lock/source/appsettings.json b/distributed-lock/source/appsettings.json new file mode 100644 index 0000000..cbbfb82 --- /dev/null +++ b/distributed-lock/source/appsettings.json @@ -0,0 +1,7 @@ +{ + "CosmosUri": "", + "CosmosKey": "", + "CosmosDatabase": "LockDB", + "CosmosContainer": "Locks", + "retryInterval": 1 +} \ No newline at end of file diff --git a/distributed-lock/source/consoleapp/Cosmos_Patterns_GlobalLock.sln b/distributed-lock/source/consoleapp/Cosmos_Patterns_GlobalLock.sln deleted file mode 100644 index 4fbb966..0000000 --- a/distributed-lock/source/consoleapp/Cosmos_Patterns_GlobalLock.sln +++ /dev/null @@ -1,25 +0,0 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 17 -VisualStudioVersion = 17.5.33502.453 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GlobalLock", "GlobalLock.csproj", "{28E13C58-EAD7-47E0-A76A-DB3DFC0B3DE9}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Release|Any CPU = Release|Any CPU - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {28E13C58-EAD7-47E0-A76A-DB3DFC0B3DE9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {28E13C58-EAD7-47E0-A76A-DB3DFC0B3DE9}.Debug|Any CPU.Build.0 = Debug|Any CPU - {28E13C58-EAD7-47E0-A76A-DB3DFC0B3DE9}.Release|Any CPU.ActiveCfg = Release|Any CPU - {28E13C58-EAD7-47E0-A76A-DB3DFC0B3DE9}.Release|Any CPU.Build.0 = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(ExtensibilityGlobals) = postSolution - SolutionGuid = {B8C70C75-BEE9-45B2-883D-B903A31A1A24} - EndGlobalSection -EndGlobal diff --git a/distributed-lock/source/consoleapp/appsettings.json b/distributed-lock/source/consoleapp/appsettings.json deleted file mode 100644 index 399d958..0000000 --- a/distributed-lock/source/consoleapp/appsettings.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "DetailedErrors": true, - "Logging": { - "LogLevel": { - "Default": "Information", - "Microsoft.AspNetCore": "Warning" - } - }, - "AllowedHosts": "*", - - "CosmosUri": "", - "CosmosKey": "", - "CosmosDatabase": "LockDB", - "CosmosContainer": "Locks", - "retryInterval": 1 - -} \ No newline at end of file diff --git a/document-versioning/README.md b/document-versioning/README.md index 37478a2..0c5baca 100644 --- a/document-versioning/README.md +++ b/document-versioning/README.md @@ -56,7 +56,7 @@ Now, suppose the customer had to cancel the order. The replacement document coul } ``` -Looking at these documents, though, there is no easy way to tell which of these documents is the current document. By using document versioning, add a field to the document to track the version number. Update the current document in a `CurrentOrderStatus` container and add the change to the `HistoricalOrderStatus` container. While Azure Cosmos DB for NoSQL does not have a document versioning feature, you can build in the handling through an application. In [the demo](./code/setup.md), you can see how to implement the document versioning feature with the following components: +Looking at these documents, though, there is no easy way to tell which of these documents is the current document. By using document versioning, add a field to the document to track the version number. Update the current document in a `CurrentOrderStatus` container and add the change to the `HistoricalOrderStatus` container. While Azure Cosmos DB for NoSQL does not have a document versioning feature, you can build in the handling through an application. In the two projects here, you can see how to implement the document versioning feature with the following components: - A website that allows you to create orders and change the order status. The website updates the document version and saves the document to the current status container. - A Function App that reads the data for the Azure Cosmos DB change feed and copies the versioned documents to the historical status container @@ -69,7 +69,7 @@ The demo website includes links to update the orders to the different statuses. In order to run the demos, you will need: -- [.NET 6.0 Runtime](https://dotnet.microsoft.com/download/dotnet/6.0) +- [.NET 8.0 Runtime](https://dotnet.microsoft.com/download) - [Azure Functions Core Tools v4](https://learn.microsoft.com/azure/azure-functions/functions-run-local#install-the-azure-functions-core-tools) ## Confirm required tools are installed @@ -82,7 +82,7 @@ First, check the .NET runtime with this command: dotnet --list-runtimes ``` -As you may have multiple versions of the runtime installed, make sure that .NET components with versions that start with 6.0 appear as part of the output. +As you may have multiple versions of the runtime installed, make sure that .NET components with versions that start with 8.0 appear as part of the output. Next, check the version of Azure Functions Core Tools with this command: @@ -120,41 +120,46 @@ You can try out this implementation by running the code in [GitHub Codespaces](h [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/azure-samples/cosmos-db-design-patterns?quickstart=1&devcontainer_path=.devcontainer%2Fdocument-versioning%2Fdevcontainer.json) -## Create an Azure Cosmos DB for NoSQL account +## Set up application configuration files -1. Create a free Azure Cosmos DB for NoSQL account: () - -1. In the Data Explorer, create a new database and container with the following values: +You need to configure the application configuration file to run these demos. - | | Value | - | --- | --- | - | **Database name** | `Orders` | - | **Container name** | `CurrentOrderStatus` | - | **Partition key path** | `/CustomerId` | - | **Throughput** | `1000` (*Autoscale*) | +1. Go to resource group. -1. In the Data Explorer, create a second container with the following values: +1. Select the Serverless Azure Cosmos DB for NoSQL account that you created for this repository. - | | Value | - | --- | --- | - | **Database name** | `Orders` | - | **Container name** | `HistoricalOrderStatus` | - | **Partition key path** | `/CustomerId` | - | **Throughput** | `1000` (*Autoscale*) | +1. From the navigation, under **Settings**, select **Keys**. The values you need for the application settings for the demo are here. -**Note:** We are using shared database throughput because it can scale down to 100 RU/s when not running. This is the most cost efficient if running in a paid subscription and not using Free Tier. +While on the Keys blade, make note of the `URI`, `PRIMARy KEY` and `PRIMARY CONNECTION STRING`. You will need these for the sections below. -## Set up application configuration file +## Prepare the web app configuration -You need to configure the application configuration file to run these demos. +1. Open the website project and add a new **appsettings.development.json** file with the following contents: -1. Go to resource group. + ```json + { + "CosmosDb": { + "CosmosUri": "", + "CosmosKey": "", + "Database": "DocumentVersionDB", + "CurrentOrderContainer": "CurrentOrderStatus", + "HistoricalOrderContainer": "HistoricalOrderStatus", + "PartitionKey": "/CustomerId" + } + } + ``` -1. Select the new Azure Cosmos DB for NoSQL account. +1. Replace the `CosmosURI` and `CosmosKey` with the values from the Keys blade in the Azure Portal. +1. Modify the **Copy to Output Directory** to **Copy Always** (For VS Code add the XML below to the csproj file) +1. Save the file. -1. From the navigation, under **Settings**, select **Keys**. The values you need for the application settings for the demo are here. - -While on the Keys blade, make note of the `PRIMARY CONNECTION STRING`. You will need this for the Azure Function App. + ```xml + + + Always + + + ``` ## Prepare the function app configuration @@ -171,13 +176,30 @@ While on the Keys blade, make note of the `PRIMARY CONNECTION STRING`. You will } ``` - Make sure to replace `YOUR_PRIMARY_CONNECTION_STRING` with the `PRIMARY CONNECTION STRING` value noted earlier. +1. Replace `YOUR_PRIMARY_CONNECTION_STRING` with the `PRIMARY CONNECTION STRING` value noted earlier. +1. Modify the **Copy to Output Directory** to **Copy Always** (For VS Code add the XML below to the csproj file) +1. Save the file. -2. Edit **host.json** Set the `userAgentSuffix` to a value you prefer to use. This is used in tracking in Activity Monitor. See [host.json settings](https://learn.microsoft.com/azure/azure-functions/functions-bindings-cosmosdb-v2?tabs=in-process%2Cextensionv4&pivots=programming-language-csharp#hostjson-settings) for more details. + ```xml + + + Always + Never + + + ``` ## Run the demo locally -1. Switch to the `website` folder. Then start the website with: +1. From the `function-app` folder, start the Azure Function with: + + ```bash + func start + ``` + +1. Open a new Terminal in VS Code or where ever you are running this. + +1. Navigate to the `website` folder, start the website with: ```bash dotnet run @@ -187,15 +209,8 @@ While on the Keys blade, make note of the `PRIMARY CONNECTION STRING`. You will ![Screenshot of the 'dotnet run' output. The URL to navigate to is highlighted. In the screenshot, the URL is 'http://localhost:5183'.](images/local-site-url.png) - **Don't do anything on this website yet. Continue to the next step.** - -1. At another command prompt, switch to the `function-app` folder. Then, run the Function App with: - - ```bash - func start - ``` -Now that you have the website and function app started, create 5-10 orders with the website. +1. With both the Azure Function and web app running, create 5-10 orders with the website. This is what the website will look like when starting out: diff --git a/document-versioning/source/azuredeploy.json b/document-versioning/source/azuredeploy.json deleted file mode 100644 index 1f5c93b..0000000 --- a/document-versioning/source/azuredeploy.json +++ /dev/null @@ -1,158 +0,0 @@ -{ - "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", - "contentVersion": "1.0.0.0", - "parameters": { - "location": { - "type": "string", - "defaultValue": "[resourceGroup().location]", - "metadata": { - "description": "Location for all resources." - } - }, - "accountName": { - "type": "string", - "defaultValue": "doc-versioning-demo-YOUR_SUFFIX", - "metadata": { - "description": "Azure Cosmos DB account name, max length 44 characters" - } - }, - "databaseName": { - "type": "string", - "defaultValue": "Orders", - "metadata": { - "description": "The name for the database" - } - }, - "currentContainerName": { - "type": "string", - "defaultValue": "CurrentOrderStatus", - "metadata": { - "description": "The name for the container partitioned by CustomerId" - } - }, - "historicalContainerName": { - "type": "string", - "defaultValue": "HistoricalOrderStatus", - "metadata": { - "description": "The name for the container partitioned by CustomerId" - } - }, - "throughput": { - "type": "int", - "defaultValue": 400, - "metadata": { - "description": "The throughput for the container" - }, - "maxValue": 1000000, - "minValue": 400 - }, - "enableFreeTier": { - "type": "bool", - "defaultValue": false - } - }, - "resources": [ - { - "type": "Microsoft.DocumentDB/databaseAccounts", - "apiVersion": "2022-05-15", - "name": "[toLower(parameters('accountName'))]", - "kind": "GlobalDocumentDB", - "location": "[parameters('location')]", - "properties": { - "databaseAccountOfferType": "Standard", - "enableFreeTier": "[parameters('enableFreeTier')]", - "locations": [ - { - "locationName": "[parameters('location')]", - "failoverPriority": 0 - } - ] - } - }, - { - "type": "Microsoft.DocumentDB/databaseAccounts/sqlDatabases", - "apiVersion": "2022-05-15", - "name": "[format('{0}/{1}', toLower(parameters('accountName')), parameters('databaseName'))]", - "properties": { - "resource": { - "id": "[parameters('databaseName')]" - } - }, - "dependsOn": [ - "[resourceId('Microsoft.DocumentDB/databaseAccounts', toLower(parameters('accountName')))]" - ] - }, - { - "type": "Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers", - "apiVersion": "2022-05-15", - "name": "[format('{0}/{1}', format('{0}/{1}', toLower(parameters('accountName')), parameters('databaseName')), parameters('currentContainerName'))]", - "properties": { - "resource": { - "id": "[parameters('currentContainerName')]", - "partitionKey": { - "paths": [ - "/CustomerId" - ], - "kind": "Hash" - }, - "indexingPolicy": { - "indexingMode": "consistent", - "includedPaths": [ - { - "path": "/*" - } - ], - "excludedPaths": [ - { - "path": "/_etag/?" - } - ] - }, - "defaultTtl": 86400 - }, - "options": { - "throughput": "[parameters('throughput')]" - } - }, - "dependsOn": [ - "[resourceId('Microsoft.DocumentDB/databaseAccounts/sqlDatabases', split(format('{0}/{1}', toLower(parameters('accountName')), parameters('databaseName')), '/')[0], split(format('{0}/{1}', toLower(parameters('accountName')), parameters('databaseName')), '/')[1])]" - ] - }, - { - "type": "Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers", - "apiVersion": "2022-05-15", - "name": "[format('{0}/{1}', format('{0}/{1}', toLower(parameters('accountName')), parameters('databaseName')), parameters('historicalContainerName'))]", - "properties": { - "resource": { - "id": "[parameters('historicalContainerName')]", - "partitionKey": { - "paths": [ - "/CustomerId" - ], - "kind": "Hash" - }, - "indexingPolicy": { - "indexingMode": "consistent", - "includedPaths": [ - { - "path": "/*" - } - ], - "excludedPaths": [ - { - "path": "/_etag/?" - } - ] - }, - "defaultTtl": 86400 - }, - "options": { - "throughput": "[parameters('throughput')]" - } - }, - "dependsOn": [ - "[resourceId('Microsoft.DocumentDB/databaseAccounts/sqlDatabases', split(format('{0}/{1}', toLower(parameters('accountName')), parameters('databaseName')), '/')[0], split(format('{0}/{1}', toLower(parameters('accountName')), parameters('databaseName')), '/')[1])]" - ] - } - ] -} \ No newline at end of file diff --git a/document-versioning/source/function-app/DocumentVersioningProcessor.cs b/document-versioning/source/function-app/DocumentVersioningProcessor.cs index a99e3d1..728f12a 100644 --- a/document-versioning/source/function-app/DocumentVersioningProcessor.cs +++ b/document-versioning/source/function-app/DocumentVersioningProcessor.cs @@ -8,23 +8,34 @@ namespace Versioning public static class DocumentVersioningProcessor { [FunctionName("DocumentVersioningProcessor")] - public static async Task Run([CosmosDBTrigger( - databaseName: "Orders", - containerName: "CurrentOrderStatus", - Connection = "CosmosDBConnection", - LeaseContainerName = "leases", CreateLeaseContainerIfNotExists=true)]IReadOnlyList input, - [CosmosDB(databaseName: "Orders", - containerName: "HistoricalOrderStatus", - Connection="CosmosDBConnection", CreateIfNotExists=true, PartitionKey="/CustomerId")] IAsyncCollector historicalOrdersOut, + public static async Task Run( + [CosmosDBTrigger( + databaseName: "DocumentVersionDB", + containerName: "CurrentOrderStatus", + Connection = "CosmosDBConnection", + LeaseContainerName = "leases", + CreateLeaseContainerIfNotExists=true)] + IReadOnlyList input, + [CosmosDB( + databaseName: "DocumentVersionDB", + containerName: "HistoricalOrderStatus", + Connection="CosmosDBConnection", + CreateIfNotExists=true, + PartitionKey="/CustomerId")] + IAsyncCollector historicalOrdersOut, ILogger log) { if (input != null && input.Count > 0) { log.LogInformation("Document count: " + input.Count); + foreach (VersionedOrder versionedOrder in input){ + log.LogInformation($"Processing {versionedOrder.OrderId} - Status: {versionedOrder.Status}"); + // new id for the historical collection to preserve the history rather than overwrite it versionedOrder.id = System.Guid.NewGuid().ToString(); + await historicalOrdersOut.AddAsync(versionedOrder); } } diff --git a/document-versioning/source/function-app/function-app.csproj b/document-versioning/source/function-app/function-app.csproj index f84b01d..3606d0f 100644 --- a/document-versioning/source/function-app/function-app.csproj +++ b/document-versioning/source/function-app/function-app.csproj @@ -14,7 +14,7 @@ PreserveNewest - PreserveNewest + Always Never diff --git a/document-versioning/source/setup.md b/document-versioning/source/setup.md deleted file mode 100644 index e301389..0000000 --- a/document-versioning/source/setup.md +++ /dev/null @@ -1,182 +0,0 @@ -# Document Versioning - -In order to run the demos, you will need: - -- [.NET 6.0 Runtime](https://dotnet.microsoft.com/en-us/download/dotnet/6.0) -- [Azure Functions Core Tools v4](https://learn.microsoft.com/azure/azure-functions/functions-run-local#install-the-azure-functions-core-tools) - -## Confirm required tools are installed - -Confirm you have the required versions of the tools installed for this demo. - -First, check the .NET runtime with this command: - -```bash -dotnet --list-runtimes -``` - -As you may have multiple versions of the runtime installed, make sure that .NET components with versions that start with 6.0 appear as part of the output. - -Next, check the version of Azure Functions Core Tools with this command: - -```bash -func --version -``` - -You should have installed a version that starts with `4.`. If you do not have a v4 version installed, you will need to uninstall the older version and follow [these instructions for installing Azure Functions Core Tools](https://learn.microsoft.com/azure/azure-functions/functions-run-local#install-the-azure-functions-core-tools). - -## Create an Azure Cosmos DB for NoSQL account - -This template will create an Azure Cosmos DB for NoSQL account with a database named `Orders` with a container named `CurrentOrderStatus`. The partition key is set for `/CustomerId`. The data generator defaults to these values. This will also create a container named `HistoricalOrderStatus` with the partition key of `/CustomerId`. - -The suggested account name includes 'YOUR_SUFFIX'. Change that to a suffix to make your account name unique. - -The Azure Cosmos DB for NoSQL account will automatically be created with the region of the selected resource group. - -There is an option to enable the free tier. This is so that others can try this out with minimal costs to them. - ---- - -**This link will work if this is a public repo.** - -[![Deploy to Azure](https://aka.ms/deploytoazurebutton)](https://portal.azure.com/#create/Microsoft.Template/uri/https%3A%2F%2Fraw.githubusercontent.com%2Fsolliancenet%2Fcosmos-db-nosql-modeling%2Fmain%2Fdocument-versioning%2Fcode%2Fazuredeploy.json) - -**For the private repo** - -1. [Create a custom template deployment](https://portal.azure.com/#create/Microsoft.Template/). -2. Select **Build your own template in the editor**. -3. Copy the contents from [this template](azuredeploy.json) into the editor. -4. Select **Save**. - ---- - -Once the template is loaded, populate the values: - -- **Subscription** - Choose a subscription. -- **Resource group** - Choose a resource group. -- **Region** - Select a region for the instance. -- **Location** - Enter a location for the Azure Cosmos DB for NoSQL account. **Note**: By default, it is set to use the location of the resource group. If you need to change this value, you can find the supported regions for your subscription via: - - [Azure CLI](https://learn.microsoft.com/cli/azure/account?view=azure-cli-latest#az-account-list-locations) - - PowerShell: `Get-AzLocation | Where-Object {$_.Providers -contains "Microsoft.DocumentDB"} | Select location` -- **Account Name** - Replace `YOUR_SUFFIX` with a suffix to make your Azure Cosmos DB account name unique. -- **Database Name** - Set to the default **Orders**. -- **Current Container Name** - This is the container partitioned by `/CustomerId`. Set to the default **CurrentOrderStatus**. -- **Historical Container Name** - This is the container partitioned by `/CustomerId`. Set to the default **HistoricalOrderStatus**. -- **Throughput** - Set to the default **400**. -- **Enable Free Tier** - This defaults to `false`. Set it to **true** if you want to use it as [the free tier account](https://learn.microsoft.com/azure/cosmos-db/free-tier). - -Once those settings are set, select **Review + create**, then **Create**. - -## Set up environment variables - -You need 2 environment variables to run these demos. - -1. Once the template deployment is complete, select **Go to resource group**. -2. Select the new Azure Cosmos DB for NoSQL account. -3. From the navigation, under **Settings**, select **Keys**. The values you need for the environment variables for the demo are here. - -Create 2 environment variables to run the demos: - -- `COSMOS_ENDPOINT`: set to the `URI` value on the Azure Cosmos DB account Keys blade. -- `COSMOS_KEY`: set to the Read-Write `PRIMARY KEY` for the Azure Cosmos DB for NoSQL account - -Create your environment variables with the following syntax: - -PowerShell: - -```powershell -$env:COSMOS_ENDPOINT="YOUR_COSMOS_ENDPOINT" -$env:COSMOS_KEY="YOUR_COSMOS_READ_WRITE_PRIMARY_KEY" -``` - -Bash: - -```bash -export COSMOS_ENDPOINT="YOUR_COSMOS_ENDPOINT" -export COSMOS_KEY="YOUR_COSMOS_KEY" -``` - -Windows Command: - -```text -set COSMOS_ENDPOINT=YOUR_COSMOS_ENDPOINT -set COSMOS_KEY=YOUR_COSMOS_KEY -``` - -While on the Keys blade, make note of the `PRIMARY CONNECTION STRING`. You will need this for the Azure Function App. - -## Prepare the function app configuration - -1. Add a file to the `function-app` folder called **local.settings.json** with the following contents: - - ```json - { - "IsEncrypted": false, - "Values": { - "AzureWebJobsStorage": "UseDevelopmentStorage=false", - "FUNCTIONS_WORKER_RUNTIME": "dotnet", - "CosmosDBConnection" : "YOUR_PRIMARY_CONNECTION_STRING" - } - } - ``` - - Make sure to replace `YOUR_PRIMARY_CONNECTION_STRING` with the `PRIMARY CONNECTION STRING` value noted earlier. - -2. Edit **host.json** Set the `userAgentSuffix` to a value you prefer to use. This is used in tracking in Activity Monitor. See [host.json settings](https://learn.microsoft.com/en-us/azure/azure-functions/functions-bindings-cosmosdb-v2?tabs=in-process%2Cextensionv4&pivots=programming-language-csharp#hostjson-settings) for more details. - -## Run the demo locally - -1. Switch to the `website` folder. Then start the website with: - - ```bash - dotnet run - ``` - - Navigate to the URL displayed in the output. In the example below, the URL is shown as part of the `info` output, following the "Now listening on:" text. - - ![Screenshot of the 'dotnet run' output. The URL to navigate to is highlighted. In the screenshot, the URL is 'http://localhost:5183'.](../images/local-site-url.png) - - **Do not doing anything on this website yet. Continue to the next step.** - -2. At another command prompt, switch to the `function-app` folder. Then, run the function app with: - - ```bash - func start - ``` - -Now that you have the website and function app started, create 5-10 orders with the website. - - -This is what the website will look like when starting out: - -![Screenshot of the Document Versioning Demo website. There is a form at the top labeled 'Create New Orders'. It has a field labeled 'Number to create', an input box, and a 'Submit' button. There are tables for Submitted Orders, Fulfilled Orders, Delivered Orders, and Cancelled Orders.](../images/document-versioning-demo-1.png) - - The Create New Orders form will create orders without the DocumentVersion property. Enter a number in the **Number to create** text box, then select **Submit**. This is how the new order appears on the website: - -![Screenshot of the Submitted Orders section with an order showing the Document Version of Submitted.](../images/newly-submitted-order.png) - -This is what the new order looks like in Azure Cosmos DB. Notice that the `DocumentVersion` property is absent. - -![Screenshot of a query in Azure Data Explorer for the order above. The JSON result does not include the 'DocumentVersion' property.](../images/newly-submitted-order-data-explorer.png) - -The Azure Function is working directly with a `VersionedDocument` type, so it will carry the `DocumentVersion` field into the `HistoricalOrderStatus` container. For new documents, this will assume the DocumentVersion is 1 when it isn't specified. - -Unversioned documents will still show as document version 1 due to the `VersionedOrder` C# class. - -Select any of the links in the Links columns to change the status on the document. - -- As you advance the status of the orders, notice that the Document Version field increments. The document version numbering is managed by the application, specifically in the `HandleVersioning()` function in the `OrderHelper` class in the `Services` folder. - - ![Screenshot of the Document Versioning Demo website. There are tables for Submitted Orders, Fulfilled Orders, Delivered Orders, and Cancelled Orders. Fulfilled Orders and Cancelled Orders show a Document Version of 2. Delivered Orders show a Document Version of 3.](../images/document-versioning-demo-2.png) - -- You can query the `CurrentOrderStatus` container in Data Explorer for the order number (`OrderId`) and Customer Id (`CustomerId`) and should only get back 1 document - the current document. - - In this example, the previously shown document was fulfilled. Notice in the Azure Data Explorer results that the `DocumentVersion` property is now a part of the document in `CurrentOrderStatus`. - - ![Screenshot of Azure Data Explorer with the document from the previous example. The query is querying for a specific OrderId and CustomerId in the CurrentOrderStatus container. The Status for this document is now Fulfilled. The DocumentVersion property appears at the top of the document and is now at 2.](../images/newly-submitted-order-fulfilled-with-document-version.png) - -- You can also query the `HistoricalOrderStatus` container for that order number and customer Id and get back the entire order status history. - - In this example, the previously shown document was fulfilled. Notice in the Azure Data Explorer results that the `DocumentVersion` property is now a part of the document in `CurrentOrderStatus`. - - ![Screenshot of Azure Data Explorer querying HistoricalOrderStatus with the OrderId and CustomerId from the previous example. The Status is now Fulfilled. The DocumentVersion property appears at the top of the document and is now at 2. There are 2 results in the results list.](../images/newly-submitted-order-fulfilled-with-history.png) diff --git a/document-versioning/source/website/Controllers/StatusController.cs b/document-versioning/source/website/Controllers/StatusController.cs index f95513a..1cb97c3 100644 --- a/document-versioning/source/website/Controllers/StatusController.cs +++ b/document-versioning/source/website/Controllers/StatusController.cs @@ -1,33 +1,40 @@ using Microsoft.AspNetCore.Mvc; -using Versioning; +using Services; public class StatusController : Controller { - private OrderHelper helper = new OrderHelper(); + //private OrderHelper helper = new OrderHelper(); + + private readonly OrderHelper _helper; + + public StatusController(OrderHelper helper) + { + _helper = helper; + } [HttpGet("Cancel/{orderId}/{customerId}")] public async Task Cancel(string orderId, int customerId){ - var versionedDocument = await helper.RetrieveOrderAsync(orderId, customerId); - helper.CancelOrder(versionedDocument); - await helper.SaveVersionedOrder(versionedDocument); + var versionedDocument = await _helper.RetrieveOrderAsync(orderId, customerId); + _helper.CancelOrder(versionedDocument); + await _helper.SaveVersionedOrder(versionedDocument); return RedirectToPage("/Index"); } [HttpGet("Deliver/{orderId}/{customerId}")] public async Task Deliver(string orderId, int customerId) { - var versionedDocument = await helper.RetrieveOrderAsync(orderId, customerId); - helper.DeliverOrder(versionedDocument); - await helper.SaveVersionedOrder(versionedDocument); + var versionedDocument = await _helper.RetrieveOrderAsync(orderId, customerId); + _helper.DeliverOrder(versionedDocument); + await _helper.SaveVersionedOrder(versionedDocument); return RedirectToPage("/Index"); } [HttpGet("Fulfill/{orderId}/{customerId}")] public async Task Fulfill(string orderId, int customerId) { - var versionedDocument = await helper.RetrieveOrderAsync(orderId, customerId); - helper.FulfillOrder(versionedDocument); - await helper.SaveVersionedOrder(versionedDocument); + var versionedDocument = await _helper.RetrieveOrderAsync(orderId, customerId); + _helper.FulfillOrder(versionedDocument); + await _helper.SaveVersionedOrder(versionedDocument); return RedirectToPage("/Index"); } } \ No newline at end of file diff --git a/document-versioning/source/website/Models/Order.cs b/document-versioning/source/website/Models/Order.cs index 2c3b6bd..c47bc81 100644 --- a/document-versioning/source/website/Models/Order.cs +++ b/document-versioning/source/website/Models/Order.cs @@ -1,4 +1,4 @@ -namespace Versioning { +namespace Models { public class Order { public string id { get; set; } = Guid.NewGuid().ToString(); public string OrderId { get; set; } = default!; diff --git a/document-versioning/source/website/Models/OrderItem.cs b/document-versioning/source/website/Models/OrderItem.cs index f2f3841..b15710e 100644 --- a/document-versioning/source/website/Models/OrderItem.cs +++ b/document-versioning/source/website/Models/OrderItem.cs @@ -1,4 +1,4 @@ -namespace Versioning +namespace Models { public class OrderItem { public string ProductName { get; set; } = ""; diff --git a/document-versioning/source/website/Models/VersionedOrder.cs b/document-versioning/source/website/Models/VersionedOrder.cs index 8563e8c..577ed88 100644 --- a/document-versioning/source/website/Models/VersionedOrder.cs +++ b/document-versioning/source/website/Models/VersionedOrder.cs @@ -1,4 +1,4 @@ -namespace Versioning { +namespace Models { public class VersionedOrder : Order { public int DocumentVersion { get; set; } = 1; diff --git a/document-versioning/source/website/Options/CosmosDb.cs b/document-versioning/source/website/Options/CosmosDb.cs new file mode 100644 index 0000000..857fd8e --- /dev/null +++ b/document-versioning/source/website/Options/CosmosDb.cs @@ -0,0 +1,13 @@ +namespace Options +{ + public record CosmosDb + { + public required string CosmosUri { get; init; } + public required string CosmosKey { get; init; } + public required string Database { get; init; } + public required string CurrentOrderContainer { get; init; } + public required string HistoricalOrderContainer { get; init; } + public required string PartitionKey { get; init; } + + }; +} diff --git a/document-versioning/source/website/Pages/Index.cshtml.cs b/document-versioning/source/website/Pages/Index.cshtml.cs index 89a8009..3f8599b 100644 --- a/document-versioning/source/website/Pages/Index.cshtml.cs +++ b/document-versioning/source/website/Pages/Index.cshtml.cs @@ -1,6 +1,7 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.RazorPages; -using Versioning; +using Models; +using Services; namespace website.Pages; @@ -11,12 +12,14 @@ public class IndexModel : PageModel public List FulfilledOrders = new List(); public List DeliveredOrders = new List(); public List CancelledOrders = new List(); - private OrderHelper helper = new OrderHelper(); + //private OrderHelper helper = new OrderHelper(); + private readonly OrderHelper _helper; private readonly ILogger _logger; - public IndexModel(ILogger logger) + public IndexModel(OrderHelper helper, ILogger logger) { + _helper = helper; _logger = logger; } @@ -29,8 +32,8 @@ public async Task OnPost(){ int numberToCreate = Convert.ToInt32(Request.Form["DocCount"]); for (int counter = 0; counter < numberToCreate; counter++) { - Order newOrder = helper.GenerateOrder(); - await helper.SaveOrder(newOrder); + Order newOrder = _helper.GenerateOrder(); + await _helper.SaveOrder(newOrder); } await GetOrders(); return Page(); @@ -38,7 +41,7 @@ public async Task OnPost(){ private async Task GetOrders() { - List orders = (await helper.RetrieveAllOrdersAsync()).ToList(); + List orders = (await _helper.RetrieveAllOrdersAsync()).ToList(); SubmittedOrders = orders.Where(order => order.Status == "Submitted").ToList(); FulfilledOrders = orders.Where(order => order.Status == "Fulfilled").ToList(); DeliveredOrders = orders.Where(order => order.Status == "Delivered").ToList(); diff --git a/document-versioning/source/website/Program.cs b/document-versioning/source/website/Program.cs index 4947e38..8cc5643 100644 --- a/document-versioning/source/website/Program.cs +++ b/document-versioning/source/website/Program.cs @@ -1,25 +1,28 @@ +using Microsoft.Extensions.Options; +using Options; +using Services; + var builder = WebApplication.CreateBuilder(args); // Add services to the container. +builder.RegisterConfiguration(); +builder.Services.RegisterServices(); builder.Services.AddRazorPages(); +builder.Services.AddServerSideBlazor(); + var app = builder.Build(); -// Configure the HTTP request pipeline. if (!app.Environment.IsDevelopment()) { app.UseExceptionHandler("/Error"); - // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts. app.UseHsts(); } app.UseHttpsRedirection(); app.UseStaticFiles(); - app.UseRouting(); -app.UseAuthorization(); - app.MapControllerRoute( name: "updateOrderStatus", pattern: "{controller=Status}/{action}/{orderId}/{customerId}"); @@ -27,3 +30,52 @@ app.MapRazorPages(); app.Run(); + +static class ProgramExtensions +{ + public static void RegisterConfiguration(this WebApplicationBuilder builder) + { + builder.Configuration.AddJsonFile("appsettings.json"); + builder.Configuration.AddJsonFile($"appsettings.development.json", optional: true); + + builder.Services.AddOptions() + .Bind(builder.Configuration.GetSection(nameof(Options.CosmosDb))); + + } + public static void RegisterServices(this IServiceCollection services) + { + services.AddSingleton((provider) => + { + var cosmosOptions = provider.GetRequiredService>(); + if (cosmosOptions is null) + { + throw new ArgumentException($"{nameof(IOptions)} was not resolved through dependency injection."); + } + else + { + return new Services.CosmosDb( + cosmosUri: cosmosOptions.Value?.CosmosUri ?? string.Empty, + cosmosKey: cosmosOptions.Value?.CosmosKey ?? string.Empty, + database: cosmosOptions.Value?.Database ?? string.Empty, + currentOrderContainer: cosmosOptions.Value?.CurrentOrderContainer ?? string.Empty, + historicalOrderContainer: cosmosOptions.Value?.HistoricalOrderContainer ?? string.Empty, + partitionKey: cosmosOptions.Value?.PartitionKey ?? string.Empty + ); + } + + }); + services.AddSingleton((provider) => + { + var cosmosDb = provider.GetRequiredService(); + if (cosmosDb is null) + { + throw new ArgumentException($"{nameof(Services.CosmosDb)} was not resolved through dependency injection."); + } + else + { + return new OrderHelper(cosmosDb); + } + }); + } +} + diff --git a/document-versioning/source/website/Properties/launchSettings.json b/document-versioning/source/website/Properties/launchSettings.json index 82664ff..d064511 100644 --- a/document-versioning/source/website/Properties/launchSettings.json +++ b/document-versioning/source/website/Properties/launchSettings.json @@ -8,20 +8,11 @@ } }, "profiles": { - "http": { + "Doc Version": { "commandName": "Project", "dotnetRunMessages": true, "launchBrowser": true, - "applicationUrl": "http://localhost:5183", - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development" - } - }, - "https": { - "commandName": "Project", - "dotnetRunMessages": true, - "launchBrowser": true, - "applicationUrl": "https://localhost:7207;http://localhost:5183", + "applicationUrl": "https://localhost:7207", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } diff --git a/document-versioning/source/website/Services/CosmosDb.cs b/document-versioning/source/website/Services/CosmosDb.cs new file mode 100644 index 0000000..0a4bb7c --- /dev/null +++ b/document-versioning/source/website/Services/CosmosDb.cs @@ -0,0 +1,39 @@ +using Microsoft.Azure.Cosmos; + +namespace Services +{ + public class CosmosDb + { + private readonly CosmosClient client; + private Container? orderContainer; + private Container? historyContainer; + + public Container OrderContainer => orderContainer ?? throw new InvalidOperationException("OrderContainer is not initialized."); + + + public CosmosDb(string cosmosUri, string cosmosKey, string database, string currentOrderContainer, string historicalOrderContainer, string partitionKey) + { + + client = new CosmosClient( + accountEndpoint: cosmosUri!, + authKeyOrResourceToken: cosmosKey!); + + InitializeAsync(database, currentOrderContainer, historicalOrderContainer, partitionKey).Wait(); + } + + private async Task InitializeAsync(string databaseName, string currentOrderContainerName, string historicalOrderContainerName, string partitionKey) + { + Database database = await client.CreateDatabaseIfNotExistsAsync(databaseName); + + orderContainer = await database.CreateContainerIfNotExistsAsync( + id: currentOrderContainerName, + partitionKeyPath: partitionKey + ); + + historyContainer = await database.CreateContainerIfNotExistsAsync( + id: historicalOrderContainerName, + partitionKeyPath: partitionKey + ); + } + } +} diff --git a/document-versioning/source/website/Services/OrderHelper.cs b/document-versioning/source/website/Services/OrderHelper.cs index 9ec127f..df98abe 100644 --- a/document-versioning/source/website/Services/OrderHelper.cs +++ b/document-versioning/source/website/Services/OrderHelper.cs @@ -1,21 +1,20 @@ using Microsoft.Azure.Cosmos; using Microsoft.Azure.Cosmos.Linq; +using Microsoft.Extensions.Options; +using Options; +using Models; -namespace Versioning +namespace Services { public class OrderHelper { - private CosmosClient client; - private Database? database; - private Container? container; - private string databaseName = "Orders"; - private string containerName = "CurrentOrderStatus"; - private string partitionKey = "/CustomerId"; + private readonly CosmosDb _cosmosDb; + - public OrderHelper(){ - client = new CosmosClient( - accountEndpoint: Environment.GetEnvironmentVariable("COSMOS_ENDPOINT")!, - authKeyOrResourceToken: Environment.GetEnvironmentVariable("COSMOS_KEY")!); + public OrderHelper(CosmosDb cosmosDb) + { + + _cosmosDb = cosmosDb; } public Order GenerateOrder() { @@ -70,15 +69,12 @@ public OrderItem GenerateOrderItem() return orderItem; } - public async Task> RetrieveAllOrdersAsync(){ - database = await client.CreateDatabaseIfNotExistsAsync(id: databaseName); - container = await database.CreateContainerIfNotExistsAsync( - id: containerName, - partitionKeyPath: partitionKey, - throughput: 400 - ); + public async Task> RetrieveAllOrdersAsync() + { + List orders = new(); - using FeedIterator feed = container.GetItemQueryIterator( + + using FeedIterator feed = _cosmosDb.OrderContainer!.GetItemQueryIterator( queryText: "SELECT * FROM Orders" ); while (feed.HasMoreResults) @@ -94,19 +90,19 @@ public async Task> RetrieveAllOrdersAsync(){ return orders; } - public async Task RetrieveOrderAsync(string orderId, int customerId){ - database = await client.CreateDatabaseIfNotExistsAsync(id: databaseName); - container = await database.CreateContainerIfNotExistsAsync( - id: containerName, - partitionKeyPath: partitionKey, - throughput: 400 - ); - IOrderedQueryable ordersQueryable = container.GetItemLinqQueryable(); + public async Task RetrieveOrderAsync(string orderId, int customerId) + { + + IOrderedQueryable ordersQueryable = _cosmosDb.OrderContainer!.GetItemLinqQueryable(); + var matches = ordersQueryable .Where(order => order.CustomerId == customerId) .Where(order => order.OrderId == orderId); + using FeedIterator orderFeed = matches.ToFeedIterator(); + VersionedOrder selectedOrder = new VersionedOrder(); + while (orderFeed.HasMoreResults) { FeedResponse response = await orderFeed.ReadNextAsync(); @@ -116,30 +112,19 @@ public async Task RetrieveOrderAsync(string orderId, int custome } } - //return orderResponse.Resource; return selectedOrder; } public async Task SaveOrder(Order orderToUpdate) { - database = await client.CreateDatabaseIfNotExistsAsync(id: databaseName); - container = await database.CreateContainerIfNotExistsAsync( - id: containerName, - partitionKeyPath: partitionKey, - throughput: 400 - ); - return await container.UpsertItemAsync(orderToUpdate); + + return await _cosmosDb.OrderContainer!.UpsertItemAsync(orderToUpdate); } public async Task SaveVersionedOrder(VersionedOrder orderToUpdate) { - database = await client.CreateDatabaseIfNotExistsAsync(id: databaseName); - container = await database.CreateContainerIfNotExistsAsync( - id: containerName, - partitionKeyPath: partitionKey, - throughput: 400 - ); - return await container.UpsertItemAsync(orderToUpdate); + + return await _cosmosDb.OrderContainer!.UpsertItemAsync(orderToUpdate); } } } \ No newline at end of file diff --git a/document-versioning/source/website/appsettings.json b/document-versioning/source/website/appsettings.json index 10f68b8..f6470d5 100644 --- a/document-versioning/source/website/appsettings.json +++ b/document-versioning/source/website/appsettings.json @@ -5,5 +5,13 @@ "Microsoft.AspNetCore": "Warning" } }, - "AllowedHosts": "*" + "AllowedHosts": "*", + "CosmosDb": { + "CosmosUri": "", + "CosmosKey": "", + "Database": "DocumentVersionDB", + "CurrentOrderContainer": "CurrentOrderStatus", + "HistoricalOrderContainer": "HistoricalOrderStatus", + "PartitionKey": "/CustomerId" + } } diff --git a/document-versioning/source/website/website.csproj b/document-versioning/source/website/website.csproj index ca41b84..f851f2e 100644 --- a/document-versioning/source/website/website.csproj +++ b/document-versioning/source/website/website.csproj @@ -11,4 +11,10 @@ + + + Always + + + diff --git a/event-sourcing/README.md b/event-sourcing/README.md index 3afcb60..5c1dffb 100644 --- a/event-sourcing/README.md +++ b/event-sourcing/README.md @@ -63,7 +63,7 @@ This pattern provides: In this section we will walk through a case study on how to design and implement event sourcing, provide code examples and review cost considerations that will impact the design. -Consider a shopping cart application for an eCommerce company. All changes to the cart should be tracked as events but will be queried for multiple uses by different consuming services. Event sourcing pattern is chosen to ensure all history is retained and point in time state can be calculated. Each time a change is made to the cart there will be multiple calculations downstream. Rather than have the application update multiple containers, the single event store collection `shopping_cart_event` will be appended with the change. The partition key will be `/cartId` to support the most common queries by the shopping cart service. Other services will consume data from the change feed and use solutions like [materialized views](../materialized_views/README.md) to support different query patterns. +Consider a shopping cart application for an eCommerce company. All changes to the cart should be tracked as events but will be queried for multiple uses by different consuming services. Event sourcing pattern is chosen to ensure all history is retained and point in time state can be calculated. Each time a change is made to the cart there will be multiple calculations downstream. Rather than have the application update multiple containers, the single event store collection `shopping_cart_event` will be appended with the change. The partition key will be `/CartId` to support the most common queries by the shopping cart service. Other services will consume data from the change feed and use solutions like [materialized views](../materialized_views/README.md) to support different query patterns. In this example the state of all products in the cart is maintained as `productsInCart`. However, this could also be derived by each query or consumer if the application that writes the data does not know the full state. @@ -124,7 +124,7 @@ Sample events in the event store could look like this: To run the function app for Event Sourcing Pattern, you will need to have: -- [.NET 6.0 Runtime](https://dotnet.microsoft.com/download/dotnet/6.0) +- [.NET 8.0 Runtime](https://dotnet.microsoft.com/download) - [Azure Functions Core Tools](https://learn.microsoft.com/azure/azure-functions/functions-run-local#install-the-azure-functions-core-tools) ## Confirm required tools are installed @@ -137,7 +137,7 @@ First, check the .NET runtime with this command: dotnet --list-runtimes ``` -As you may have multiple versions of the runtime installed, make sure that .NET components with versions that start with 6.0 appear as part of the output. +As you may have multiple versions of the runtime installed, make sure that .NET components with versions that start with 8.0 appear as part of the output. Next, check the version of Azure Functions Core Tools with this command: @@ -175,24 +175,9 @@ You can try out this implementation by running the code in [GitHub Codespaces](h [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/azure-samples/cosmos-db-design-patterns?quickstart=1&devcontainer_path=.devcontainer%2Fevent-sourcing%2Fdevcontainer.json) -## Create an Azure Cosmos DB for NoSQL account - -1. If you don't already have an Azure Subscription, create a free Azure Cosmos DB for NoSQL account: () - -1. In the Data Explorer, create a new database and container with the following values: - - | | Value | - | --- | --- | - | **Database name** | `Sales` | - | **Container name** | `CartEvents` | - | **Partition key path** | `/CartId` | - | **Throughput** | `1000` (*Autoscale*) | - -**Note:** We are using shared database throughput because it can scale down to 100 RU/s when not running. This is the most cost efficient if running in a paid subscription and not using Free Tier. - ## Get Azure Cosmos DB connection information -You will need a connection string for the Azure Cosmos DB account. +You will need the connection string for the Azure Cosmos DB account for this repository. 1. Go to resource group @@ -206,7 +191,7 @@ You will need a connection string for the Azure Cosmos DB account. 1. Open the application code. -2. Add a file to the `source` folder called **local.settings.json** with the following contents: +1. Add a file to the `source` folder called **local.settings.json** with the following contents: ```json { @@ -219,9 +204,18 @@ You will need a connection string for the Azure Cosmos DB account. } ``` - Make sure to replace `YOUR_PRIMARY_CONNECTION_STRING` with the `PRIMARY CONNECTION STRING` value noted earlier. - -3. Edit **host.json** Set the `userAgentSuffix` to a value you prefer to use. This is used in tracking in Activity Monitor. See [host.json settings](https://learn.microsoft.com/azure/azure-functions/functions-bindings-cosmosdb-v2?tabs=in-process%2Cextensionv4&pivots=programming-language-csharp#hostjson-settings) for more details. +1. Replace `YOUR_PRIMARY_CONNECTION_STRING` with the `PRIMARY CONNECTION STRING` value noted earlier. +1. Modify the **Copy to Output Directory** to **Copy Always** (For VS Code add the XML below to the csproj file) +1. Save the file. + + ```xml + + + Always + Never + + + ``` ## Run the demo diff --git a/event-sourcing/source/.gitignore b/event-sourcing/source/.gitignore deleted file mode 100644 index b6d6155..0000000 --- a/event-sourcing/source/.gitignore +++ /dev/null @@ -1,267 +0,0 @@ -## Ignore Visual Studio temporary files, build results, and -## files generated by popular Visual Studio add-ons. - -# Azure Functions localsettings file -local.settings.json - -# User-specific files -*.suo -*.user -*.userosscache -*.sln.docstates - -# User-specific files (MonoDevelop/Xamarin Studio) -*.userprefs - -# Build results -[Dd]ebug/ -[Dd]ebugPublic/ -[Rr]elease/ -[Rr]eleases/ -x64/ -x86/ -bld/ -[Bb]in/ -[Oo]bj/ -[Ll]og/ - -# VS Code files -.vscode/ - -# Visual Studio 2015 cache/options directory -.vs/ -# Uncomment if you have tasks that create the project's static files in wwwroot -#wwwroot/ - -# MSTest test Results -[Tt]est[Rr]esult*/ -[Bb]uild[Ll]og.* - -# NUNIT -*.VisualState.xml -TestResult.xml - -# Build Results of an ATL Project -[Dd]ebugPS/ -[Rr]eleasePS/ -dlldata.c - -# DNX -project.lock.json -project.fragment.lock.json -artifacts/ - -*_i.c -*_p.c -*_i.h -*.ilk -*.meta -*.obj -*.pch -*.pdb -*.pgc -*.pgd -*.rsp -*.sbr -*.tlb -*.tli -*.tlh -*.tmp -*.tmp_proj -*.log -*.vspscc -*.vssscc -.builds -*.pidb -*.svclog -*.scc - -# Chutzpah Test files -_Chutzpah* - -# Visual C++ cache files -ipch/ -*.aps -*.ncb -*.opendb -*.opensdf -*.sdf -*.cachefile -*.VC.db -*.VC.VC.opendb - -# Visual Studio profiler -*.psess -*.vsp -*.vspx -*.sap - -# TFS 2012 Local Workspace -$tf/ - -# Guidance Automation Toolkit -*.gpState - -# ReSharper is a .NET coding add-in -_ReSharper*/ -*.[Rr]e[Ss]harper -*.DotSettings.user - -# JustCode is a .NET coding add-in -.JustCode - -# TeamCity is a build add-in -_TeamCity* - -# DotCover is a Code Coverage Tool -*.dotCover - -# NCrunch -_NCrunch_* -.*crunch*.local.xml -nCrunchTemp_* - -# MightyMoose -*.mm.* -AutoTest.Net/ - -# Web workbench (sass) -.sass-cache/ - -# Installshield output folder -[Ee]xpress/ - -# DocProject is a documentation generator add-in -DocProject/buildhelp/ -DocProject/Help/*.HxT -DocProject/Help/*.HxC -DocProject/Help/*.hhc -DocProject/Help/*.hhk -DocProject/Help/*.hhp -DocProject/Help/Html2 -DocProject/Help/html - -# Click-Once directory -publish/ - -# Publish Web Output -*.[Pp]ublish.xml -*.azurePubxml -# TODO: Comment the next line if you want to checkin your web deploy settings -# but database connection strings (with potential passwords) will be unencrypted -#*.pubxml -*.publishproj - -# Microsoft Azure Web App publish settings. Comment the next line if you want to -# checkin your Azure Web App publish settings, but sensitive information contained -# in these scripts will be unencrypted -PublishScripts/ - -# NuGet Packages -*.nupkg -# The packages folder can be ignored because of Package Restore -**/packages/* -# except build/, which is used as an MSBuild target. -!**/packages/build/ -# Uncomment if necessary however generally it will be regenerated when needed -#!**/packages/repositories.config -# NuGet v3's project.json files produces more ignoreable files -*.nuget.props -*.nuget.targets - -# Microsoft Azure Build Output -csx/ -*.build.csdef - -# Microsoft Azure Emulator -ecf/ -rcf/ - -# Windows Store app package directories and files -AppPackages/ -BundleArtifacts/ -Package.StoreAssociation.xml -_pkginfo.txt - -# Visual Studio cache files -# files ending in .cache can be ignored -*.[Cc]ache -# but keep track of directories ending in .cache -!*.[Cc]ache/ - -# Others -ClientBin/ -~$* -*~ -*.dbmdl -*.dbproj.schemaview -*.jfm -*.pfx -*.publishsettings -node_modules/ -orleans.codegen.cs - -# Since there are multiple workflows, uncomment next line to ignore bower_components -# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) -#bower_components/ - -# RIA/Silverlight projects -Generated_Code/ - -# Backup & report files from converting an old project file -# to a newer Visual Studio version. Backup files are not needed, -# because we have git ;-) -_UpgradeReport_Files/ -Backup*/ -UpgradeLog*.XML -UpgradeLog*.htm - -# SQL Server files -*.mdf -*.ldf - -# Business Intelligence projects -*.rdl.data -*.bim.layout -*.bim_*.settings - -# Microsoft Fakes -FakesAssemblies/ - -# GhostDoc plugin setting file -*.GhostDoc.xml - -# Node.js Tools for Visual Studio -.ntvs_analysis.dat - -# Visual Studio 6 build log -*.plg - -# Visual Studio 6 workspace options file -*.opt - -# Visual Studio LightSwitch build output -**/*.HTMLClient/GeneratedArtifacts -**/*.DesktopClient/GeneratedArtifacts -**/*.DesktopClient/ModelManifest.xml -**/*.Server/GeneratedArtifacts -**/*.Server/ModelManifest.xml -_Pvt_Extensions - -# Paket dependency manager -.paket/paket.exe -paket-files/ - -# FAKE - F# Make -.fake/ - -# JetBrains Rider -.idea/ -*.sln.iml - -# CodeRush -.cr/ - -# Python Tools for Visual Studio (PTVS) -__pycache__/ -*.pyc diff --git a/event-sourcing/source/CartEvent.cs b/event-sourcing/source/CartEvent.cs index f63d2f3..a5e2e57 100644 --- a/event-sourcing/source/CartEvent.cs +++ b/event-sourcing/source/CartEvent.cs @@ -1,12 +1,12 @@ using Newtonsoft.Json; -namespace Cosmos_Patterns_EventSourcing +namespace EventSourcing { public class CartEvent { [JsonProperty("id")] public string Id { get; set; } = Guid.NewGuid().ToString(); - public string CartId { get; set; } = Guid.NewGuid().ToString(); + public string CartId { get; set; } = Guid.NewGuid().ToString(); //Partition Key public string SessionId { get; set; } = Guid.NewGuid().ToString(); public int UserId { get; set; } public string EventType { get; set; } = ""; diff --git a/event-sourcing/source/CartItem.cs b/event-sourcing/source/CartItem.cs index 1c04b88..d75d8ab 100644 --- a/event-sourcing/source/CartItem.cs +++ b/event-sourcing/source/CartItem.cs @@ -1,4 +1,4 @@ -namespace Cosmos_Patterns_EventSourcing +namespace EventSourcing { public class CartItem { public string ProductName { get; set; } = ""; diff --git a/event-sourcing/source/Cosmos_Patterns_EventSourcing.csproj b/event-sourcing/source/Cosmos_Patterns_EventSourcing.csproj deleted file mode 100644 index 497d409..0000000 --- a/event-sourcing/source/Cosmos_Patterns_EventSourcing.csproj +++ /dev/null @@ -1,22 +0,0 @@ - - - net8.0 - v4 - enable - enable - Exe - - - - - - - - PreserveNewest - - - PreserveNewest - Never - - - diff --git a/event-sourcing/source/CosmosPatternsEventSourcingExample.cs b/event-sourcing/source/EventSourceFunction.cs similarity index 57% rename from event-sourcing/source/CosmosPatternsEventSourcingExample.cs rename to event-sourcing/source/EventSourceFunction.cs index 5e18c7a..8086ade 100644 --- a/event-sourcing/source/CosmosPatternsEventSourcingExample.cs +++ b/event-sourcing/source/EventSourceFunction.cs @@ -1,37 +1,52 @@ +using System; +using System.Collections.Generic; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; +using Microsoft.Azure.Functions.Worker; using Microsoft.Azure.WebJobs; -using Microsoft.Azure.WebJobs.Extensions.Http; -using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; using Newtonsoft.Json; -namespace Cosmos_Patterns_EventSourcing +namespace EventSourcing { - public static class CosmosPatternsEventSourcingExample + public class EventSourceFunction { - [FunctionName("CosmosPatternsEventSourcingExample")] + private readonly ILogger _logger; + + public EventSourceFunction(ILoggerFactory loggerFactory) + { + _logger = loggerFactory.CreateLogger(); + } + + [Function("EventSourcing")] + [CosmosDBOutput( + databaseName: "EventSourcingDB", + containerName: "CartEvents", + Connection = "CosmosDBConnection", + CreateIfNotExists = true, + PartitionKey = "/CartId")] public static async Task Run( [HttpTrigger(AuthorizationLevel.Anonymous, "get", "post", Route = null)] HttpRequest req, - [CosmosDB(databaseName: "Sales", - containerName: "CartEvents", - Connection="CosmosDBConnection", CreateIfNotExists=true, PartitionKey="/CartId")] IAsyncCollector cartEventOut, - ILogger log) + IAsyncCollector cartEventOut, + ILogger log) { log.LogInformation("C# HTTP trigger function processed a request."); string requestBody = await new StreamReader(req.Body).ReadToEndAsync(); string responseMessage; - + if (requestBody != null) { CartEvent cartEvent = JsonConvert.DeserializeObject(requestBody) ?? throw new ArgumentException("Request body is empty"); await cartEventOut.AddAsync(cartEvent); responseMessage = $"HTTP function successful for event {cartEvent.EventType} for cart {cartEvent.CartId}."; - } else { + } + else + { responseMessage = "No event sent in body"; } - + return new OkObjectResult(responseMessage); } } diff --git a/event-sourcing/source/EventSourcing.csproj b/event-sourcing/source/EventSourcing.csproj new file mode 100644 index 0000000..e29d614 --- /dev/null +++ b/event-sourcing/source/EventSourcing.csproj @@ -0,0 +1,32 @@ + + + net8.0 + v4 + Exe + enable + enable + + + + + + + + + + + + + + + PreserveNewest + + + Always + Never + + + + + + \ No newline at end of file diff --git a/event-sourcing/source/Program.cs b/event-sourcing/source/Program.cs index 9b73df7..9ff8426 100644 --- a/event-sourcing/source/Program.cs +++ b/event-sourcing/source/Program.cs @@ -1,20 +1,33 @@ -// See https://aka.ms/new-console-template for more information -using System.Net.Http; +using Microsoft.Azure.Functions.Worker; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; using Newtonsoft.Json; -namespace Cosmos_Patterns_EventSourcing +var host = new HostBuilder() + .ConfigureFunctionsWebApplication() + .ConfigureServices(services => + { + services.AddApplicationInsightsTelemetryWorkerService(); + services.ConfigureFunctionsApplicationInsights(); + }) + .Build(); + +host.Run(); + +namespace EventSourcing { - internal class Program + + internal class Program { static string urlBase = "http://localhost:7071"; public static async Task CreateCartEvent(HttpClient client, CartEvent cartEvent) { - var url = $"{urlBase}/api/CosmosPatternsEventSourcingExample"; + var url = $"{urlBase}/api/EventSourceFunction"; string jsonBody = JsonConvert.SerializeObject(cartEvent); var body = new StringContent(jsonBody, System.Text.Encoding.UTF8, "application/json"); - + var response = await client.PostAsync(url, body); string result = await response.Content.ReadAsStringAsync(); return result; @@ -26,7 +39,7 @@ public static List GenerateCartEvents() Random rng = new Random(); string[] actions = new string[] { - "cart_created", + "cart_created", "product_added", "product_deleted", "cart_purchased" @@ -86,15 +99,17 @@ public static List GenerateCartEvents() new CartItem("Product 1", product1Qty) }; cartEvents.Add(cartEvent); - } - } else { + } + } + else + { var cartEvent = new CartEvent(); cartEvent.CartId = cartId; cartEvent.SessionId = sessionId; cartEvent.UserId = userId; cartEvent.EventType = action; cartEvent.ProductsInCart = null; - cartEvents.Add(cartEvent); + cartEvents.Add(cartEvent); } } return cartEvents; @@ -102,8 +117,17 @@ public static List GenerateCartEvents() static async Task Main(string[] args) { - HttpClient client = new HttpClient(); - client.Timeout = TimeSpan.FromMinutes(10); + + HttpClient httpClient = new HttpClient(); + + //var services = new ServiceCollection(); + //services.AddHttpClient(); // Add the HttpClientFactory service + //services.AddLogging(); // Add the logging service + //var serviceProvider = services.BuildServiceProvider(); + + //// Resolve the HttpClient from the service provider + //var httpClient = serviceProvider.GetRequiredService(); + httpClient.Timeout = TimeSpan.FromMinutes(10); Console.WriteLine("This code will demonstrate the Event Sourcing pattern by saving shopping cart events to Azure Cosmos DB for NoSQL account."); @@ -116,14 +140,15 @@ static async Task Main(string[] args) { // Create a list of cart events List cartEvents = GenerateCartEvents(); + // Send each event to function which writes to Azure Cosmos DB for NoSQL foreach (var cartEvent in cartEvents) { - var result1 = await CreateCartEvent(client, cartEvent); + var result1 = await CreateCartEvent(httpClient, cartEvent); System.Console.WriteLine(result1); } } - + System.Console.WriteLine($"Function completed generation of shopping cart events"); Console.WriteLine($"Check CartEventContainer for new shopping cart events"); } diff --git a/event-sourcing/source/Properties/launchSettings.json b/event-sourcing/source/Properties/launchSettings.json index 04a8ece..20e96a5 100644 --- a/event-sourcing/source/Properties/launchSettings.json +++ b/event-sourcing/source/Properties/launchSettings.json @@ -1,8 +1,8 @@ { "profiles": { - "Cosmos_Patterns_EventSourcing": { + "EventSourcing": { "commandName": "Project", - "commandLineArgs": "--port 7010", + "commandLineArgs": "--port 7086", "launchBrowser": false } } diff --git a/event-sourcing/source/azuredeploy.json b/event-sourcing/source/azuredeploy.json deleted file mode 100644 index 6469275..0000000 --- a/event-sourcing/source/azuredeploy.json +++ /dev/null @@ -1,115 +0,0 @@ -{ - "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", - "contentVersion": "1.0.0.0", - "parameters": { - "location": { - "type": "string", - "defaultValue": "[resourceGroup().location]", - "metadata": { - "description": "Location for all resources." - } - }, - "accountName": { - "type": "string", - "defaultValue": "evnt-src-demo-YOUR_SUFFIX", - "metadata": { - "description": "Azure Cosmos DB account name, max length 44 characters" - } - }, - "databaseName": { - "type": "string", - "defaultValue": "Sales", - "metadata": { - "description": "The name for the database" - } - }, - "cartContainerName": { - "type": "string", - "defaultValue": "CartEvents", - "metadata": { - "description": "The name for the container for shopping cart events partitioned by cartId" - } - }, - "throughput": { - "type": "int", - "defaultValue": 400, - "metadata": { - "description": "The throughput for the container" - }, - "maxValue": 1000000, - "minValue": 400 - }, - "enableFreeTier": { - "type": "bool", - "defaultValue": false - } - }, - "resources": [ - { - "type": "Microsoft.DocumentDB/databaseAccounts", - "apiVersion": "2022-05-15", - "name": "[toLower(parameters('accountName'))]", - "kind": "GlobalDocumentDB", - "location": "[parameters('location')]", - "properties": { - "databaseAccountOfferType": "Standard", - "enableFreeTier": "[parameters('enableFreeTier')]", - "locations": [ - { - "locationName": "[parameters('location')]", - "failoverPriority": 0 - } - ] - } - }, - { - "type": "Microsoft.DocumentDB/databaseAccounts/sqlDatabases", - "apiVersion": "2022-05-15", - "name": "[format('{0}/{1}', toLower(parameters('accountName')), parameters('databaseName'))]", - "properties": { - "resource": { - "id": "[parameters('databaseName')]" - } - }, - "dependsOn": [ - "[resourceId('Microsoft.DocumentDB/databaseAccounts', toLower(parameters('accountName')))]" - ] - }, - { - "type": "Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers", - "apiVersion": "2022-05-15", - "name": "[format('{0}/{1}', format('{0}/{1}', toLower(parameters('accountName')), parameters('databaseName')), parameters('cartContainerName'))]", - "properties": { - "resource": { - "id": "[parameters('cartContainerName')]", - "partitionKey": { - "paths": [ - "/CartId" - ], - "kind": "Hash" - }, - "indexingPolicy": { - "indexingMode": "consistent", - "includedPaths": [ - { - "path": "/*" - } - ], - "excludedPaths": [ - { - "path": "/_etag/?" - } - ] - }, - "defaultTtl": 86400 - }, - "options": { - "throughput": "[parameters('throughput')]" - } - }, - "dependsOn": [ - "[resourceId('Microsoft.DocumentDB/databaseAccounts/sqlDatabases', split(format('{0}/{1}', toLower(parameters('accountName')), parameters('databaseName')), '/')[0], split(format('{0}/{1}', toLower(parameters('accountName')), parameters('databaseName')), '/')[1])]" - ] - } - ] -} \ No newline at end of file diff --git a/event-sourcing/source/host.json b/event-sourcing/source/host.json index bcd09ae..ac6ea2b 100644 --- a/event-sourcing/source/host.json +++ b/event-sourcing/source/host.json @@ -1,17 +1,18 @@ { - "version": "2.0", - "logging": { - "applicationInsights": { - "samplingSettings": { - "isEnabled": true, - "excludedTypes": "Request" - } - } - }, - "extensions": { - "cosmosDB": { - "connectionMode": "Gateway", - "userAgentSuffix": "YOUR_DESIRED_SUFFIX" - } + "version": "2.0", + "logging": { + "applicationInsights": { + "samplingSettings": { + "isEnabled": true, + "excludedTypes": "Request" + }, + "enableLiveMetricsFilters": true } + }, + "extensions": { + "cosmosDB": { + "connectionMode": "Gateway", + "userAgentSuffix": "YOUR_DESIRED_SUFFIX" + } + } } \ No newline at end of file diff --git a/event-sourcing/source/setup.md b/event-sourcing/source/setup.md deleted file mode 100644 index 1511d4d..0000000 --- a/event-sourcing/source/setup.md +++ /dev/null @@ -1,145 +0,0 @@ -# Event Sourcing Pattern Demo - -This folder contains an Azure Function that will simulate shopping cart events for an event sourcing pattern which appends events to Azure Cosmos DB. Use Program.cs to generate example events and send to an Azure function that deserializes and saves to Azure Cosmos DB. - -## CosmosPatternsEventSourcingExample function - -To run the function app for Bucketing pattern, you will need to have: - -- [.NET 6.0 Runtime](https://dotnet.microsoft.com/en-us/download/dotnet/6.0) -- [Azure Functions Core Tools](https://learn.microsoft.com/azure/azure-functions/functions-run-local#install-the-azure-functions-core-tools) - -## Confirm required tools are installed - -Confirm you have the required versions of the tools installed for this demo. - -First, check the .NET runtime with this command: - -```bash -dotnet --list-runtimes -``` - -As you may have multiple versions of the runtime installed, make sure that .NET components with versions that start with 6.0 appear as part of the output. - -Next, check the version of Azure Functions Core Tools with this command: - -```bash -func --version -``` - -You should have a version 4._x_ installed. If you do not have this version installed, you will need to uninstall the older version and follow [these instructions for installing Azure Functions Core Tools](https://learn.microsoft.com/azure/azure-functions/functions-run-local#install-the-azure-functions-core-tools). - -## Create an Azure Cosmos DB for NoSQL account - -This template will create an Azure Cosmos DB for NoSQL account with a database named `Sales` with a container named `CartEvents`. The partition key is set for `/CartId`. The data generator defaults to these values. - -The suggested account name includes 'YOUR_SUFFIX'. Change that to a suffix to make your account name unique. - -The Azure Cosmos DB for NoSQL account will automatically be created with the region of the selected resource group. - -There is an option to enable the free tier. This is so that others can try this out with minimal costs to them. - -**This link will work if this is a public repo.** - -[![Deploy to Azure](https://aka.ms/deploytoazurebutton)](https://portal.azure.com/#create/Microsoft.Template/uri/https%3A%2F%2Fraw.githubusercontent.com%2Fsolliancenet%2Fcosmos-db-nosql-modeling%2Fmain%2Fevent_sourcing%2Fcode%2Fazuredeploy.json) - -**For the private repo** - -1. [Create a custom template deployment](https://portal.azure.com/#create/Microsoft.Template). -2. Select **Build your own template in the editor**. -3. Copy the contents from [this template](azuredeploy.json) into the editor. -4. Select **Save**. - ---- - -Once the template is loaded, populate the values: - -- **Subscription** - Choose a subscription. -- **Resource group** - Choose a resource group. -- **Region** - Select a region for the instance. -- **Location** - Enter a location for the Azure Cosmos DB for NoSQL account. **Note**: By default, it is set to use the location of the resource group. If you need to change this value, you can find the supported regions for your subscription via: - - [Azure CLI](https://learn.microsoft.com/cli/azure/account?view=azure-cli-latest#az-account-list-locations) - - PowerShell: `Get-AzLocation | Where-Object {$_.Providers -contains "Microsoft.DocumentDB"} | Select location` -- **Account Name** - Replace `YOUR_SUFFIX` with a suffix to make your Azure Cosmos DB account name unique. -- **Database Name** - Set to the default **Sales**. -- **Cart Container Name** - This is the container partitioned by `/CartId`. Set to the default **CartEvents**. -- **Throughput** - Set to the default **400**. -- **Enable Free Tier** - This defaults to `false`. Set it to **true** if you want to use it as [the free tier account](https://learn.microsoft.com/azure/cosmos-db/free-tier). - -Once those settings are set, select **Review + create**, then **Create**. - -## Get Azure Cosmos DB connection information - -You will need a connection string for the Azure Cosmos DB account. - -1. Once the template deployment is complete, select **Go to resource group**. -2. Select the new Azure Cosmos DB for NoSQL account. -3. From the navigation, under **Settings**, select **Keys**. The values you need for the environment variables for the demo are here. - -## Prepare the function app configuration - -1. Add a file to the `Cosmos_Patterns_EventSourcing` folder called **local.settings.json** with the following contents: - - ```json - { - "IsEncrypted": false, - "Values": { - "AzureWebJobsStorage": "UseDevelopmentStorage=true", - "FUNCTIONS_WORKER_RUNTIME": "dotnet", - "CosmosDBConnection" : "YOUR_PRIMARY_CONNECTION_STRING" - } - } - ``` - -Make sure to replace `YOUR_PRIMARY_CONNECTION_STRING` with the `PRIMARY CONNECTION STRING` value noted earlier. - -2. Edit **host.json** Set the `userAgentSuffix` to a value you prefer to use. This is used in tracking in Activity Monitor. See [host.json settings](https://learn.microsoft.com/en-us/azure/azure-functions/functions-bindings-cosmosdb-v2?tabs=in-process%2Cextensionv4&pivots=programming-language-csharp#hostjson-settings) for more details. - -## Run the demo - -1. Start the function app to wait for HTTP calls. Each call should have a payload of a single CartEvent, then the function will save it to Cosmos DB. - -```bash -func start -``` - -To trigger the function to generate events and send to the function, you can make HTTP calls with each CartEvent sent as JSON. Review and run Program.cs to see this in action. - -Open a new terminal and run the included Console App (Program.cs) which generates simple shopping cart events: -```bash -dotnet run -``` - -## Querying the event source data -Once you have run [the demo](./code/setup.md) which generates data, you can run queries directly against the event source container by using **Data Explorer** in the Azure Portal. - -1. In Azure Portal, browse to you Azure Cosmos DB resource. -2. Select **Data Explorer** in the left menu. -3. Select your container, then choose **New SQL Query**. -![Screenshot of creating a SQL Query in Data Explorer within the Azure portal.](./images/data-explorer-create-new-query.png) - -The most common query for this append-only store is to retrieve events for a specific `CartId`, ordered by `EventTimestamp`. In this case only the latest event for a cart is needed to know the last status and what products were in the cart. - -The Console App (started with `dotnet run`) used in the demo will print out `CartId` values as it creates events. -``` -HTTP function successful for event cart_created for cart 38f4687d-35f2-4933-aadd-8776f4134589. -``` -Copy the query below and paste into the query pane in Data Explorer. **Replace the CartId value** with a GUID copied from the Console App program output. - -```sql -SELECT * -FROM CartEvents c -WHERE c.CartId = "38f4687d-35f2-4933-aadd-8776f4134589" -ORDER BY c.EventTimestamp DESC -``` - -More complex queries can be run on the events container directly. Ideally, they will still use the partitionKey to optimize costs while the change feed is used to build other views when needed. One example is if the source application did not track the `productsInCart` information. In that case the product and quantities in the cart can be derived with a slightly more complex query. This query returns a record per product with the final quantity. It filters to a specific cart and also ignores the events that do not include a product, such as cart creation or purchase. You can test this in Data Explorer, but remember to replace the CartId value with one generated by running the demo. - -```sql -SELECT c.CartId, c.UserId, c.Product, - Sum(c.QuantityChange) as Quantity -FROM CartEvents c -WHERE c.CartId = "38f4687d-35f2-4933-aadd-8776f4134589" - and IS_NULL(c.Product) = false -GROUP BY c.CartId, c.UserId, c.Product -``` \ No newline at end of file diff --git a/materialized-view/README.md b/materialized-view/README.md index 81d8edc..c00e55a 100644 --- a/materialized-view/README.md +++ b/materialized-view/README.md @@ -44,11 +44,11 @@ In essence, materialized views serve as a performance-enhancing layer that strik In this section, we will look at implementing materialized views using the change feed. -Suppose Tailspin Toys stores its sales information in Azure Cosmos DB for NoSQL. As the sales details are coming, the sales details are written to a container named `Sales` and partitioned by the `/CustomerId`. However, the eCommerce site wants to show the products that are popular now, so it wants to show products with the most sales. Rather than querying the partitions by `CustomerId`, it makes more sense to query a container partitioned by the `Product`. The Azure Cosmos DB change feed can be used to help create the materialized view to speed up queries over a year. +Tailspin Toys stores its sales information in Azure Cosmos DB for NoSQL. As the sales details are coming, the sales details are written to a container named `Sales` and partitioned by the `/CustomerId`. However, the eCommerce site wants to show the products that are popular now, so it wants to show products with the most sales. Rather than querying the partitions by `CustomerId`, it makes more sense to query a container partitioned by the `Product`. Azure Cosmos DB's Change Feed can be used to create and maintain a materialized view of the sales data for faster and more efficient queries for the most popular products. In the following diagram, there is a single Azure Cosmos DB for NoSQL account with two containers. The primary container is named **Sales** and stores the sales information. The secondary container named **SalesByProduct** stores the sales information by product, to meet Tailspin Toys' requirements for showing popular products. -![Diagram of the Azure Cosmos DB for NoSQL materialized view processing. [This demo](./code/setup.md) starts with a container Sales that holds data with one partition key. The Azure Cosmos DB change feed captures the data written to Sales, and the Azure Function processing the change feed writes the data to the SalesByDate container that is partitioned by the year.](./images/materialized-views-aggregates.png) +![Diagram of the Azure Cosmos DB for NoSQL materialized view processing. This demo starts with a container Sales that holds data with one partition key. The Azure Cosmos DB change feed captures the data written to Sales, and the Azure Function processing the change feed writes the data to the SalesByDate container that is partitioned by the year.](./images/materialized-views-aggregates.png) When implementing the materialized view pattern, there is a container for each materialized view. @@ -58,23 +58,23 @@ Why would you want to create two containers? Why does the partition key matter? SELECT c.Product, SUM(c.Qty) as NumberSold FROM c WHERE c.Product = "Widget" GROUP BY c.Product ``` -When running this query for the Sales container - the container where the source data is stored, Azure Cosmos DB will look at the WHERE clause and try to identify the partitions that contain the data filtered in the WHERE clause. When the partition key is not in the WHERE clause, Azure Cosmos DB will query all partitions. For this query, all customers may have widgets sold, so Azure Cosmos DB will query all customers' partitions for widget sales. +When running this query for the Sales container - the container where the source data is stored, Azure Cosmos DB will look at the WHERE clause and try to identify the partitions that contain the data filtered in the WHERE clause. When the partition key is not in the WHERE clause, Azure Cosmos DB will query **all the partitions**. This may be ok for small containers with 1-2 partitions (up to 100GB) or data. However, as the container grows, this query will get progressively slower and more expensive. In short, *it will not scale*. ![Diagram of the widget total query with an arrow going from the query to the Sales container partitioned by CustomerId. There are arrows going from the Sales container to each customer's partition.](images/sales-partitioned-by-customer-id.png) -However, when running the query to get the totals for a product in the SalesByProduct container, Azure Cosmos DB will only need to query one partition - the partition that holds the data for the product in the WHERE clause. +The secret to Cosmos DB is that it can scale infinitely. However, for that to occur, you have to design for it. In the scenario here, the solution is to have this query be served by a container where it only needs to access a single partition. So for our query here where we want to filter by product, the query to get the totals for a product in the SalesByProduct container, Azure Cosmos DB will only need to query one partition - the partition that holds the data for the product in the WHERE clause. ![Diagram of the widget total query with an arrow going from the query to the SalesByProduct container partitioned by Product. There is another arrow going from the container to the partition with Widget sales as it is easy to identify which partition has the Widget product's sales.](images/sales-partitioned-by-product.png) -In the demo, you may not see the performance implications with smaller sets of data - smaller in terms of the amount of data overall as well as diversity in the `CustomerId` column. However, when your data grows beyond 50 GB in storage or throughput of 10000 RU/s, you will see the performance implications at scale. +In the demo, you will not notice the performance implications with smaller sets of data - smaller in terms of the amount of data overall as well as diversity in the `CustomerId` column. However, when your data grows beyond 50 GB in storage or throughput of 10000 RU/s, you will see the performance implications at scale. Again, this is the key to why Cosmos DB can scale to handle any number of requests and any amount of data. It is designed to **scale out**. The key is the *partition key* that is used to read and write the data in the container. -**Note**: If you are running into aggregation analysis at scale, the materialized views would not be advised. For large-scale analysis, consider [Azure Cosmos DB analytical store](https://learn.microsoft.com/azure/cosmos-db/analytical-store-introduction) and [Azure Synapse Link for Azure Cosmos DB](https://learn.microsoft.com/azure/cosmos-db/synapse-link). +**Note**: If you are running into aggregation analysis at scale, the materialized views would not be advised. For large-scale analysis, consider using [Azure Cosmos DB Mirroring for Azure Fabric](https://learn.microsoft.com/fabric/database/mirrored-database/azure-cosmos-db) or [Azure Synapse Link for Azure Cosmos DB](https://learn.microsoft.com/azure/cosmos-db/synapse-link). ## Try this implementation To run this demo, you will need to have: -- [.NET 6.0 Runtime](https://dotnet.microsoft.com/download/dotnet/6.0) +- [.NET 8.0 Runtime](https://dotnet.microsoft.com/download/) - [Azure Functions Core Tools](https://learn.microsoft.com/azure/azure-functions/functions-run-local#install-the-azure-functions-core-tools) ## Confirm required tools are installed @@ -125,56 +125,45 @@ You can try out this implementation by running the code in [GitHub Codespaces](h [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/azure-samples/cosmos-db-design-patterns?quickstart=1&devcontainer_path=.devcontainer%2Fmaterialized-view%2Fdevcontainer.json) -## Create an Azure Cosmos DB for NoSQL account -1. Create a free Azure Cosmos DB for NoSQL account: () +## Set up application configuration files -1. In the Data Explorer, create a new database and container with the following values: +You need to configure the application configuration file to run these demos. - | | Value | - | --- | --- | - | **Database name** | `Sales` | - | **Container name** | `Sales` | - | **Partition key path** | `/CustomerId` | - | **Throughput** | `1000` (*Autoscale*) | +1. Go to resource group. -1. In the Data Explorer, create a second container with the following values: +1. Select the Serverless Azure Cosmos DB for NoSQL account that you created for this repository. - | | Value | - | --- | --- | - | **Database name** | `Orders` | - | **Container name** | `SalesByProduct` | - | **Partition key path** | `/Product` | - | **Throughput** | `1000` (*Autoscale*) | +1. From the navigation, under **Settings**, select **Keys**. The values you need for the application settings for the demo are here. -**Note:** We are using shared database throughput because it can scale down to 100 RU/s when not running. This is the most cost efficient if running in a paid subscription and not using Free Tier. +While on the Keys blade, make note of the `URI`, `PRIMARy KEY` and `PRIMARY CONNECTION STRING`. You will need these for the sections below. -## Get Azure Cosmos DB connection information +## Prepare the data generator configuration -You will need a connection string for the Azure Cosmos DB account. +1. Navigate to the data-generator folder, open the project and add a new **appsettings.development.json** file with the following contents: -1. Go to resource group - -1. Select the new Azure Cosmos DB for NoSQL account. - -1. From the navigation, under **Settings**, select **Keys**. The values you need for the application configuration for the demo are here. - -1. While on the Keys blade, make note of the `PRIMARY CONNECTION STRING`. You will need this for the Azure Function App. - -## Generate data - -Open the application code. + ```json + { + "CosmosUri": "", + "CosmosKey": "" + } + ``` -Run the data generator to generate sales data. +1. Replace the `CosmosURI` and `CosmosKey` with the values from the Keys blade in the Azure Portal. +1. Modify the **Copy to Output Directory** to **Copy Always** (For VS Code add the XML below to the csproj file) +1. Save the file. -```bash -cd ./data-generator -dotnet run -``` + ```xml + + + Always + + + ``` ## Prepare the function app configuration -1. Add a file to the `function-app` folder called **local.settings.json** with the following contents: +1. Open the function-app folder, open the project and add a new **local.settings.json** file with the following contents: ```json { @@ -187,9 +176,18 @@ dotnet run } ``` - Make sure to replace `YOUR_PRIMARY_CONNECTION_STRING` with the `PRIMARY CONNECTION STRING` value noted earlier. +1. Replace `YOUR_PRIMARY_CONNECTION_STRING` with the `PRIMARY CONNECTION STRING` value noted earlier. +1. Modify the **Copy to Output Directory** to **Copy Always** (For VS Code add the XML below to the csproj file) +1. Save the file. -2. Edit **host.json** Set the `userAgentSuffix` to a value you prefer to use. This is used in tracking in Activity Monitor. See [host.json settings](https://learn.microsoft.com/azure/azure-functions/functions-bindings-cosmosdb-v2?tabs=in-process%2Cextensionv4&pivots=programming-language-csharp#hostjson-settings) for more details. + ```xml + + + Always + Never + + + ``` ## Run the demo locally @@ -207,7 +205,7 @@ dotnet run As the data generator runs, switch to the function app's command window and show the logging to demonstrate what's happening using the change feed. -You can confirm the entries by looking at the Sales and SalesByProduct containers in Data Explorer in the Azure portal in the Azure Cosmos DB for NoSQL account. +You can confirm the entries by looking at the Sales and SalesByProduct containers in the MaterializedViewDB in Data Explorer in the Azure portal for this Azure Cosmos DB for NoSQL account. ## Run an in-partition query @@ -219,11 +217,11 @@ SELECT c.Product, SUM(c.Qty) as NumberSold FROM c WHERE c.Product = "Widget" GRO ### Run the query in the Sales container -Once data is loaded, you can test an in-partition query to make note of the difference in performance. +Let's test an in-partition query versus a fan-out query. Because this example is at such a small scale, the impact here is minimal. But there is a slight difference in performance we can see in the Query Stats we'll look at here. 1. Open the Azure Cosmos DB for NoSQL account in the Azure portal. 2. From the lefthand navigation, select **Data Explorer**. -3. In the NOSQL API navigation, expand the **Sales** database and the **Sales** container. +3. In the NOSQL API navigation, expand the **MaterializedViewDB** database and the **Sales** container. 4. Select the ellipsis at the end of **Items**, then select **New SQL Query**. 5. In the query window, enter the following query: @@ -239,7 +237,7 @@ Look at the values under **Query Stats**. For this demo, pay close attention to ### Run the query in the SalesByProduct container -1. In the NOSQL API Navigation, in the **Sales** database, expand the **SalesByProduct** container. +1. In the NOSQL API Navigation, in the **MaterializedViewDB** database, expand the **SalesByProduct** container. 2. Select the ellipsis at the end of **Items**, then select **New SQL Query**. 3. In the query window, enter the following query: diff --git a/materialized-view/source/azuredeploy.json b/materialized-view/source/azuredeploy.json deleted file mode 100644 index b094b5a..0000000 --- a/materialized-view/source/azuredeploy.json +++ /dev/null @@ -1,158 +0,0 @@ -{ - "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", - "contentVersion": "1.0.0.0", - "parameters": { - "location": { - "type": "string", - "defaultValue": "[resourceGroup().location]", - "metadata": { - "description": "Location for all resources." - } - }, - "accountName": { - "type": "string", - "defaultValue": "mat-view-demo-YOUR_SUFFIX", - "metadata": { - "description": "Azure Cosmos DB account name, max length 44 characters" - } - }, - "databaseName": { - "type": "string", - "defaultValue": "Sales", - "metadata": { - "description": "The name for the database" - } - }, - "customersContainerName": { - "type": "string", - "defaultValue": "Sales", - "metadata": { - "description": "The name for the container partitioned by CustomerId" - } - }, - "productsContainerName": { - "type": "string", - "defaultValue": "SalesByProduct", - "metadata": { - "description": "The name for the container partitioned by Product" - } - }, - "throughput": { - "type": "int", - "defaultValue": 400, - "metadata": { - "description": "The throughput for the container" - }, - "maxValue": 1000000, - "minValue": 400 - }, - "enableFreeTier": { - "type": "bool", - "defaultValue": false - } - }, - "resources": [ - { - "type": "Microsoft.DocumentDB/databaseAccounts", - "apiVersion": "2022-05-15", - "name": "[toLower(parameters('accountName'))]", - "kind": "GlobalDocumentDB", - "location": "[parameters('location')]", - "properties": { - "databaseAccountOfferType": "Standard", - "enableFreeTier": "[parameters('enableFreeTier')]", - "locations": [ - { - "locationName": "[parameters('location')]", - "failoverPriority": 0 - } - ] - } - }, - { - "type": "Microsoft.DocumentDB/databaseAccounts/sqlDatabases", - "apiVersion": "2022-05-15", - "name": "[format('{0}/{1}', toLower(parameters('accountName')), parameters('databaseName'))]", - "properties": { - "resource": { - "id": "[parameters('databaseName')]" - } - }, - "dependsOn": [ - "[resourceId('Microsoft.DocumentDB/databaseAccounts', toLower(parameters('accountName')))]" - ] - }, - { - "type": "Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers", - "apiVersion": "2022-05-15", - "name": "[format('{0}/{1}', format('{0}/{1}', toLower(parameters('accountName')), parameters('databaseName')), parameters('customersContainerName'))]", - "properties": { - "resource": { - "id": "[parameters('customersContainerName')]", - "partitionKey": { - "paths": [ - "/CustomerId" - ], - "kind": "Hash" - }, - "indexingPolicy": { - "indexingMode": "consistent", - "includedPaths": [ - { - "path": "/*" - } - ], - "excludedPaths": [ - { - "path": "/_etag/?" - } - ] - }, - "defaultTtl": 86400 - }, - "options": { - "throughput": "[parameters('throughput')]" - } - }, - "dependsOn": [ - "[resourceId('Microsoft.DocumentDB/databaseAccounts/sqlDatabases', split(format('{0}/{1}', toLower(parameters('accountName')), parameters('databaseName')), '/')[0], split(format('{0}/{1}', toLower(parameters('accountName')), parameters('databaseName')), '/')[1])]" - ] - }, - { - "type": "Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers", - "apiVersion": "2022-05-15", - "name": "[format('{0}/{1}', format('{0}/{1}', toLower(parameters('accountName')), parameters('databaseName')), parameters('productsContainerName'))]", - "properties": { - "resource": { - "id": "[parameters('productsContainerName')]", - "partitionKey": { - "paths": [ - "/Product" - ], - "kind": "Hash" - }, - "indexingPolicy": { - "indexingMode": "consistent", - "includedPaths": [ - { - "path": "/*" - } - ], - "excludedPaths": [ - { - "path": "/_etag/?" - } - ] - }, - "defaultTtl": 86400 - }, - "options": { - "throughput": "[parameters('throughput')]" - } - }, - "dependsOn": [ - "[resourceId('Microsoft.DocumentDB/databaseAccounts/sqlDatabases', split(format('{0}/{1}', toLower(parameters('accountName')), parameters('databaseName')), '/')[0], split(format('{0}/{1}', toLower(parameters('accountName')), parameters('databaseName')), '/')[1])]" - ] - } - ] - } \ No newline at end of file diff --git a/materialized-view/source/data-generator/Options/Cosmos.cs b/materialized-view/source/data-generator/Options/Cosmos.cs new file mode 100644 index 0000000..6d2f75a --- /dev/null +++ b/materialized-view/source/data-generator/Options/Cosmos.cs @@ -0,0 +1,9 @@ +namespace MaterializedViews.Options +{ + public class Cosmos + { + public string? CosmosUri { get; set; } + + public string? CosmosKey { get; set; } + } +} diff --git a/materialized-view/source/data-generator/Program.cs b/materialized-view/source/data-generator/Program.cs index 94745af..1942f54 100644 --- a/materialized-view/source/data-generator/Program.cs +++ b/materialized-view/source/data-generator/Program.cs @@ -1,54 +1,64 @@ -using Microsoft.Azure.Cosmos; +using MaterializedViews.Options; +using Microsoft.Azure.Cosmos; +using Microsoft.Extensions.Configuration; -namespace MaterializedViews +namespace MaterializedViews { internal class Program { - static Database? db; - + static Database? database; static Container? container; + static string databaseName = "MaterializedViewsDB"; + static string containerName = "Sales"; static string partitionKeyPath = "/CustomerId"; static void Main(string[] args) { - - Console.WriteLine("This code will generate sample sales and create them in an Azure Cosmos DB for NoSQL account."); - Console.WriteLine($"The primary key for this container will be {partitionKeyPath}.\n\n"); - Console.WriteLine("Enter the database name [default:Sales]:"); - string? userInput = Console.ReadLine(); - - string databaseName = string.IsNullOrWhiteSpace(userInput) ? "Sales" : userInput; + IConfigurationBuilder configuration = new ConfigurationBuilder() + .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true) + .AddJsonFile($"appsettings.development.json", optional: true); - CosmosClient client = new( - accountEndpoint: Environment.GetEnvironmentVariable("COSMOS_ENDPOINT")!, - authKeyOrResourceToken: Environment.GetEnvironmentVariable("COSMOS_KEY")!); + Cosmos? config = configuration + .Build() + .Get(); - db = client.CreateDatabaseIfNotExistsAsync(databaseName).Result; - Console.WriteLine("Enter the container name [default:Sales]:"); - userInput = Console.ReadLine(); + Console.WriteLine("This code will generate sample sales in the MaterializedViewsDB in the Sales container with a partition key of /CustomerId."); + Console.WriteLine("Press any key to continue."); + Console.ReadKey(); - string containerName = string.IsNullOrWhiteSpace(userInput) ? "Sales" : userInput; + CosmosClient client = new( + accountEndpoint: config?.CosmosUri, + authKeyOrResourceToken: config?.CosmosKey); + + database = client.CreateDatabaseIfNotExistsAsync(id: databaseName).Result; + container = database.CreateContainerIfNotExistsAsync(id: containerName, partitionKeyPath: partitionKeyPath).Result; + + string userInput; do { Console.WriteLine("How many sales records should be created?"); - userInput = Console.ReadLine(); + userInput = Console.ReadLine()!; int.TryParse(userInput, out int numOfSales); - container = db.CreateContainerIfNotExistsAsync(id: containerName, partitionKeyPath: partitionKeyPath, throughput: 400).Result; for (int i = 0; i < numOfSales; i++) { var order = SalesHelper.GenerateSales(); - container.CreateItemAsync(order).Wait(); + + container.CreateItemAsync( + item: order, + partitionKey: new PartitionKey(order.CustomerId)).Wait(); + } Console.WriteLine("Add more records? [y/N]"); - userInput = Console.ReadLine(); + userInput = Console.ReadLine()!; + } while (userInput != null && userInput.ToUpper() == "Y"); Console.WriteLine($"Check {containerName} for new Sales"); diff --git a/materialized-view/source/data-generator/appsettings.json b/materialized-view/source/data-generator/appsettings.json new file mode 100644 index 0000000..f699f8f --- /dev/null +++ b/materialized-view/source/data-generator/appsettings.json @@ -0,0 +1,4 @@ +{ + "CosmosUri": "", + "CosmosKey": "" +} \ No newline at end of file diff --git a/materialized-view/source/data-generator/data-generator.csproj b/materialized-view/source/data-generator/data-generator.csproj index 01f8449..cea1dc0 100644 --- a/materialized-view/source/data-generator/data-generator.csproj +++ b/materialized-view/source/data-generator/data-generator.csproj @@ -10,7 +10,20 @@ + + + + + + + Always + + + Always + + + diff --git a/materialized-view/source/function-app/MaterializeViews.csproj b/materialized-view/source/function-app/MaterializeViews.csproj index 683bee8..4ca1894 100644 --- a/materialized-view/source/function-app/MaterializeViews.csproj +++ b/materialized-view/source/function-app/MaterializeViews.csproj @@ -13,7 +13,7 @@ PreserveNewest - PreserveNewest + Always Never diff --git a/materialized-view/source/function-app/MaterializedViewProcessor.cs b/materialized-view/source/function-app/MaterializedViewProcessor.cs index 3e099ec..3fbf5be 100644 --- a/materialized-view/source/function-app/MaterializedViewProcessor.cs +++ b/materialized-view/source/function-app/MaterializedViewProcessor.cs @@ -8,21 +8,29 @@ namespace MaterializedViews public static class MaterializedViewProcessor { [FunctionName("MaterializedViewProcessor")] - public static async Task Run([CosmosDBTrigger( - databaseName: "Sales", - containerName: "Sales", - Connection = "CosmosDBConnection", - LeaseContainerName = "leases", CreateLeaseContainerIfNotExists=true)]IReadOnlyList input, - [CosmosDB(databaseName: "Sales", - containerName: "SalesByProduct", - Connection="CosmosDBConnection", CreateIfNotExists=true, PartitionKey="/Product")] IAsyncCollector salesByProduct, + public static async Task Run( + [CosmosDBTrigger( + databaseName: "MaterializedViewsDB", + containerName: "Sales", + Connection = "CosmosDBConnection", + LeaseContainerName = "leases", + CreateLeaseContainerIfNotExists=true)]IReadOnlyList input, + [CosmosDB( + databaseName: "MaterializedViewsDB", + containerName: "SalesByProduct", + Connection="CosmosDBConnection", + CreateIfNotExists=true, + PartitionKey="/Product")] IAsyncCollector salesByProduct, ILogger log) { if (input != null && input.Count > 0) { log.LogInformation("Document count: " + input.Count); + foreach (Sales document in input){ + await salesByProduct.AddAsync(new SalesByProduct(document)); + } } } diff --git a/materialized-view/source/setup.md b/materialized-view/source/setup.md deleted file mode 100644 index 51e0e13..0000000 --- a/materialized-view/source/setup.md +++ /dev/null @@ -1,200 +0,0 @@ -# Materialized Views demo - -To run this demo, you will need to have: - -- [.NET 6.0 Runtime](https://dotnet.microsoft.com/en-us/download/dotnet/6.0) -- [Azure Functions Core Tools v4](https://learn.microsoft.com/azure/azure-functions/functions-run-local#install-the-azure-functions-core-tools) - -## Confirm required tools are installed - -Confirm you have the required versions of the tools installed for this demo. - -First, check the .NET runtime with this command: - -```bash -dotnet --list-runtimes -``` - -As you may have multiple versions of the runtime installed, make sure that .NET components with versions that start with 6.0 appear as part of the output. - -Next, check the version of Azure Functions Core Tools with this command: - -```bash -func --version -``` - -You should have installed a version that starts with `4.`. If you do not have a v4 version installed, you will need to uninstall the older version and follow [these instructions for installing Azure Functions Core Tools](https://learn.microsoft.com/azure/azure-functions/functions-run-local#install-the-azure-functions-core-tools). - -## Create an Azure Cosmos DB for NoSQL account - -This template will create an Azure Cosmos DB for NoSQL account with a database named `Sales` with a container named `Sales`. The partition key is set for `/CustomerId`. The data generator defaults to these values. This will also create a container named `SalesByDate` with the partition key of `/Product`. - -The suggested account name includes 'YOUR_SUFFIX'. Change that to a suffix to make your account name unique. - -The Azure Cosmos DB for NoSQL account will automatically be created with the region of the selected resource group. - -There is an option to enable the free tier. This is so that others can try this out with minimal costs to them. - ---- - -**This link will work if this is a public repo.** - -[![Deploy to Azure](https://aka.ms/deploytoazurebutton)](https://portal.azure.com/#create/Microsoft.Template/uri/https%3A%2F%2Fraw.githubusercontent.com%2Fsolliancenet%2Fcosmos-db-nosql-modeling%2Fmain%2Fmaterialized_views%2Fcode%2Fazuredeploy.json) - -**For the private repo** - -1. [Create a custom template deployment](https://portal.azure.com/#create/Microsoft.Template/). -2. Select **Build your own template in the editor**. -3. Copy the contents from [this template](azuredeploy.json) into the editor. -4. Select **Save**. - ---- - -Once the template is loaded, populate the values: - -- **Subscription** - Choose a subscription. -- **Resource group** - Choose a resource group. -- **Region** - Select a region for the instance. -- **Location** - Enter a location for the Azure Cosmos DB for NoSQL account. **Note**: By default, it is set to use the location of the resource group. If you need to change this value, you can find the supported regions for your subscription via: - - [Azure CLI](https://learn.microsoft.com/cli/azure/account?view=azure-cli-latest#az-account-list-locations) - - PowerShell: `Get-AzLocation | Where-Object {$_.Providers -contains "Microsoft.DocumentDB"} | Select location` -- **Account Name** - Replace `YOUR_SUFFIX` with a suffix to make your Azure Cosmos DB account name unique. -- **Database Name** - Set to the default **Sales**. -- **Customers Container Name** - This is the container partitioned by `/CustomerId`. Set to the default **Sales**. -- **Products Container Name** - This is the container partitioned by `/Product`. Set to the default **SalesByProduct**. -- **Throughput** - Set to the default **400**. -- **Enable Free Tier** - This defaults to `false`. Set it to **true** if you want to use it as [the free tier account](https://learn.microsoft.com/azure/cosmos-db/free-tier). - -Once those settings are set, select **Review + create**, then **Create**. - -## Set up environment variables - -You need 2 environment variables to run these demos. - -1. Once the template deployment is complete, select **Go to resource group**. -2. Select the new Azure Cosmos DB for NoSQL account. -3. From the navigation, under **Settings**, select **Keys**. The values you need for the environment variables for the demo are here. - -Create 2 environment variables to run the demos: - -- `COSMOS_ENDPOINT`: set to the `URI` value on the Azure Cosmos DB account Keys blade. -- `COSMOS_KEY`: set to the Read-Write `PRIMARY KEY` for the Azure Cosmos DB for NoSQL account - -Create your environment variables with the following syntax: - -PowerShell: - -```powershell -$env:COSMOS_ENDPOINT="YOUR_COSMOS_ENDPOINT" -$env:COSMOS_KEY="YOUR_COSMOS_READ_WRITE_PRIMARY_KEY" -``` - -Bash: - -```bash -export COSMOS_ENDPOINT="YOUR_COSMOS_ENDPOINT" -export COSMOS_KEY="YOUR_COSMOS_KEY" -``` - -Windows Command: - -```text -set COSMOS_ENDPOINT=YOUR_COSMOS_ENDPOINT -set COSMOS_KEY=YOUR_COSMOS_KEY -``` - -While on the Keys blade, make note of the `PRIMARY CONNECTION STRING`. You will need this for the Azure Function App. - -## Generate data - -Run the data generator to generate sales data. - -```bash -cd ./data-generator -dotnet run -``` - -## Prepare the function app configuration - -1. Add a file to the `function-app` folder called **local.settings.json** with the following contents: - - ```json - { - "IsEncrypted": false, - "Values": { - "AzureWebJobsStorage": "UseDevelopmentStorage=false", - "FUNCTIONS_WORKER_RUNTIME": "dotnet", - "CosmosDBConnection" : "YOUR_PRIMARY_CONNECTION_STRING" - } - } - ``` - - Make sure to replace `YOUR_PRIMARY_CONNECTION_STRING` with the `PRIMARY CONNECTION STRING` value noted earlier. - -2. Edit **host.json** Set the `userAgentSuffix` to a value you prefer to use. This is used in tracking in Activity Monitor. See [host.json settings](https://learn.microsoft.com/en-us/azure/azure-functions/functions-bindings-cosmosdb-v2?tabs=in-process%2Cextensionv4&pivots=programming-language-csharp#hostjson-settings) for more details. - -## Run the demo locally - -1. Switch to the `function-app` folder. Then start the function with: - - ```dotnetcli - func start - ``` - -2. At another command prompt, switch to the `data-generator` folder. Run the data generator with: - - ```dotnetcli - dotnet run - ``` - -As the data generator runs, switch to the function app's command window and show the logging to demonstrate what's happening using the change feed. - -You can confirm the entries by looking at the Sales and SalesByProduct containers in Data Explorer in the Azure portal in the Azure Cosmos DB for NoSQL account. - -## Run an in-partition query - -In this part, you will look at a query meant for the Product partitioned container and how it runs in the two different containers. This is the query that will be used: - -```sql -SELECT c.Product, SUM(c.Qty) as NumberSold FROM c WHERE c.Product = "Widget" GROUP BY c.Product -``` - -### Run the query in the Sales container - -Once data is loaded, you can test an in-partition query to make note of the difference in performance. - -1. Open the Azure Cosmos DB for NoSQL account in the Azure portal. -2. From the lefthand navigation, select **Data Explorer**. -3. In the NOSQL API navigation, expand the **Sales** database and the **Sales** container. -4. Select the ellipsis at the end of **Items**, then select **New SQL Query**. -5. In the query window, enter the following query: - - ```sql - SELECT c.Product, SUM(c.Qty) as NumberSold FROM c WHERE c.Product = "Widget" GROUP BY c.Product - ``` - -6. Select **Execute Query**. - -Look at the values under **Query Stats**. For this demo, pay close attention to the **Index lookup time**. In this example, the query was run over 50 documents in the **Sales** container. The index lookup time came back at 0.17 ms - -![Screenshot of Data Explorer with the query run over the Sales container. The Sales container in navigation and the 'Index lookup time' in Query Stats are highlighted.](../images/index-lookup-type-sales.png) - -### Run the query in the SalesByProduct container - -1. In the NOSQL API Navigation, in the **Sales** database, expand the **SalesByProduct** container. -2. Select the ellipsis at the end of **Items**, then select **New SQL Query**. -3. In the query window, enter the following query: - - ```sql - SELECT c.Product, SUM(c.Qty) as NumberSold FROM c WHERE c.Product = "Widget" GROUP BY c.Product - ``` - -4. Select **Execute Query**. - -Make note of the values under **Query Stats**. These are the stats for the query when the partition key is in the WHERE clause. - -Look at the values under **Query Stats**. For this demo, pay close attention to the **Index lookup time**. In this example, the query was run over the 50 documents only in the **SalesByProduct** container. The index lookup time came back at 0.08 ms. - -![Screenshot of Data Explorer with the query run over the SalesByProduct container. The SalesByProduct container in navigation and the 'Index lookup time' in Query Stats are highlighted.](../images/index-lookup-type-salesbyproduct.png) - -When deciding what field to use for the partition key, keep in mind the queries you use and how you filter your data. A materialized view for your query can significantly improve the performance. diff --git a/preallocation/README.md b/preallocation/README.md index 5c0ade3..aea8eff 100644 --- a/preallocation/README.md +++ b/preallocation/README.md @@ -12,7 +12,7 @@ description: This is an example that shows how preallocation is used to optimize # Preallocation Pattern -The pre-allocation pattern involves creating an initial empty structure that will be populated later. This approach simplifies the design of queries and logic compared to alternative methods. However, it should be noted that pre-allocating data can result in larger storage and RU usage. +The pre-allocation pattern involves creating an initial empty structure that will be populated later. This approach simplifies the design of queries and logic compared to alternative methods. It should be noted that pre-allocating data can result in larger storage and RU usage but queries execute faster and the overall operations involved also tend to be faster as the query itself returns the necessary data versus having to manually merge data from multiple queries. This sample demonstrates: @@ -39,7 +39,7 @@ The main components of the models used in the sample include: ### Non-preallocation -In the non-preallocation pattern, you will see that a hotel is created with 10 rooms. These rooms have no reservations and the process of checking for reservations would be to query for any existing reservations and then subtracting out all the dates. +In the non-preallocation pattern, you will see that a hotel is created with 10 rooms. These rooms have no reservations and the process of checking for reservations would be to query for all the rooms in the hotel, then querying for any existing reservations, then merging both datasets and subtracting out any rooms that have reservations for the dates being searched for. - An example hotel: @@ -80,7 +80,7 @@ In the non-preallocation pattern, you will see that a hotel is created with 10 r } ``` -- An example reservation, where the room is a part of the reservation item: +- An example reservation item, where the room is a part of the reservation item: ```json { @@ -122,7 +122,7 @@ In the non-preallocation pattern, you will see that a hotel is created with 10 r ### Preallocation -In the following example you will see the reservation dates for a room being pre-allocated in a collection with a simple `IsReserved` property for each date. This will then make the process of finding available dates a bit easier when it comes to sending queries to the database. +In the following example you will see the reservation dates for a room being pre-allocated in a collection with a simple `IsReserved` property for each date. This will then make the process of finding available dates easier and faster. ```csharp DateTime start = DateTime.Parse(DateTime.Now.ToString("01/01/yyyy")); @@ -202,7 +202,7 @@ By not choosing the pre-allocation pattern, the alternative way to find availabl To run this demo, you will need to have: -- [.NET 6.0 Runtime](https://dotnet.microsoft.com/download/dotnet/6.0) +- [.NET 8.0 Runtime](https://dotnet.microsoft.com/download) ## Confirm required tools are installed @@ -214,7 +214,7 @@ First, check the .NET runtime with this command: dotnet --list-runtimes ``` -As you may have multiple versions of the runtime installed, make sure that .NET components with versions that start with 6.0 appear as part of the output. +As you may have multiple versions of the runtime installed, make sure that .NET components with versions that start with 8.0 appear as part of the output. ## Getting the code @@ -244,43 +244,46 @@ You can try out this implementation by running the code in [GitHub Codespaces](h [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/azure-samples/cosmos-db-design-patterns?quickstart=1&devcontainer_path=.devcontainer%2Fpreallocation%2Fdevcontainer.json) -## Create an Azure Cosmos DB for NoSQL account +## Set up application configuration files -1. Create a free Azure Cosmos DB for NoSQL account: () +You need to configure the application configuration file to run these demos. -1. In the Data Explorer, create a new databased named **CosmosPatterns** with shared autoscale throughput and a container **WithPreallocation**: +1. Go to resource group. - | | Value | - | --- | --- | - | **Database name** | `CosmosPatterns` | - | **Container name** | `WithPreallocation` | - | **Partition key path** | `/id` | - | **Throughput** | `1000` (*Autoscale*) | +1. Select the Serverless Azure Cosmos DB for NoSQL account that you created for this repository. -1. Create a second container in the same `CosmosPatterns` database named `WithoutPreallocation`: +1. From the navigation, under **Settings**, select **Keys**. The values you need for the application settings for the demo are here. - | | Value | - | --- | --- | - | **Database name** | `CosmosPatterns` | - | **Container name** | `WithoutPreallocation` | - | **Partition key path** | `/id` | +While on the Keys blade, make note of the `URI` and `PRIMARy KEY`. You will need these for the sections below. -**Note:** We are using shared database throughput because it can scale down to 100 RU/s when not running. This is the most cost efficient if running in a paid subscription and not using Free Tier. +1. Open the project and add a new **appsettings.development.json** file with the following contents: -## Updating Azure Cosmos DB URI and Key in Code - -1. Once the account deployment is complete, select the new Azure Cosmos DB for NoSQL account. + ```json + { + "CosmosUri": "", + "CosmosKey": "", + "DatabaseName": "PreallocationDB", + "WithPreallocation": "WithPreallocation", + "WithoutPreallocation": "WithoutPreallocation" + } + ``` -1. Open the Keys blade, click the Eye icon to view the `PRIMARY KEY`. Keep this and the `URI` handy. You will need these for the next step. -Update the following in the **appsettings.json** before you run the code: +1. Replace the `CosmosURI` and `CosmosKey` with the values from the Keys blade in the Azure Portal. +1. Modify the **Copy to Output Directory** to **Copy Always** (For VS Code add the XML below to the csproj file) +1. Save the file. -- `CosmosUri`: Set to the `URI` value on the Azure Cosmos DB account Keys blade. -- `CosmosKey`: Set to the Read-Write `PRIMARY KEY` for the Azure Cosmos DB for NoSQL account + ```xml + + + Always + + + ``` ## Run the demo 1. Open the application code. -2. From Visual Studio Code, start the app by running the following: +1. From Visual Studio Code, start the app by running the following: ```bash dotnet build @@ -290,15 +293,18 @@ Update the following in the **appsettings.json** before you run the code: or From Visual Studio, press **F5** to start the application. -3. Select option `1` in the console application to build the sample database. This option will create a database called `CosmosPatterns` which will have 2 containers called `HotelApp_containerWithPreallocation` and `HotelApp_containerWithoutPreallocation`. - 1. In Azure Portal, browse to you Cosmos DB resource. - 2. Select **Data Explorer** in the left menu. - 3. Review the data in both container, notice that the 'Reservation' documents is not used in the *HotelApp_containerWithPreallocation*, instead the reservation dates for a room are pre-allocated in a collection. +1. The application will automatically create a database called `PreallocationDB` with two containers, `WithPreallocation` and `WithoutPreallocation`. +1. Select option `1` in the console application to load the hotel and room data. + 1. In Azure Portal, browse to the Azure Cosmos DB account for this respository. + 1. Select **Data Explorer** in the left menu. + 1. Locate and open the `PreallocationDB` + 1. Review the data in both containers. Notice different structure for both containers. The 'Reservation' entity type documents are not used in the `WithPreallocation` container. Instead the reservation dates for a room are pre-allocated in an array in each room document. -4. Select option `2` to run the query with out any Preallocation. Provide a date in DD-MM-YYYY format. -5. Select option `3` to run the same query using Preallocation. Provide a date in DD-MM-YYYY format. -6. Compare the code for both Step# 4 and Step# 5. Notice that Pre-allocation allows for a much simpler design for queries and logic versus other approaches however it can come at the cost of a larger document in storage and memory given the pre-allocation of the data. +1. Select option `2` to run the query with out Preallocation. Provide a date in DD-MM-YYYY format. Note the RU Charge and elapsed time. +1. Select option `3` to run the same query using Preallocation. Provide a date in DD-MM-YYYY format. Note the RU Charge and elapsed time. +1. Compare the code for both options. Notice that Preallocation allows for faster response times. However it often comes at a cost of higher RU charge due to the larger document sizes. +1. In the `Hotel.cs` view the queries for each method. Note the simpler design for queries and application logic using Preallocation versus when not using it. ## Summary -Pre-allocation allows for a much simpler design for queries and logic versus other approaches however it can come at the cost of a larger document in storage and RU charge given the pre-allocation of the data. +Pre-allocation allows for a much simpler design for queries and logic versus other approaches. It can often yield faster reponse times as well. However it can come at the cost of a larger document in storage and RU charge given the pre-allocation of the data. diff --git a/preallocation/source/Cosmos_Patterns_Preallocation.sln b/preallocation/source/Cosmos_Patterns_Preallocation.sln deleted file mode 100644 index d264db0..0000000 --- a/preallocation/source/Cosmos_Patterns_Preallocation.sln +++ /dev/null @@ -1,25 +0,0 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 17 -VisualStudioVersion = 17.7.34009.444 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Cosmos_Patterns_Preallocation", "Cosmos_Patterns_Preallocation.csproj", "{131A20F2-19E9-4667-8E48-1167E8581D34}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Release|Any CPU = Release|Any CPU - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {131A20F2-19E9-4667-8E48-1167E8581D34}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {131A20F2-19E9-4667-8E48-1167E8581D34}.Debug|Any CPU.Build.0 = Debug|Any CPU - {131A20F2-19E9-4667-8E48-1167E8581D34}.Release|Any CPU.ActiveCfg = Release|Any CPU - {131A20F2-19E9-4667-8E48-1167E8581D34}.Release|Any CPU.Build.0 = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(ExtensibilityGlobals) = postSolution - SolutionGuid = {F2399B59-EBC4-4228-9FAB-36DFABF098BF} - EndGlobalSection -EndGlobal diff --git a/preallocation/source/Hotel.cs b/preallocation/source/Hotel.cs index c8fc8ca..9bb02f3 100644 --- a/preallocation/source/Hotel.cs +++ b/preallocation/source/Hotel.cs @@ -3,28 +3,20 @@ using System; using Container = Microsoft.Azure.Cosmos.Container; -namespace Cosmos_Patterns_Preallocation +namespace Preallocation { public class Hotel { [JsonProperty("id")] public string? Id { get; set; } - - [JsonProperty("_etag")] - public string? ETag { get; set; } - - public string? EntityType { get; set; } - public string? LeaseId { get; set; } - public DateTime? LeasedUntil { get; set; } - [JsonProperty("hotelId")] - public string? HotelId { get; set; } + public string? HotelId { get; set; } //Partition Key + public string? EntityType { get; set; } public string? Name { get; set; } public string? City { get; set; } public string? Address { get; set; } public int Rating { get; set; } public int AvailableRooms { get; set; } - public List Rooms { get; set; } public ICollection Reservations { get; set; } @@ -39,66 +31,83 @@ public Hotel() EntityType = "hotel"; } - //query db to get list of rooms - async public Task> GetRooms(Container c) + //Query the container to get list of rooms + async public Task> GetRooms(Container container) { - string query = $"select * from c where c.EntityType = 'room' and c.hotelId = '{this.HotelId}'"; + string query = $""" + SELECT * + FROM c + WHERE + c.EntityType = 'room' AND + c.hotelId = '{this.HotelId}' + """; - QueryDefinition qd = new QueryDefinition(query); + QueryDefinition queryDefinition = new QueryDefinition(query); + using FeedIterator feed = container.GetItemQueryIterator(queryDefinition: queryDefinition); List items = new List(); - - using FeedIterator feed = c.GetItemQueryIterator(queryDefinition: qd); - while (feed.HasMoreResults) { FeedResponse response = await feed.ReadNextAsync(); - items.AddRange(response); } return items; } - //query room availability for a given date - async public Task FindAvailableRooms(Container c, DateTime queryDate, string mode) + //query room availability for a given date + async public Task FindAvailableRooms(Container container, DateTime queryDate, string mode) { var requestCharge = 0.0; var executionTime = new TimeSpan(); - //very easy to find available dates... + //Very easy to find available dates using Preallocation if (mode == "preallocation") { - List items = new List(); - - //do a cosmos query that finds all hotel rooms - string query = $"SELECT a.Date, a.IsReserved, r.hotelId FROM room r JOIN a IN r.ReservationDates WHERE a.Date>= '{queryDate:o}' AND a.Date < '{queryDate.AddDays(1):o}' and a.IsReserved=false and r.hotelId = '{this.HotelId}'"; - using FeedIterator feed = c.GetItemQueryIterator(new QueryDefinition(query)); - + //Cosmos query to find hotel rooms + string query = $""" + SELECT + a.Date, a.IsReserved, room.hotelId + FROM + room room + JOIN a IN room.ReservationDates + WHERE + a.Date>= '{queryDate:o}' AND + a.Date < '{queryDate.AddDays(1):o}' AND + a.IsReserved=false AND + room.hotelId = '{this.HotelId}' + """; + + using FeedIterator feed = container.GetItemQueryIterator(new QueryDefinition(query)); + + List rooms = new List(); while (feed.HasMoreResults) { FeedResponse response = await feed.ReadNextAsync(); requestCharge += response.RequestCharge; executionTime += response.Diagnostics.GetClientElapsedTime(); - items.AddRange(response); + rooms.AddRange(response); } - - Console.WriteLine($"With preallocation: {items.Count} room(s) available on {queryDate}, query consumed {requestCharge} RU(s) and completed in {executionTime.Milliseconds} milliseconds(s). "); - - //return items; + Console.WriteLine($"With preallocation: {rooms.Count} room(s) available on {queryDate}, query consumed {requestCharge} RU(s) and completed in {executionTime.Milliseconds} milliseconds(s). "); + } - else + else //Not using Preallocation { - List availableRooms = new List(); List reservedDates = new List(); List reservations = new List(); - //get all the rooms in hotel - string query = $"select * from c where c.EntityType = 'room' and c.hotelId = '{this.HotelId}'"; - using FeedIterator feedRooms = c.GetItemQueryIterator(new QueryDefinition(query)); + //Cosmos query without Preallocation + //First, query all the rooms in hotel which is inefficient + string query = $""" + SELECT * + FROM c + WHERE c.EntityType = 'room' AND c.hotelId = '{this.HotelId}' + """; + + using FeedIterator feedRooms = container.GetItemQueryIterator(new QueryDefinition(query)); while (feedRooms.HasMoreResults) { @@ -108,9 +117,18 @@ async public Task FindAvailableRooms(Container c, DateTime queryDate, string mod availableRooms.AddRange(response); } - //get all the reservations on the given date - query = $"select * from c where c.EntityType = 'reservation' and c.hotelId = '{this.HotelId}' and c.StartDate>= '{queryDate:o}' AND c.StartDate < '{queryDate.AddDays(1):o}'"; - using FeedIterator feedReservations = c.GetItemQueryIterator(new QueryDefinition(query)); + //Next run a second query to get all the reservations on the given dates + query = $""" + SELECT * + FROM c + WHERE + c.EntityType = 'reservation' AND + c.hotelId = '{this.HotelId}' AND + c.StartDate>= '{queryDate:o}' AND + c.StartDate < '{queryDate.AddDays(1):o}' + """; + + using FeedIterator feedReservations = container.GetItemQueryIterator(new QueryDefinition(query)); while (feedReservations.HasMoreResults) { @@ -120,7 +138,7 @@ async public Task FindAvailableRooms(Container c, DateTime queryDate, string mod reservations.AddRange(response); } - //merge reservation with to remove any rooms where reservations overlap the search dates + //Now merge the data to remove any rooms where reservations overlap the search dates foreach (Reservation r in reservations) { //if room reserved on the the give date @@ -148,62 +166,61 @@ public async Task CreateReservationsAsync(DateTime start, DateTime end, string m //create some random reservations and remove available dates for (int i = 0; i < rooms.Count; i++) { - Room r = rooms[i]; + Room room = rooms[i]; - //lets assume there is 1 reservation every 25 days for each room + //lets assume there is 1 reservation every 25 days for each room int noDays = 25; DateTime targetDate = start; + while (targetDate < end) { - Reservation res = new Reservation(); - res.HotelId = this.Id; - res.Id = $"reservation_{r.Id}_{targetDate.ToString("yyyyMMdd")}"; - res.RoomId = $"{r.Id}"; - res.StartDate = targetDate; - res.EndDate = targetDate.AddDays(1); - res.Room = r; + Reservation reservation = new Reservation(); + reservation.HotelId = this.Id; + reservation.Id = $"reservation_{room.Id}_{targetDate.ToString("yyyyMMdd")}"; + reservation.RoomId = $"{room.Id}"; + reservation.StartDate = targetDate; + reservation.EndDate = targetDate.AddDays(1); + reservation.Room = room; - //additional preallocated data array is used to track available dates...faster if (mode == "preallocation") { - //set the available date to false... - foreach (var ad in r.ReservationDates.Where(r => r.Date == targetDate)) - ad.IsReserved = true; + //set the available date to false... + foreach (var availableDate in room.ReservationDates.Where(room => room.Date == targetDate)) + availableDate.IsReserved = true; //save the room data await hotelContainer.UpsertItemAsync( - item: r, - partitionKey: new PartitionKey(r.HotelId) - ); + item: room, + partitionKey: new PartitionKey(room.HotelId) + ); } else { + //save the reservation await hotelContainer.UpsertItemAsync( - item: res, - partitionKey: new PartitionKey(res.HotelId) + item: reservation, + partitionKey: new PartitionKey(reservation.HotelId) ); } targetDate = targetDate.AddDays(noDays); } - } } - public static Hotel CreateHotel(string id) { - Hotel h = new Hotel(); - h.Id = $"hotel_{id}"; - h.HotelId = h.Id; - h.Name = "Microsoft Hotels Inc"; - h.City = "Redmond"; - h.Address = "1 Microsoft Way"; - - return h; + Hotel hotel = new Hotel(); + hotel.Id = $"hotel_{id}"; + hotel.HotelId = hotel.Id; + hotel.Name = "Microsoft Hotels Inc"; + hotel.City = "Redmond"; + hotel.Address = "1 Microsoft Way"; + + return hotel; } public static List CreateRooms(Hotel h) @@ -212,10 +229,10 @@ public static List CreateRooms(Hotel h) for (int i = 0; i < maxRooms; i++) { - Room r = new Room(); - r.HotelId = h.Id; - r.Id = $"room_{i.ToString()}"; - rooms.Add(r); + Room room = new Room(); + room.HotelId = h.Id; + room.Id = $"room_{i.ToString()}"; + rooms.Add(room); } return rooms; diff --git a/preallocation/source/Options/Cosmos.cs b/preallocation/source/Options/Cosmos.cs new file mode 100644 index 0000000..34b7a75 --- /dev/null +++ b/preallocation/source/Options/Cosmos.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Preallocation.Options +{ + public record Cosmos + { + public string? CosmosUri { get; set; } + public string? CosmosKey { get; set; } + public string? DatabaseName { get; set; } + public string? WithPreallocation { get; set; } + public string? WithoutPreallocation { get; set; } + } +} diff --git a/preallocation/source/Cosmos_Patterns_Preallocation.csproj b/preallocation/source/Preallocation.csproj similarity index 71% rename from preallocation/source/Cosmos_Patterns_Preallocation.csproj rename to preallocation/source/Preallocation.csproj index 667b290..bd9cb33 100644 --- a/preallocation/source/Cosmos_Patterns_Preallocation.csproj +++ b/preallocation/source/Preallocation.csproj @@ -9,10 +9,10 @@ - - + + + - diff --git a/preallocation/source/Program.cs b/preallocation/source/Program.cs index 393df0e..7225487 100644 --- a/preallocation/source/Program.cs +++ b/preallocation/source/Program.cs @@ -1,39 +1,31 @@ using Microsoft.Azure.Cosmos; -using Microsoft.Azure.Cosmos.Serialization.HybridRow; -using Microsoft.Azure.Cosmos.Serialization.HybridRow.Schemas; -using Container = Microsoft.Azure.Cosmos.Container; -using PartitionKey = Microsoft.Azure.Cosmos.PartitionKey; using Microsoft.Extensions.Configuration; +using Preallocation.Options; using System.Globalization; -using System.ComponentModel.Design; -namespace Cosmos_Patterns_Preallocation +namespace Preallocation { internal class Program { - static CosmosClient? client; - - static Database? db; - static string WithPreallocation=string.Empty; - static string WithoutPreallocation=string.Empty; + static CosmosClient? _client; + static Container? _withPreallocationContainer; + static Container? _withoutPreallocationContainer; + static Cosmos? _config; static async Task Main(string[] args) { - IConfigurationRoot configuration = new ConfigurationBuilder() - .AddJsonFile("appsettings.json") - .AddJsonFile("appsettings.development.json") - .Build(); + IConfigurationBuilder configuration = new ConfigurationBuilder() + .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true) + .AddJsonFile("appsettings.development.json", optional: true); - var client = new CosmosClient(configuration["CosmosUri"], configuration["CosmosKey"]); - var database = configuration["Database"]; - WithPreallocation = configuration["WithPreallocation"]; - WithoutPreallocation = configuration["WithoutPreallocation"]; + _config = configuration + .Build() + .Get(); - db = await client.CreateDatabaseIfNotExistsAsync( - id: database - ); + _client = new CosmosClient(_config?.CosmosUri, _config?.CosmosKey); + await InitializeDatabase(); bool exit = false; @@ -42,13 +34,14 @@ static async Task Main(string[] args) Console.Clear(); Console.WriteLine($"Azure Cosmos DB Preallocation Design Pattern Demo"); Console.WriteLine($"-----------------------------------------------------------"); - Console.WriteLine($"[1] Create Sample Collections"); + Console.WriteLine($"[1] Load Hotel and Room Data"); Console.WriteLine($"[2] Run Query with out Preallocation"); Console.WriteLine($"[3] Run Query with Preallocation"); - Console.WriteLine($"[4] Exit\n"); + Console.WriteLine($"[4] Reset Data"); + Console.WriteLine($"[5] Exit\n"); Console.WriteLine($""); - Console.Write("Enter your choice (1-4): "); + Console.Write("Enter your choice (1-5): "); ConsoleKeyInfo result = Console.ReadKey(true); @@ -56,21 +49,28 @@ static async Task Main(string[] args) { Console.Clear(); Console.WriteLine("Creating objects without Preallocation..."); - await CreateWithNoPreallocationAsync(WithoutPreallocation); + await CreateWithNoPreallocationAsync(); Console.WriteLine("Creating objects with Preallocation..."); - await CreateWithPreallocationAsync(WithPreallocation); + await CreateWithPreallocationAsync(); } else if (result.KeyChar == '2') { - QueryContainerAsync(WithoutPreallocation,"nopreallocation").GetAwaiter().GetResult(); + QueryContainerAsync(_withoutPreallocationContainer!,"nopreallocation").GetAwaiter().GetResult(); } else if (result.KeyChar == '3') { - QueryContainerAsync(WithPreallocation, "preallocation").GetAwaiter().GetResult(); + QueryContainerAsync(_withPreallocationContainer!, "preallocation").GetAwaiter().GetResult(); } else if (result.KeyChar == '4') + { + await ResetData(); + Console.WriteLine("Data reset for containers"); + Console.WriteLine("Press Any Key to Continue.."); + Console.ReadKey(); + } + else if (result.KeyChar == '5') { Console.WriteLine("Goodbye!"); exit = true; @@ -78,39 +78,40 @@ static async Task Main(string[] args) } } - - async static Task CreateContainerIfNotExistsAsync(string containerName) + async static Task InitializeDatabase() { - return await db.CreateContainerIfNotExistsAsync( - id: containerName, - partitionKeyPath: "/hotelId", - throughput: 400 - ); + Database database = await _client!.CreateDatabaseIfNotExistsAsync(id: _config?.DatabaseName!); + + _withPreallocationContainer = await database.CreateContainerIfNotExistsAsync( + id: _config?.WithPreallocation!, + partitionKeyPath: "/hotelId"); + + _withoutPreallocationContainer = await database.CreateContainerIfNotExistsAsync( + id: _config?.WithoutPreallocation!, + partitionKeyPath: "/hotelId"); } - async static Task CreateWithNoPreallocationAsync(string containerName) + async static Task CreateWithNoPreallocationAsync() { - Container hotelContainer = await CreateContainerIfNotExistsAsync(containerName); - string mode = "nopreallocation"; //create the hotel - Hotel h = Hotel.CreateHotel("1"); + Hotel hotel = Hotel.CreateHotel("1"); //save the hotel... - await hotelContainer.UpsertItemAsync( - item: h, - partitionKey: new PartitionKey(h.HotelId) + await _withoutPreallocationContainer!.CreateItemAsync( + item: hotel, + partitionKey: new PartitionKey(hotel.HotelId) ); //create the rooms - List rooms = Hotel.CreateRooms(h); + List rooms = Hotel.CreateRooms(hotel); - foreach (Room r in rooms) - await hotelContainer.UpsertItemAsync( - item: r, - partitionKey: new PartitionKey(r.HotelId) + foreach (Room room in rooms) + await _withoutPreallocationContainer.CreateItemAsync( + item: room, + partitionKey: new PartitionKey(room.HotelId) ); //start today then go to next 365 days @@ -119,16 +120,72 @@ await hotelContainer.UpsertItemAsync( DateTime end = DateTime.Today.AddDays(365); //create random reservations. - await h.CreateReservationsAsync(start,end,mode, hotelContainer); + await hotel.CreateReservationsAsync(start, end, mode, _withoutPreallocationContainer); } - - async static Task QueryContainerAsync(string containerName, string mode) + + async static Task CreateWithPreallocationAsync() + { + //create a hotel + Hotel hotel = Hotel.CreateHotel("1"); + + //save the hotel... + await _withPreallocationContainer!.CreateItemAsync( + item: hotel, + partitionKey: new PartitionKey(hotel.Id) + ); + + //create the rooms + List rooms = Hotel.CreateRooms(hotel); + + foreach (Room room in rooms) + await _withPreallocationContainer!.CreateItemAsync( + item: room, + partitionKey: new PartitionKey(room.HotelId) + ); + + // go 365 days from now + DateTime start = DateTime.SpecifyKind(new DateTime(DateTime.Today.Year, DateTime.Today.Month, DateTime.Today.Day), DateTimeKind.Utc); + + DateTime end = DateTime.Today.AddDays(365); + + //Preallocate all the days for the year which can be queried to reserve a room later. + foreach (Room room in rooms) + { + int count = 0; + + while (start.AddDays(count) < end) + { + room.ReservationDates.Add(new ReservationDate { Date = start.AddDays(count), IsReserved = false }); + count++; + } + + try + { + //Update the room with the preallocated data + await _withPreallocationContainer!.ReplaceItemAsync( + item: room, + id: room.Id, + partitionKey: new PartitionKey(room.HotelId) + ); + } + catch (Exception ex) + { + Console.WriteLine(ex.Message); + } + } + + //create random reservations. + await hotel.CreateReservationsAsync(start, end, "preallocation", _withPreallocationContainer!); + + } + + async static Task QueryContainerAsync(Container hotelContainer, string mode) { DateTime reservationSearchDate; Console.Clear(); Console.WriteLine("Specify reservation date. (please provide a date within next 365 days in MM-dd-yyyy format)"); - string input = Console.ReadLine(); + string input = Console.ReadLine()!; if (input == "") reservationSearchDate = System.DateTime.Today; @@ -137,79 +194,22 @@ async static Task QueryContainerAsync(string containerName, string mode) //create the hotel - Hotel h = Hotel.CreateHotel("1"); + Hotel hotel = Hotel.CreateHotel("1"); - Container hotelContainer = await CreateContainerIfNotExistsAsync(containerName); //find an available room for a date using reservations... - await h.FindAvailableRooms(hotelContainer, reservationSearchDate, mode); + await hotel.FindAvailableRooms(hotelContainer, reservationSearchDate, mode); } - async static Task CreateWithPreallocationAsync(string containerName) + async static Task ResetData() { - Container hotelContainer = await CreateContainerIfNotExistsAsync(containerName); - - string mode = "preallocation"; - - //create the hotel - Hotel h = Hotel.CreateHotel("1"); - - //save the hotel... - try - { - //save the hotel... - await hotelContainer.UpsertItemAsync( - item: h, - partitionKey: new PartitionKey(h.Id) - ); - } - catch (Exception ex) - { - Console.WriteLine(ex.Message); - } - - //create the rooms - List rooms = Hotel.CreateRooms(h); - - foreach (Room r in rooms) - await hotelContainer.UpsertItemAsync( - item: r, - partitionKey: new PartitionKey(r.HotelId) - ); - - // go 365 days from now - DateTime start = DateTime.SpecifyKind(new DateTime(DateTime.Today.Year, DateTime.Today.Month, DateTime.Today.Day), DateTimeKind.Utc); - - DateTime end = DateTime.Today.AddDays(365); - - //add all the days for the year which can be queried later. - foreach (Room r in rooms) - { - int count = 0; - - while (start.AddDays(count) < end) - { - r.ReservationDates.Add(new ReservationDate { Date = start.AddDays(count), IsReserved = false }); - count++; - } - - try - { - await hotelContainer.UpsertItemAsync( - item: r, - partitionKey: new PartitionKey(r.HotelId) - ); - } - catch (Exception ex) - { - Console.WriteLine(ex.Message); - } - } - - //create random reservations. - await h.CreateReservationsAsync(start,end, "preallocation", hotelContainer); + await _withPreallocationContainer!.DeleteContainerAsync(); + await _withoutPreallocationContainer!.DeleteContainerAsync(); + await InitializeDatabase(); } + + } } \ No newline at end of file diff --git a/preallocation/source/Properties/launchSettings.json b/preallocation/source/Properties/launchSettings.json index 4a081d7..42a0f9c 100644 --- a/preallocation/source/Properties/launchSettings.json +++ b/preallocation/source/Properties/launchSettings.json @@ -1,6 +1,6 @@ { "profiles": { - "Cosomos_Patterns": { + "Preallocation": { "commandName": "Project", "environmentVariables": { diff --git a/preallocation/source/Reservation.cs b/preallocation/source/Reservation.cs index 52cac47..f4e5fff 100644 --- a/preallocation/source/Reservation.cs +++ b/preallocation/source/Reservation.cs @@ -1,6 +1,6 @@ using Newtonsoft.Json; -namespace Cosmos_Patterns_Preallocation +namespace Preallocation { public class Reservation { diff --git a/preallocation/source/Room.cs b/preallocation/source/Room.cs index 42babfd..78e22d3 100644 --- a/preallocation/source/Room.cs +++ b/preallocation/source/Room.cs @@ -1,28 +1,17 @@ using Newtonsoft.Json; -namespace Cosmos_Patterns_Preallocation +namespace Preallocation { public class Room { [JsonProperty("id")] public string? Id { get; set; } - - [JsonProperty("_etag")] - public string? ETag { get; set; } - - public string? EntityType { get; set; } - public string? LeaseId { get; set; } - public DateTime? LeasedUntil { get; set; } [JsonProperty("hotelId")] - public string? HotelId { get; set; } + public string? HotelId { get; set; } //Partition Key + public string? EntityType { get; set; } public string? Name { get; set; } - public string? Type { get; set; } - public string? Status { get; set; } - - public int NoBeds { get; set; } - public int SizeInSqFt { get; set; } public decimal Price { get; set; } public bool Available { get; set; } @@ -43,12 +32,6 @@ public class ReservationDate { public DateTime Date { get; set; } public bool IsReserved { get; set; } - - //public ReservationDate(DateTime date) - //{ - // Date = date; - // Confirmed = true; - //} } public class RoomAttibuteBased : Room diff --git a/preallocation/source/SETUP.md b/preallocation/source/SETUP.md deleted file mode 100644 index b22768b..0000000 --- a/preallocation/source/SETUP.md +++ /dev/null @@ -1,85 +0,0 @@ -# Setup - -This template will create an Azure Cosmos DB for NoSQL account with a database named `CosmosPatterns` with a container named `HotelApp`. - -The suggested account name includes 'YOUR_SUFFIX'. Change that to a suffix to make your account name unique. - -The Azure Cosmos DB for NoSQL account will automatically be created with the region of the selected resource group. - ---- - -**This link will work if this is a public repo.** - -[![Deploy to Azure](https://aka.ms/deploytoazurebutton)](https://portal.azure.com/#create/Microsoft.Template/uri/https%3A%2F%2Fraw.githubusercontent.com%2Fsolliancenet%2Fcosmos-db-nosql-modeling%2Fmain%2FCosmos_Patterns_Preallocation%2Fsetup%2Fazuredeploy.json) - -**For the private repo** - -1. [Create a custom template deployment](https://portal.azure.com/#create/Microsoft.Template/). -2. Select **Build your own template in the editor**. -3. Copy the contents from [this template](azuredeploy.json) into the editor. -4. Select **Save**. - ---- - -Once the template is loaded, populate the values: - -- **Subscription** - Choose a subscription. -- **Resource group** - Choose a resource group. -- **Region** - Select a region for the instance. -- **Location** - Enter a location for the Azure Cosmos DB for NoSQL account. **Note**: By default, it is set to use the location of the resource group. If you need to change this value, you can find the supported regions for your subscription via: - - [Azure CLI](https://learn.microsoft.com/cli/azure/account?view=azure-cli-latest#az-account-list-locations) - - PowerShell: `Get-AzLocation | Where-Object {$_.Providers -contains "Microsoft.DocumentDB"} | Select location` -- **Account Name** - Replace `YOUR_SUFFIX` with a suffix to make your Azure Cosmos DB account name unique. -- **Database Name** - Set to the default **CosmosPatterns**. -- **Hotel App Container Name** - This is the container partitioned by `/Id`. -- **Throughput** - Set to the default **400**. -- **Enable Free Tier** - This defaults to `false`. Set it to **true** if you want to use it as [the free tier account](https://learn.microsoft.com/azure/cosmos-db/free-tier). - -Once those settings are set, select **Review + create**, then **Create**. - -## Updating Azure Cosmos DB URI and Key in Code - -1. Once the template deployment is complete, select **Go to resource group**. -2. Select the new Azure Cosmos DB for NoSQL account. -3. From the navigation, under **Settings**, select **Keys**. - -Update the following in the **appsettings.json** before you run the code: - -- `CosmosUri`: Set to the `URI` value on the Azure Cosmos DB account Keys blade. -- `CosmosKey`: Set to the Read-Write `PRIMARY KEY` for the Azure Cosmos DB for NoSQL account - -## Run the demo - -1. Review the `program.cs` file. -2. Notice the following code snippets - -```csharp -Hotel h = Hotel.CreateHotel("1"); -``` - -> NOTE: This will create a new hotel object with the id. - -```csharp -List rooms = Hotel.CreateRooms(h); -``` - -> NOTE: This will create a set of rooms for a hotel, you can change the numbner of rooms (set to 10) to create by modifying the appropriate property in the class. - -3. From Visual Studio Code, start the app by running the following: - - ```bash - dotnet build - dotnet run - ``` - -4. From Visual Studio, press **F5** to start the application. - -5. Select Option 1 in the console application to create the containers and populate data in them. The code will create two containers. One that contains reservations that are used to determine open dates and another hotel that uses pre-allocation of dates. - - 1. In Azure Portal, browse to you Cosmos DB resource. - 2. Select **Data Explorer** in the left menu. - 3. Review the data in both container, notice that the 'Reservation' documents is not used in the *HotelApp_containerWithPreallocation*. - -6. Select Option 2 to run the query with out any Preallocation. Provide a date in DD--MM-YYYY format. -7. Select Option 3 to run the same query using Preallocation. Provide a date in DD--MM-YYYY format. -8. Compare the code for both Step#6 and Step#7. Notice that Pre-allocation allows for a much simpler design for queries and logic versus other approaches however it can come at the cost of a larger document in storage and memory given the pre-allocation of the data. diff --git a/preallocation/source/appsettings.json b/preallocation/source/appsettings.json index 98c8314..4a0cbe2 100644 --- a/preallocation/source/appsettings.json +++ b/preallocation/source/appsettings.json @@ -1,8 +1,7 @@ { "CosmosUri": "", "CosmosKey": "", - "Database": "CosmosPatterns", + "DatabaseName": "PreallocationDB", "WithPreallocation": "WithPreallocation", "WithoutPreallocation": "WithoutPreallocation" - } \ No newline at end of file diff --git a/preallocation/source/azuredeploy.json b/preallocation/source/azuredeploy.json deleted file mode 100644 index ee6b5b9..0000000 --- a/preallocation/source/azuredeploy.json +++ /dev/null @@ -1,115 +0,0 @@ -{ - "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", - "contentVersion": "1.0.0.0", - "parameters": { - "location": { - "type": "string", - "defaultValue": "[resourceGroup().location]", - "metadata": { - "description": "Location for all resources." - } - }, - "accountName": { - "type": "string", - "defaultValue": "preallocation-your_suffix", - "metadata": { - "description": "Azure Cosmos DB account name, max length 44 characters" - } - }, - "databaseName": { - "type": "string", - "defaultValue": "HotelDB", - "metadata": { - "description": "The name for the database" - } - }, - "containerName": { - "type": "string", - "defaultValue": "Hotels", - "metadata": { - "description": "The name for the container" - } - }, - "throughput": { - "type": "int", - "defaultValue": 400, - "metadata": { - "description": "The throughput for the container" - }, - "maxValue": 1000000, - "minValue": 400 - }, - "enableFreeTier": { - "type": "bool", - "defaultValue": false - } - }, - "resources": [ - { - "type": "Microsoft.DocumentDB/databaseAccounts", - "apiVersion": "2022-05-15", - "name": "[toLower(parameters('accountName'))]", - "kind": "GlobalDocumentDB", - "location": "[parameters('location')]", - "properties": { - "databaseAccountOfferType": "Standard", - "enableFreeTier": "[parameters('enableFreeTier')]", - "locations": [ - { - "locationName": "[parameters('location')]", - "failoverPriority": 0 - } - ] - } - }, - { - "type": "Microsoft.DocumentDB/databaseAccounts/sqlDatabases", - "apiVersion": "2022-05-15", - "name": "[format('{0}/{1}', toLower(parameters('accountName')), parameters('databaseName'))]", - "properties": { - "resource": { - "id": "[parameters('databaseName')]" - } - }, - "dependsOn": [ - "[resourceId('Microsoft.DocumentDB/databaseAccounts', toLower(parameters('accountName')))]" - ] - }, - { - "type": "Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers", - "apiVersion": "2022-05-15", - "name": "[format('{0}/{1}', format('{0}/{1}', toLower(parameters('accountName')), parameters('databaseName')), parameters('containerName'))]", - "properties": { - "resource": { - "id": "[parameters('containerName')]", - "partitionKey": { - "paths": [ - "/hotelId" - ], - "kind": "Hash" - }, - "indexingPolicy": { - "indexingMode": "consistent", - "includedPaths": [ - { - "path": "/*" - } - ], - "excludedPaths": [ - { - "path": "/_etag/?" - } - ] - }, - "defaultTtl": 86400 - }, - "options": { - "throughput": "[parameters('throughput')]" - } - }, - "dependsOn": [ - "[resourceId('Microsoft.DocumentDB/databaseAccounts/sqlDatabases', split(format('{0}/{1}', toLower(parameters('accountName')), parameters('databaseName')), '/')[0], split(format('{0}/{1}', toLower(parameters('accountName')), parameters('databaseName')), '/')[1])]" - ] - } - ] -} \ No newline at end of file diff --git a/schema-versioning/README.md b/schema-versioning/README.md index f77a11d..77769f8 100644 --- a/schema-versioning/README.md +++ b/schema-versioning/README.md @@ -134,7 +134,7 @@ When it comes to data modeling, a schema version field in a JSON document can be If you use a nullable type for the version, this will allow the developers to check for the presence of a value and act accordingly. -In [the demo](./source/setup.md), `SchemaVersion` is treated as a nullable integer with the `int?` data type. The developers added a `HasSpecialOrders()` method to help determine whether to show the special order details. This is what the Cart class looks like on the website side: +In this demo, `SchemaVersion` is treated as a nullable integer with the `int?` data type. The developers added a `HasSpecialOrders()` method to help determine whether to show the special order details. This is what the Cart class looks like on the website side: ```csharp public class Cart @@ -198,7 +198,7 @@ When you need to keep track of schema changes, use this schema versioning patter In order to run the demos, you will need: -- [.NET 6.0 Runtime](https://dotnet.microsoft.com/download/dotnet/6.0) +- [.NET 8.0 Runtime](https://dotnet.microsoft.com/download) - [Azure Functions Core Tools v4](https://learn.microsoft.com/azure/azure-functions/functions-run-local#install-the-azure-functions-core-tools) ## Confirm required tools are installed @@ -249,42 +249,77 @@ You can try out this implementation by running the code in [GitHub Codespaces](h [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/azure-samples/cosmos-db-design-patterns?quickstart=1&devcontainer_path=.devcontainer%2Fschema-versioning%2Fdevcontainer.json) -## Create an Azure Cosmos DB for NoSQL account +## Set up application configuration files -1. Create a free Azure Cosmos DB for NoSQL account: () +You need to configure **two** application configuration files to run these demos. -1. In the Data Explorer, create a new database and container with the following values: +1. Go to your resource group. - | | Value | - | --- | --- | - | **Database name** | `Sales` | - | **Container name** | `Carts` | - | **Partition key path** | `/id` | - | **Throughput** | `1000` (*Autoscale*) | +1. Select the Serverless Azure Cosmos DB for NoSQL account that you created for this repository. -**Note:** We are using shared database throughput because it can scale down to 100 RU/s when not running. This is the most cost efficient if running in a paid subscription and not using Free Tier. +1. From the navigation, under **Settings**, select **Keys**. The values you need for the application settings for the demo are here. -## Set up environment variables +While on the Keys blade, make note of the `URI` and `PRIMARY KEY`. You will need these for the sections below. -You need 2 environment variables to run these demos. +1. Open the data-generator project and add a new **appsettings.development.json** file with the following contents: -1. Go to resource group. + ```json + { + "CosmosUri": "", + "CosmosKey": "", + "DatabaseName": "SchemaVersionDB", + "ContainerName": "ShoppingCart", + "PartitionKeyPath": "/id" + } + ``` -1. Select the new Azure Cosmos DB for NoSQL account. +1. Replace the `CosmosURI` and `CosmosKey` with the values from the Keys blade in the Azure Portal. +1. Modify the **Copy to Output Directory** to **Copy Always** (For VS Code add the XML below to the csproj file) +1. Save the file. -1. From the navigation, under **Settings**, select **Keys**. The values you need for the environment variables for the demo are here. + ```xml + + + Always + + + ``` -Create 2 environment variables to run the demos: +Then repeat this for the next project. -- `COSMOS_ENDPOINT`: set to the `URI` value on the Azure Cosmos DB account Keys blade. -- `COSMOS_KEY`: set to the Read-Write `PRIMARY KEY` for the Azure Cosmos DB for NoSQL account +1. Open the website project and add a new **appsettings.development.json** file with the following contents: -Create your environment variables in a bash terminal with the following syntax: + ```json + { + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*", + "CosmosDb": { + "CosmosUri": "", + "CosmosKey": "", + "DatabaseName": "SchemaVersionDB", + "ContainerName": "ShoppingCart", + "PartitionKeyPath": "/id" + } + } + ``` + +1. Replace the `CosmosURI` and `CosmosKey` with the values from the Keys blade in the Azure Portal. +1. Modify the **Copy to Output Directory** to **Copy Always** (For VS Code add the XML below to the csproj file) +1. Save the file. + + ```xml + + + Always + + + ``` -```bash -export COSMOS_ENDPOINT="YOUR_COSMOS_ENDPOINT" -export COSMOS_KEY="YOUR_COSMOS_KEY" -``` ## Generate data diff --git a/schema-versioning/source/azuredeploy.json b/schema-versioning/source/azuredeploy.json deleted file mode 100644 index 1d7c286..0000000 --- a/schema-versioning/source/azuredeploy.json +++ /dev/null @@ -1,115 +0,0 @@ -{ - "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", - "contentVersion": "1.0.0.0", - "parameters": { - "location": { - "type": "string", - "defaultValue": "[resourceGroup().location]", - "metadata": { - "description": "Location for all resources." - } - }, - "accountName": { - "type": "string", - "defaultValue": "schema-versioning-demo-YOUR_SUFFIX", - "metadata": { - "description": "Azure Cosmos DB account name, max length 44 characters" - } - }, - "databaseName": { - "type": "string", - "defaultValue": "CartsDemo", - "metadata": { - "description": "The name for the database" - } - }, - "containerName": { - "type": "string", - "defaultValue": "Carts", - "metadata": { - "description": "The name for the container" - } - }, - "throughput": { - "type": "int", - "defaultValue": 400, - "metadata": { - "description": "The throughput for the container" - }, - "maxValue": 1000000, - "minValue": 400 - }, - "enableFreeTier": { - "type": "bool", - "defaultValue": false - } - }, - "resources": [ - { - "type": "Microsoft.DocumentDB/databaseAccounts", - "apiVersion": "2022-05-15", - "name": "[toLower(parameters('accountName'))]", - "kind": "GlobalDocumentDB", - "location": "[parameters('location')]", - "properties": { - "databaseAccountOfferType": "Standard", - "enableFreeTier": "[parameters('enableFreeTier')]", - "locations": [ - { - "locationName": "[parameters('location')]", - "failoverPriority": 0 - } - ] - } - }, - { - "type": "Microsoft.DocumentDB/databaseAccounts/sqlDatabases", - "apiVersion": "2022-05-15", - "name": "[format('{0}/{1}', toLower(parameters('accountName')), parameters('databaseName'))]", - "properties": { - "resource": { - "id": "[parameters('databaseName')]" - } - }, - "dependsOn": [ - "[resourceId('Microsoft.DocumentDB/databaseAccounts', toLower(parameters('accountName')))]" - ] - }, - { - "type": "Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers", - "apiVersion": "2022-05-15", - "name": "[format('{0}/{1}', format('{0}/{1}', toLower(parameters('accountName')), parameters('databaseName')), parameters('containerName'))]", - "properties": { - "resource": { - "id": "[parameters('containerName')]", - "partitionKey": { - "paths": [ - "/id" - ], - "kind": "Hash" - }, - "indexingPolicy": { - "indexingMode": "consistent", - "includedPaths": [ - { - "path": "/*" - } - ], - "excludedPaths": [ - { - "path": "/_etag/?" - } - ] - }, - "defaultTtl": 86400 - }, - "options": { - "throughput": "[parameters('throughput')]" - } - }, - "dependsOn": [ - "[resourceId('Microsoft.DocumentDB/databaseAccounts/sqlDatabases', split(format('{0}/{1}', toLower(parameters('accountName')), parameters('databaseName')), '/')[0], split(format('{0}/{1}', toLower(parameters('accountName')), parameters('databaseName')), '/')[1])]" - ] - } - ] - } \ No newline at end of file diff --git a/schema-versioning/source/data-generator/Options/Cosmos.cs b/schema-versioning/source/data-generator/Options/Cosmos.cs new file mode 100644 index 0000000..d4697a7 --- /dev/null +++ b/schema-versioning/source/data-generator/Options/Cosmos.cs @@ -0,0 +1,11 @@ +namespace data_generator.Options +{ + public record Cosmos + { + public string? CosmosUri { get; init; } + public string? CosmosKey { get; init; } + public string? DatabaseName { get; init; } + public string? ContainerName { get; init; } + public string? PartitionKeyPath { get; init; } + } +} diff --git a/schema-versioning/source/data-generator/Program.cs b/schema-versioning/source/data-generator/Program.cs index 7aae79e..a2c07ec 100644 --- a/schema-versioning/source/data-generator/Program.cs +++ b/schema-versioning/source/data-generator/Program.cs @@ -1,59 +1,65 @@ -using Microsoft.Azure.Cosmos; -using System.ComponentModel.DataAnnotations; +using data_generator.Options; +using Microsoft.Azure.Cosmos; +using Microsoft.Extensions.Configuration; -namespace Versioning +namespace Versioning { internal class Program { - - static Database? db; - static Container? container; + static CosmosClient? _client; + static Container? _container; + static Cosmos? _config; - static string partitionKeyPath = "/id"; - - static void Main(string[] args) + static async Task Main(string[] args) { - - Console.WriteLine("This code will generate sample carts and create them in an Azure Cosmos DB for NoSQL account."); - Console.WriteLine("The primary key for this container will be /id.\n\n"); - Console.WriteLine("Enter the database name [default:CartsDemo]:"); - string? userInput = Console.ReadLine(); - - string databaseName = string.IsNullOrWhiteSpace(userInput) ? "CartsDemo" : userInput; + IConfigurationBuilder configuration = new ConfigurationBuilder() + .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true) + .AddJsonFile("appsettings.development.json", optional: true); - CosmosClient client = new( - accountEndpoint: Environment.GetEnvironmentVariable("COSMOS_ENDPOINT")!, - authKeyOrResourceToken: Environment.GetEnvironmentVariable("COSMOS_KEY")!); + _config = configuration + .Build() + .Get(); - db = client.CreateDatabaseIfNotExistsAsync(databaseName).Result; - Console.WriteLine("Enter the container name [default:Carts]:"); - userInput = Console.ReadLine(); + _client = new CosmosClient(_config?.CosmosUri, _config?.CosmosKey); - string containerName = string.IsNullOrWhiteSpace(userInput) ? "Carts" : userInput; - + await InitializeDatabase(); + + string userInput = string.Empty; + Console.WriteLine("This code will generate sample carts and create them in an Azure Cosmos DB for NoSQL account."); + Console.WriteLine("How many carts should be created?"); - userInput = Console.ReadLine(); + userInput = Console.ReadLine()!; int.TryParse(userInput, out int numOfCarts); - container = db.CreateContainerIfNotExistsAsync(id: containerName, partitionKeyPath: partitionKeyPath, throughput: 400).Result; + for (int i = 0; i < numOfCarts; i++) { var cart = CartHelper.GenerateCart(); - container.UpsertItemAsync(cart).Wait(); + _container!.UpsertItemAsync(cart).Wait(); } for (int i = 0; i < numOfCarts; i++) { var cart = CartHelper.GenerateVersionedCart(); - container.UpsertItemAsync(cart).Wait(); + _container!.UpsertItemAsync(cart).Wait(); } - Console.WriteLine($"Check {containerName} for new carts"); + Console.WriteLine($"Check {_config!.ContainerName} container for new carts"); Console.WriteLine("Press Enter to exit."); Console.ReadKey(); } + + async static Task InitializeDatabase() + { + Database database = await _client!.CreateDatabaseIfNotExistsAsync(id: _config?.DatabaseName!); + + _container = await database.CreateContainerIfNotExistsAsync( + id: _config?.ContainerName!, + partitionKeyPath: _config?.PartitionKeyPath); + + } } } \ No newline at end of file diff --git a/schema-versioning/source/data-generator/appsettings.json b/schema-versioning/source/data-generator/appsettings.json new file mode 100644 index 0000000..4efbdc4 --- /dev/null +++ b/schema-versioning/source/data-generator/appsettings.json @@ -0,0 +1,7 @@ +{ + "CosmosUri": "", + "CosmosKey": "", + "DatabaseName": "SchemaVersionDB", + "ContainerName": "ShoppingCart", + "PartitionKeyPath": "/id" +} \ No newline at end of file diff --git a/schema-versioning/source/data-generator/data-generator.csproj b/schema-versioning/source/data-generator/data-generator.csproj index 01f8449..c282789 100644 --- a/schema-versioning/source/data-generator/data-generator.csproj +++ b/schema-versioning/source/data-generator/data-generator.csproj @@ -9,8 +9,20 @@ - + + + + + + + Always + + + Always + + + diff --git a/schema-versioning/source/setup.md b/schema-versioning/source/setup.md deleted file mode 100644 index a542bd1..0000000 --- a/schema-versioning/source/setup.md +++ /dev/null @@ -1,139 +0,0 @@ -# Schema Versioning Demo - -To run this demo for schema versioning, you will need to have: - -- [.NET 6.0 Runtime](https://dotnet.microsoft.com/en-us/download/dotnet/6.0) - -## Confirm required tools are installed - -Confirm you have the required versions of the tools installed for this demo. - -First, check the .NET runtime with this command: - -```bash -dotnet --list-runtimes -``` - -As you may have multiple versions of the runtime installed, make sure that .NET components with versions that start with 6.0 appear as part of the output. - -## Create an Azure Cosmos DB for NoSQL account - -This template will create an Azure Cosmos DB for NoSQL account with a database named `CartsDemo` with a container named `Carts`. The data generator defaults to these values. - -The suggested account name includes 'YOUR_SUFFIX'. Change that to a suffix to make your account name unique. - -The Azure Cosmos DB for NoSQL account will automatically be created with the region of the selected resource group. - -There is an option to enable the free tier. This is so that others can try this out with minimal costs to them. - ---- - -**This link will work if this is a public repo.** - -[![Deploy to Azure](https://aka.ms/deploytoazurebutton)](https://portal.azure.com/#create/Microsoft.Template/uri/https%3A%2F%2Fraw.githubusercontent.com%2Fsolliancenet%2Fcosmos-db-nosql-modeling%2Fmain%2Fschema-versioning%2Fcode%2Fazuredeploy.json) - -**For the private repo** - -1. [Create a custom template deployment](https://portal.azure.com/#create/Microsoft.Template/). -2. Select **Build your own template in the editor**. -3. Copy the contents from [this template](azuredeploy.json) into the editor. -4. Select **Save**. - ---- - -Once the template is loaded, populate the values: - -- **Subscription** - Choose a subscription. -- **Resource group** - Choose a resource group. -- **Region** - Select a region for the instance. -- **Location** - Enter a location for the Azure Cosmos DB for NoSQL account. **Note**: By default, it is set to use the location of the resource group. If you need to change this value, you can find the supported regions for your subscription via: - - [Azure CLI](https://learn.microsoft.com/cli/azure/account?view=azure-cli-latest#az-account-list-locations) - - PowerShell: `Get-AzLocation | Where-Object {$_.Providers -contains "Microsoft.DocumentDB"} | Select location` -- **Account Name** - Replace `YOUR_SUFFIX` with a suffix to make your Azure Cosmos DB account name unique. -- **Database Name** - Set to the default **Sales**. -- **Container Name** - This is the container partitioned by `/id`. Set to the default **Carts**. -- **Throughput** - Set to the default **400**. -- **Enable Free Tier** - This defaults to `false`. Set it to **true** if you want to use it as [the free tier account](https://learn.microsoft.com/azure/cosmos-db/free-tier). - -Once those settings are set, select **Review + create**, then **Create**. - -## Set up environment variables - -You need 2 environment variables to run these demos. - -1. Once the template deployment is complete, select **Go to resource group**. -2. Select the new Azure Cosmos DB for NoSQL account. -3. From the navigation, under **Settings**, select **Keys**. The values you need for the environment variables for the demo are here. - -Create 2 environment variables to run the demos: - -- `COSMOS_ENDPOINT`: set to the `URI` value on the Azure Cosmos DB account Keys blade. -- `COSMOS_KEY`: set to the Read-Write `PRIMARY KEY` for the Azure Cosmos DB for NoSQL account - -Create your environment variables with the following syntax: - -PowerShell: - -```powershell -$env:COSMOS_ENDPOINT="YOUR_COSMOS_ENDPOINT" -$env:COSMOS_KEY="YOUR_COSMOS_READ_WRITE_PRIMARY_KEY" -``` - -Bash: - -```bash -export COSMOS_ENDPOINT="YOUR_COSMOS_ENDPOINT" -export COSMOS_KEY="YOUR_COSMOS_KEY" -``` - -Windows Command: - -```text -set COSMOS_ENDPOINT=YOUR_COSMOS_ENDPOINT -set COSMOS_KEY=YOUR_COSMOS_KEY -``` - -## Generate data - -Run the data generator to generate original carts and schema-versioned carts. - -```bash -cd ./data-generator -dotnet run -``` - -The number of carts that you specify will be doubled. The generator generates the same number of original carts and versioned carts. - -The output will look something like this: - -```bash -This code will generate sample carts and create them in an Azure Cosmos DB for NoSQL account. -The primary key for this container will be /id. - - -Enter the database name [default:CartsDemo]: - -Enter the container name [default:Carts]: - -How many carts should be created? -3 -Check Carts for new carts -Press Enter to exit. -``` - -## Run the website to show generated data - -Run the website to display the carts. - -```bash -cd ./website -dotnet run -``` - -Navigate to the URL displayed in the output. In the example below, the URL is shown as part of the `info` output, following the "Now listening on:" text. - -![Screenshot of the 'dotnet run' output. The URL to navigate to is highlighted. In the screenshot, the URL is 'http://localhost:5279'.](../images/local-site-url.png) - -The output will show a variety of randomly generated carts and include the schema version when populated. When a cart contains no special items, the Special Order Notes field will not appear in the cart table. - -![Screenshot of the schema-versioned carts demo. The first cart shows 2 items with the fields Product Name and Quantity. The second cart shows 3 items with the fields for Schema Version, Product Name, Quantity, and Special Order Notes. The third cart shows 1 item with the fields for Schema Version, Product Name, Quantity, and Special Order Notes. The fourth cart shows 1 item with the fields for Schema Version, Product Name, and Quantity.](../images/schema-versioned-carts-website.png) diff --git a/schema-versioning/source/website/Models/Cart.cs b/schema-versioning/source/website/Models/Cart.cs index f50de8f..ee1ad66 100644 --- a/schema-versioning/source/website/Models/Cart.cs +++ b/schema-versioning/source/website/Models/Cart.cs @@ -1,6 +1,7 @@ using Newtonsoft.Json; -namespace Versioning{ +namespace Versioning.Models +{ public class Cart { [JsonProperty("id")] @@ -9,8 +10,9 @@ public class Cart public long CustomerId { get; set; } public List? Items { get; set;} public int? SchemaVersion {get; set;} - public bool HasSpecialOrders() { - return this.Items.Where(x=>x.IsSpecialOrder == true).Count() > 0; + public bool HasSpecialOrders() + { + return this.Items!.Where(x=>x.IsSpecialOrder == true).Count() > 0; } } } \ No newline at end of file diff --git a/schema-versioning/source/website/Models/CartItem.cs b/schema-versioning/source/website/Models/CartItem.cs index a53a5d8..ac76cfe 100644 --- a/schema-versioning/source/website/Models/CartItem.cs +++ b/schema-versioning/source/website/Models/CartItem.cs @@ -1,4 +1,4 @@ -namespace Versioning +namespace Versioning.Models { public class CartItem { public string ProductName { get; set; } = ""; diff --git a/schema-versioning/source/website/Models/CartItemWithSpecialOrder.cs b/schema-versioning/source/website/Models/CartItemWithSpecialOrder.cs index 7af0024..39de9c4 100644 --- a/schema-versioning/source/website/Models/CartItemWithSpecialOrder.cs +++ b/schema-versioning/source/website/Models/CartItemWithSpecialOrder.cs @@ -1,4 +1,4 @@ -namespace Versioning +namespace Versioning.Models { public class CartItemWithSpecialOrder : CartItem { public bool IsSpecialOrder { get; set; } = false; diff --git a/schema-versioning/source/website/Options/CosmosDb.cs b/schema-versioning/source/website/Options/CosmosDb.cs new file mode 100644 index 0000000..86fa19c --- /dev/null +++ b/schema-versioning/source/website/Options/CosmosDb.cs @@ -0,0 +1,12 @@ +namespace Versioning.Options +{ + public record CosmosDb + { + public required string CosmosUri { get; init; } + public required string CosmosKey { get; init; } + public required string DatabaseName { get; init; } + public required string ContainerName { get; init; } + public required string PartitionKeyPath { get; init; } + + }; +} diff --git a/schema-versioning/source/website/Pages/Index.cshtml b/schema-versioning/source/website/Pages/Index.cshtml index 79a40bb..a668c95 100644 --- a/schema-versioning/source/website/Pages/Index.cshtml +++ b/schema-versioning/source/website/Pages/Index.cshtml @@ -1,6 +1,7 @@ @page @model IndexModel @using Versioning +@using Versioning.Models @{ ViewData["Title"] = "Home page"; } @@ -23,7 +24,7 @@ } - @foreach (var item in cart.Items) + @foreach (var item in cart.Items!) { @if(cart.SchemaVersion != null){ diff --git a/schema-versioning/source/website/Pages/Index.cshtml.cs b/schema-versioning/source/website/Pages/Index.cshtml.cs index 62155da..cbba1c1 100644 --- a/schema-versioning/source/website/Pages/Index.cshtml.cs +++ b/schema-versioning/source/website/Pages/Index.cshtml.cs @@ -1,21 +1,25 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.RazorPages; -using Versioning; +using Versioning.Models; +using Versioning.Services; namespace website.Pages; public class IndexModel : PageModel { - public List Carts = new(); + public List Carts; private readonly ILogger _logger; + private readonly CartService _cartService; - public IndexModel(ILogger logger) + public IndexModel(ILogger logger, CartService cartService) { _logger = logger; + _cartService = cartService; + Carts = new List(); } public void OnGet() { - Carts = CartHelper.RetrieveAllCartsAsync().Result.ToList(); + Carts = _cartService.RetrieveAllCartsAsync().Result.ToList(); } } diff --git a/schema-versioning/source/website/Program.cs b/schema-versioning/source/website/Program.cs index bc275e4..4fc6dc2 100644 --- a/schema-versioning/source/website/Program.cs +++ b/schema-versioning/source/website/Program.cs @@ -1,25 +1,72 @@ +using Microsoft.Extensions.Options; +using Versioning.Options; +using Versioning.Services; + var builder = WebApplication.CreateBuilder(args); // Add services to the container. +builder.RegisterConfiguration(); +builder.Services.RegisterServices(); builder.Services.AddRazorPages(); - +builder.Services.AddServerSideBlazor(); var app = builder.Build(); -// Configure the HTTP request pipeline. if (!app.Environment.IsDevelopment()) { app.UseExceptionHandler("/Error"); - // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts. app.UseHsts(); } app.UseHttpsRedirection(); app.UseStaticFiles(); - app.UseRouting(); +app.MapRazorPages(); +app.Run(); -app.UseAuthorization(); +static class ProgramExtensions +{ + public static void RegisterConfiguration(this WebApplicationBuilder builder) + { + builder.Configuration.AddJsonFile("appsettings.json"); + builder.Configuration.AddJsonFile($"appsettings.development.json", optional: true); -app.MapRazorPages(); + builder.Services.AddOptions() + .Bind(builder.Configuration.GetSection(nameof(CosmosDb))); + + } + public static void RegisterServices(this IServiceCollection services) + { + services.AddSingleton((provider) => + { + var cosmosOptions = provider.GetRequiredService>(); + if (cosmosOptions is null) + { + throw new ArgumentException($"{nameof(IOptions)} was not resolved through dependency injection."); + } + else + { + return new CosmosDbService( + cosmosUri: cosmosOptions.Value?.CosmosUri ?? string.Empty, + cosmosKey: cosmosOptions.Value?.CosmosKey ?? string.Empty, + databaseName: cosmosOptions.Value?.DatabaseName ?? string.Empty, + containerName: cosmosOptions.Value?.ContainerName ?? string.Empty, + partitionKeyPath: cosmosOptions.Value?.PartitionKeyPath ?? string.Empty + ); + } + + }); + services.AddSingleton((provider) => + { + var cosmosDb = provider.GetRequiredService(); + if (cosmosDb is null) + { + throw new ArgumentException($"{nameof(CosmosDbService)} was not resolved through dependency injection."); + } + else + { + return new CartService(cosmosDb); + } + }); + } +} -app.Run(); diff --git a/schema-versioning/source/website/Properties/launchSettings.json b/schema-versioning/source/website/Properties/launchSettings.json index f3115d1..3e96c43 100644 --- a/schema-versioning/source/website/Properties/launchSettings.json +++ b/schema-versioning/source/website/Properties/launchSettings.json @@ -8,16 +8,7 @@ } }, "profiles": { - "http": { - "commandName": "Project", - "dotnetRunMessages": true, - "launchBrowser": true, - "applicationUrl": "http://localhost:5279", - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development" - } - }, - "https": { + "SchemaVersioning": { "commandName": "Project", "dotnetRunMessages": true, "launchBrowser": true, diff --git a/schema-versioning/source/website/Services/CartHelper.cs b/schema-versioning/source/website/Services/CartHelper.cs deleted file mode 100644 index 55cf72b..0000000 --- a/schema-versioning/source/website/Services/CartHelper.cs +++ /dev/null @@ -1,28 +0,0 @@ -using Microsoft.Azure.Cosmos; - -namespace Versioning; - -public class CartHelper{ - - public CartHelper(){} - - public async static Task> RetrieveAllCartsAsync(){ - CosmosHelper cosmosHelper = new(); - Container container = cosmosHelper.GetContainer().Result; - List carts = new(); - using FeedIterator feed = container.GetItemQueryIterator( - queryText: "SELECT * FROM Carts" - ); - while (feed.HasMoreResults) - { - FeedResponse response = await feed.ReadNextAsync(); - - // Iterate query results - foreach (Cart cart in response) - { - carts.Add(cart); - } - } - return carts; - } -} \ No newline at end of file diff --git a/schema-versioning/source/website/Services/CartService.cs b/schema-versioning/source/website/Services/CartService.cs new file mode 100644 index 0000000..4968de2 --- /dev/null +++ b/schema-versioning/source/website/Services/CartService.cs @@ -0,0 +1,37 @@ +using Microsoft.Azure.Cosmos; +using Versioning.Models; + +namespace Versioning.Services +{ + public class CartService{ + + private readonly CosmosDbService _cosmosDbService; + + public CartService(CosmosDbService cosmosDbService) + { + _cosmosDbService = cosmosDbService; + } + + public async Task> RetrieveAllCartsAsync() + { + + List carts = new(); + + using FeedIterator feed = _cosmosDbService.CartsContainer.GetItemQueryIterator( + queryText: "SELECT * FROM Carts" + ); + + while (feed.HasMoreResults) + { + FeedResponse response = await feed.ReadNextAsync(); + + // Iterate query results + foreach (Cart cart in response) + { + carts.Add(cart); + } + } + return carts; + } + } +} \ No newline at end of file diff --git a/schema-versioning/source/website/Services/CosmosDbService.cs b/schema-versioning/source/website/Services/CosmosDbService.cs new file mode 100644 index 0000000..b5ab53e --- /dev/null +++ b/schema-versioning/source/website/Services/CosmosDbService.cs @@ -0,0 +1,33 @@ +using Microsoft.Azure.Cosmos; + +namespace Versioning.Services +{ + public class CosmosDbService + { + private readonly CosmosClient _client; + private readonly Container _container; + + public Container CartsContainer => _container ?? throw new InvalidOperationException("Carts Container is not initialized."); + + public CosmosDbService(string cosmosUri, string cosmosKey, string databaseName, string containerName, string partitionKeyPath) + { + _client = new( + accountEndpoint: cosmosUri, + authKeyOrResourceToken: cosmosKey); + + _container = InitializeAsync(databaseName, containerName, partitionKeyPath).Result; + } + + private async Task InitializeAsync(string databaseName, string containerName, string partitionKeyPath) + { + Database database = await _client.CreateDatabaseIfNotExistsAsync(id: databaseName); + + Container container = await database.CreateContainerIfNotExistsAsync( + id: containerName, + partitionKeyPath: partitionKeyPath + ); + + return container; + } + } +} \ No newline at end of file diff --git a/schema-versioning/source/website/Services/CosmosHelper.cs b/schema-versioning/source/website/Services/CosmosHelper.cs deleted file mode 100644 index 679ca59..0000000 --- a/schema-versioning/source/website/Services/CosmosHelper.cs +++ /dev/null @@ -1,41 +0,0 @@ -using Microsoft.Azure.Cosmos; - -namespace Versioning { - public class CosmosHelper - { - CosmosClient client; - - private static string DatabaseName = "CartsDemo"; - private static string ContainerName = "Carts"; - private static string PartitionKey = "/id"; - - public CosmosHelper() - { - client = new( - accountEndpoint: Environment.GetEnvironmentVariable("COSMOS_ENDPOINT")!, - authKeyOrResourceToken: Environment.GetEnvironmentVariable("COSMOS_KEY")!); - } - - async public Task GetDatabase() - { - Database database = await client.CreateDatabaseIfNotExistsAsync( - id: DatabaseName - ); - - return database; - } - - async public Task GetContainer() - { - Database database = await GetDatabase(); - - Container container = await database.CreateContainerIfNotExistsAsync( - id: ContainerName, - partitionKeyPath: PartitionKey, - throughput: 400 - ); - - return container; - } - } -} \ No newline at end of file diff --git a/schema-versioning/source/website/appsettings.json b/schema-versioning/source/website/appsettings.json index 10f68b8..94ca525 100644 --- a/schema-versioning/source/website/appsettings.json +++ b/schema-versioning/source/website/appsettings.json @@ -5,5 +5,12 @@ "Microsoft.AspNetCore": "Warning" } }, - "AllowedHosts": "*" -} + "AllowedHosts": "*", + "CosmosDb": { + "CosmosUri": "", + "CosmosKey": "", + "DatabaseName": "SchemaVersionDB", + "ContainerName": "ShoppingCart", + "PartitionKeyPath": "/id" + } + } diff --git a/schema-versioning/source/website/website.csproj b/schema-versioning/source/website/website.csproj index ca41b84..4152621 100644 --- a/schema-versioning/source/website/website.csproj +++ b/schema-versioning/source/website/website.csproj @@ -7,8 +7,20 @@ - + + + + + + + Always + + + Always + + +