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.
Great we should now have our development environment setup. It should look something like this right now.
{"IsEncrypted": false,"Values": {"AzureWebJobsStorage": "UseDevelopmentStorage=true","FUNCTIONS_WORKER_RUNTIME": "dotnet","DBConnectionString": "AccountEndpoint=https://blogpostproject.documents.azure.com:443/;AccountKey=;"}}
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.
public class Todo{public string id { get; set; }public string Name { get; set; }public bool Complete { get; set; }public DateTime DateAdded { get; set; }}
[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.
<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>
@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. On your blazor app click Todo List. You should now see there are no Tasks yet but you can add a task. Once you have added a task it should show up. 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.
@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. 6. 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. Select Azure Function App (Windows) You should be able to see your Function in the list now. Click Finish 7. Now we need to click Manage Azure App Service settings Click Insert value from Local under DBConnectionString this will ensure our CosmosDB connection string is pushed. Now click OK and then Publish 8. 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.
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.
Quick Links
Legal Stuff