Go Serverless with Blazor, CosmosDB and Functions on Azure

I find this setup to be quick, satisfying and scalable. This both minimizes cost during development or low usage times and handles spikes in traffic for production apps. You can get setup very quickly using Azure's free tier web apps and the CosmosDB free credit.

I have absolutely fallen in love with Blazor. I have really tried hard to learn SPA frameworks in the past, the only issue I always had was I am not a big JavaScript fan. TypeScript made things a little more tolerable but I have always felt more comfortable working with C#. Blazor allows you to write Single Page Applications (SPA) with C# exclusively. Using the Blazor WebAssembly App project type we can download the entire application to the client's browser and use HTTPClient calls to your Function API for all data interaction or business logic. The end result is a very smooth experience for the user.

The GitHub for this tutorial can be found Here.

Let's get started by building our project and setting up our Cosmos DB instance. We will setup the Function web app and WebAssembly web app once we are up and running.

Create Functions Project

  1. Open Visual Studio and Select New Project.
  2. Search for template Azure Functions. image.png
  3. Name your project and choose your location. I am naming mine BlogPostProject.
  4. Choose Azure Function v3, Http Trigger, and Anonymous for Authorization Level. image.png
  5. Add reference to our CosmosDB helper by right clicking on Dependencies and choosing Manage Nuget Packages. Search for Microsoft.Azure.WebJobs.Extensions.CosmosDB and install. image.png image.png
  6. You should now have a Function project as well as a solution setup that we will use.

Create Blazor WASM UI Project

  1. Right click on your solution in solution explorer, click Add>New Project
  2. Search for the Blazor template image.png
  3. Name this after your Solution and Function name but add .UI to the end. So mine would be BlogPostProject.UI
  4. Choose Blazor WebAssembly App. You can select Authentication if you plan to add it eventually but for the purposes of this walkthrough I am not selecting it. I am also choosing .NET 5 but you can use Core 3.1 if you would like. image.png
  5. One last step before we get into this further, let's setup our solution to launch both projects when we run. Right click your solution and choose properties. Choose multiple projects and set action to start on each project. image.png

Created our Shared Class Library

  1. Right click the solution again and choose Add>New Project.
  2. Search for .Net Core Class Library image.png
  3. Name it SolutionName.Common. Mine is BlogPostProject.Common. Click Create.
  4. Now we need to add a reference to this new common library to both the UI and Function project.
  5. Right click the dependencies on each project and choose Add Project Reference image.png
  6. Check your Common library image.png
  7. Make sure to do this in both of your projects, we will put our classes into Common so both the Functions Project and UI can access them without having to directly connect the 2 projects.

Great we should now have our development environment setup. It should look something like this right now. image.png

Setup and Configure our CosmosDB

  1. Go to your Azure Dashboard, if you haven't created an account yet please take a moment to do so.
  2. I like to create a New Dashboard whenever I start a new project to keep all the services in one place and easier to select and visualize. Click the Hamburger menu and choose Dashboard. image.png
  3. Click New Dashboard>Blank Dashboard. Name it then click Done Customizing>Save. You should now be at an empty Dashboard. image.png
  4. Click the Hamburger again and choose Azure Cosmos DB. Then click New.
  5. On this screen I typically select my existing Subscription Plan which you should have setup already. Then I Create a New Resource Group which we will use for all of our Azure items. I typically name is whatever the project I am working on is. Account Name I also name after the current project. So in my case blogpostproject. This one needs to be lower case. For API choose Core (SQL), choose the location that is closest to you. Typically on a production application I would go Serverless at this point but for development I choose Provisioned throughput so I can take advantage of the Apply Free Tier Discount. Note you only get 1 per account that is why mine is currently grayed out. image.png
  6. Click Review + create. Then Click Create
  7. Now we wait, it takes a few minutes to fire up the instance.
  8. Once deployment is complete click the bell icon and pin it to the Dashboard. image.png

Configure CosmosDB

  1. Click Dashboard in the top left under the menu bar. image.png
  2. You should now see our new database. Click on the database. image.png
  3. This can seem a bit overwhelming at first if you haven't done much work in Azure but I will explain the basic things you need to modify. Click Data Explorer on the left. image.png
  4. Click New Container image.png
  5. Now we can create our Database and first Container. Make sure Create New is selected and choose a Database Name. I will name mine BlogPostProjectDB. Go down to Container Id and put Todos in this field, I am going to walk us through building a simple Todo list application, this will be our first table for lack of a better word. It is actually JSON stored in a storage blob but for the sake of simplicity I refer to it as a table in SQL. In Partition Key put /id, this will be the unique identifier. At this point we can click OK. image.png
  6. Now in our SQL API windows we can see a new container is added called Todos. Notice we did not specify any additional fields, just the id, this is fine as we will be defining what this will look like when we insert our first Todo. image.png
  7. No we need to click on Keys in the left side menu. Copy the string in the Primary Connection String key. We will need this for our function project to point at the right database. image.png image.png
  8. Go back to our Visual Studio project and open the local.settings.json in your Function project. Then add our connection string to the Values section. Make sure you include your AccountKey in yours.
    {
    "IsEncrypted": false,
    "Values": {
     "AzureWebJobsStorage": "UseDevelopmentStorage=true",
     "FUNCTIONS_WORKER_RUNTIME": "dotnet",
     "DBConnectionString": "AccountEndpoint=https://blogpostproject.documents.azure.com:443/;AccountKey=;"
    }
    }
    

Setup Functions API calls

Let's get our API up and running, we will start with a Get call and a Post call so we can get our Todos and Add new Todos.

  1. In your Functions project you can delete Function1.cs, we will be creating our own functions. Then create a New Folder in the project called Todos. Then create a New Class in this folder called TodoApi.cs. Open our new class file. image.png
  2. Now lets quickly create our object in the Common application. We can delete Class1.cs as we won't be needing that. Create a New Folder in the project named Models and right click the folder to create a new class called Todo.cs. image.png
  3. Put the following in Todo.cs
    public class Todo
     {
         public string id { get; set; }
         public string Name { get; set; }
         public bool Complete { get; set; }
         public DateTime DateAdded { get; set; }
     }
    
  4. Now we can go back to our Functions project and open TodoApi.cs copy and paste the following for our getter.
    [FunctionName("GetTodos")]
         public static async Task<IActionResult> Run(
             [HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = null)]
             HttpRequest req,
             [CosmosDB(
                 databaseName: "BlogPostProjectDB",
                 collectionName: "Todos",
                 ConnectionStringSetting = "DBConnectionString"
             )]
             IEnumerable<Todo> todoSet,
             ILogger log)
         {
             log.LogInformation("Data fetched from Todos");
             return new OkObjectResult(todoSet);
         }
     }
    
    Make sure you change the databaseName to the name you chose when you created your first container. What we are doing here is using a HttpRequest and the CosmosDB extension to pull all records from the Todos container and we are returning and Ok result with the List of Todos attached. Right now there are no records to return but let's get our add, edit and delete added.
  5. Lets add our Add Method now.

    [FunctionName("PostTodo")]
         public static async Task<IActionResult> RunPost(
             [HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = null)]
             HttpRequest req,
             [CosmosDB(
                 databaseName: "BlogPostProjectDB",
                 collectionName: "Todos",
                 ConnectionStringSetting = "DBConnectionString"
             )]
             DocumentClient client,
             ILogger log)
         {
             log.LogInformation("Posting to Todos");
             var content = await new StreamReader(req.Body).ReadToEndAsync();
             var newDocument = JsonConvert.DeserializeObject<Todo>(content);
    
             var collectionUri = UriFactory.CreateDocumentCollectionUri("BlogPostProjectDB", "Todos");
             var createdDocument = await client.CreateDocumentAsync(collectionUri, newDocument);
    
             return new OkObjectResult(createdDocument.Resource);
         }
    

    When we add, we use a DocumentClient instead of a HttpRequest. We grab the object we are sending to the API and Deserialize it so we can then push the raw JSON to the database. Make sure to once again change the databaseName in both spots. There is an additional spot when we create the collectionUri.

  6. Lets add our Update Method*

    [FunctionName("PutTodo")]
         public static async Task<IActionResult> RunPut(
             [HttpTrigger(AuthorizationLevel.Anonymous, "put", Route = null)]
             HttpRequest req,
             [CosmosDB(
                 databaseName: "BlogPostProjectDB",
                 collectionName: "Todos",
                 ConnectionStringSetting = "DBConnectionString"
             )]
             DocumentClient client,
             ILogger log)
         {
             log.LogInformation("Editing Todos");
             var content = await new StreamReader(req.Body).ReadToEndAsync();
             var documentUpdate = JsonConvert.DeserializeObject<Todo>(content);
    
             Uri collectionUri = UriFactory.CreateDocumentCollectionUri("BlogPostProjectDB", "Todos");
    
             var feedOptions = new FeedOptions { EnableCrossPartitionQuery = true };
             var existingDocument = client.CreateDocumentQuery<Todo>(collectionUri, feedOptions)
                 .Where(d => d.id == documentUpdate.id)
                 .AsEnumerable().FirstOrDefault();
    
             if (existingDocument == null)
             {
                 log.LogWarning($"Todo: {documentUpdate.id} not found.");
                 return new BadRequestObjectResult($"Todo not found.");
             }
    
             var documentUri = UriFactory.CreateDocumentUri("BlogPostProjectDB", "Todos", documentUpdate.id);
             await client.ReplaceDocumentAsync(documentUri, documentUpdate);
    
             return new OkObjectResult(documentUpdate);
         }
    

    This one is a little more complicated because we need to be safe and check for the existence of the record we are trying to update or put. So we deserialize then search for a matching record based on id. If we can't find one we return a bad request. If we do find a record we move on and call the Update function. Don't forget to change the DatabaseName, there are 3 places in this method.

  7. Lets input our Delete Method now

    [FunctionName("DeleteTodo")]
         public static async Task<IActionResult> RunDelete(
             [HttpTrigger(AuthorizationLevel.Anonymous, "put", Route = null)]
             HttpRequest req,
             [CosmosDB(
                 databaseName: "BlogPostProjectDB",
                 collectionName: "Todos",
                 ConnectionStringSetting = "DBConnectionString"
             )]
             DocumentClient client,
             ILogger log)
         {
             log.LogInformation("Editing Todo");
             var content = await new StreamReader(req.Body).ReadToEndAsync();
             var documentUpdate = JsonConvert.DeserializeObject<Todo>(content);
    
             Uri collectionUri = UriFactory.CreateDocumentCollectionUri("BlogPostProjectDB", "Todos");
    
             var feedOptions = new FeedOptions { EnableCrossPartitionQuery = true };
             var existingDocument = client.CreateDocumentQuery<Todo>(collectionUri, feedOptions)
                 .Where(d => d.id == documentUpdate.id)
                 .AsEnumerable().FirstOrDefault();
    
             if (existingDocument == null)
             {
                 log.LogWarning($"Todos: {documentUpdate.id} not found.");
                 return new BadRequestObjectResult($"Todo not found.");
             }
    
             var documentUri = UriFactory.CreateDocumentUri("BlogPostProjectDB", "Todos", documentUpdate.id);
             await client.DeleteDocumentAsync(documentUri, new RequestOptions
             {
                 PartitionKey = new PartitionKey(documentUpdate.id)
             });
    
             return new OkObjectResult(true);
         }
    

    This method is very similar to Update, we are checking for the record before deleting. Make sure to update the Database Name in the 3 places.

  8. One last step we need to do to make sure we can access our Function app from our UI. Open up local.settings.json in your Functions project. Then we need to add the following after Values.
    "Host": {
     "LocalHttpPort": 7071,
     "CORS": "*"
    }
    
    Now we have a working Function CRUD class. Next we will look at calling this API.

Accessing the Functions API from our Blazor App

  1. Lets get into our Blazor app and make use of the API. Add a New Razor Component in the Pages Directory called Todos.razor. image.png
  2. Let's add a link to our new page. Open NavMenu.razor in Shared and change our Fetch Data nav link to TodoList like this.
    <li class="nav-item px-3">
             <NavLink class="nav-link" href="todo">
                 <span class="oi oi-list-rich" aria-hidden="true"></span> Todo List
             </NavLink>
         </li>
    
  3. Now let's open Todos.razor and get our code added to add and get todos.

    @page "/todo"
    @using BlogPostProject.Common.Models
    @inject HttpClient Http
    

    Add our page URL, our reference to our Model and inject our HTTPClient at the top of this page.

    @if (_todos != null)
    {
    
         <div class="card">
             <h3 class="card-header">Current Tasks</h3>
             <div class="card-body">
                 @foreach (var todo in _todos)
                 {
                     if (!todo.Complete)
                     {
                         <div>
                             <input type="checkbox" checked="@todo.Complete" @onchange="@(async () => await CheckChanged(todo))" /> @todo.Name
                         </div>
                     }
                 }
             </div>
         </div>
     }
     <div class="card">
         <h3 class="card-header">Functions</h3>
         <div class="card-body">
             <input type="text" @bind="_new.Name" />
             <button class="btn btn-success" type="submit" disabled="@_isTaskRunning" @onclick="@(async () => await CreateTodo())">Add Task</button>
         </div>
     </div>
    

    Here we are making sure we have records and showing any existing todos as well as creating our add new todo form.

    @code {
     IEnumerable<Todo> _todos;
     bool _isTaskRunning = false;
     readonly Todo _new = new Todo();
    
     protected override async Task OnInitializedAsync()
     {
         await LoadTodos();
     }
    
     private async Task LoadTodos()
     {
         _todos = await Http.GetFromJsonAsync<IEnumerable<Todo>>("http://localhost:7071/api/GetTodos");
     }
    
     private async Task CheckChanged(Todo todo)
     {
         todo.Complete = !todo.Complete;
         await Http.PutAsJsonAsync("http://localhost:7071/api/PutTodo", todo);
     }
    
     public async Task CreateTodo()
     {
         _isTaskRunning = true;
         _new.Complete = false;
         _new.DateAdded = DateTime.UtcNow;
         await Http.PostAsJsonAsync("http://localhost:7071/api/PostTodo", _new);
         _new.Name = "";
         await LoadTodos();
         _isTaskRunning = false;
     }
    }
    

    As you can see we are using our GetTodos, PostTodo and PutTodo here. The only thing I am not going to cover at the moment is the delete method. It's the exact same thing as the Put, just pass the record you want to delete.

  4. When you run the application now you should see a console window pop up with our Functions project loaded in. image.png On your blazor app click Todo List. image.png You should now see there are no Tasks yet but you can add a task. image.png Once you have added a task it should show up. image.png Now you can click the check box to mark it as complete and it will disappear from the list.

We now have a working Azure CosmosDB, Function, Blazor WebAssembly application. All we have to do now is publish the Function and Blazor app to Web Apps and it will be fully serverless and in the cloud. I will now walk through these 2 steps if you would like to take the next step. There may be some small costs associated with the Function web app as a heads up. There is a free tier for the Blazor app that you will be able to take advantage of but Function does not have that, this is why I typically use local testing until I am ready to go live, then I fire up the functions instance as described below.

Publish our Azure Functions application to the cloud

  1. Open your Azure Dashboard. Click the Hamburger Menu and select Function App then click New
  2. Select your Subscription, then the Resource Group you created when you setup your Cosmos DB. Now you need to select an app name. I typically name it after my project and add functions at the end. BlogPostFunctions. Select Code, the .NET for the Runtime stack, Version 3.1 and your region. Then click Review + create and Create. image.png
  3. Once the Function is created we need to open it up to our localhost. You can do this a couple ways. But the first thing you need to do is go to the Function Web App and click on the left menu item called CORS image.png We can either remove what is in there and add a wildcard and allow any traffic from anywhere to access the function. We can also just add a new record with your Blazor localhost and port as the value. You may need to look at your Properties folder for your launchSettings.json file to get the SSL Port out of there. *Don't Forget To Save! image.png
  4. Now we just need to replace our API calls with our new azure function we setup in Todos.razor. Here is our new code section.

    @code {
    IEnumerable<Todo> _todos;
     bool _loaded;
     bool _isTaskRunning = false;
     readonly Todo _new = new Todo();
    
     protected override async Task OnInitializedAsync()
     {
         await LoadTodos();
         _loaded = true;
     }
    
     private async Task LoadTodos()
     {
         _todos = await Http.GetFromJsonAsync<IEnumerable<Todo>>("https://blogpostfunctions.azurewebsites.net/api/GetTodos");
     }
    
     private async Task CheckChanged(Todo todo)
     {
         todo.Complete = !todo.Complete;
         await Http.PutAsJsonAsync("https://blogpostfunctions.azurewebsites.net/api/PutTodo", todo);
     }
    
     public async Task CreateTodo()
     {
         _isTaskRunning = true;
         _new.Complete = false;
         _new.DateAdded = DateTime.UtcNow;
         await Http.PostAsJsonAsync("https://blogpostfunctions.azurewebsites.net/api/PostTodo", _new);
         _new.Name = "";
         await LoadTodos();
         _isTaskRunning = false;
     }
    }
    

    Make sure to replace my URL with your own.

  5. Now before we can run and test we need to publish our Azure Function project. Right click the project and select Publish then select Azure. image.png Select Azure Function App (Windows) image.png You should be able to see your Function in the list now. image.png Click Finish
  6. Now we need to click Manage Azure App Service settings image.png Click Insert value from Local under DBConnectionString this will ensure our CosmosDB connection string is pushed. image.png Now click OK and then Publish
  7. Once the publish is finished we can test our work and run the application. You should now be able to use the application in the same way as we did earlier but we are running our API from the cloud! Now on to publishing the web app.

Publish our Blazor WebAssembly app to the cloud

  1. Go to your Azure Dashboard click the Hamburger menu then click App Services. From here we can click New
  2. Select our same Resource Group, Name should be what you expect the url to be. Under Publish select Code. Under Runtime Stack choose the version of .NET you used to create your Blazor project. Choose Windows. For SKU and Size you want to click Change size. Click Dev/Test and choose the 1GB Free Tier plan. image.png Click Review + create then Create
  3. Once that is complete and pinned to your Dashboard you can now deploy to it. Right click on your Blazor UI project and click Publish. Then choose Azure. image.png Choose Azure App Service (Windows). image.png You should see your new app service you just created. image.png Click Finish then click Publish
  4. We need to add our new site to the CORS for the Function now as a final step. Go to Azure Dashboard and select Azure Function then click your Function. Select CORS from the left side menu and add your new site http and https address, then click Save*. image.png
  5. Once the publish is complete your browser should automatically open and take you to your new site in the cloud. Navigate to Todo List and make sure all is working.

Final Thoughts

That is the basics of how to get up and running, I tried to keep it as simple as possible. Once you get the workflow down you can fire up services and get a serverless application running in record time. Please feel free to contact me if you have any issues, suggestions or comments.

The GitHub for this tutorial can be found Here.

No Comments Yet