Search
  • Gábor Csepregi

Chapter 2 - Adding backend

Preface


Last time we implemented a frontend for a basic task management system. Now let's take a look at how we would create a backend for this project. I have shared the code for this on bitbucket as well: https://bitbucket.org/4shards-blog/blog-task-backend.


Step 1 - Prepare the project


As a first step let's create the infrastructure we will need. As we discussed earlier we will use MariaDB as data storage, and we will use docker locally to have one:

docker run --rm --name mariadb -e MYSQL_ROOT_PASSWORD=s0m3s3cr3tp455w0rd -p3306:3306 -d mariadb

Of course, it's up to you to change the password in your case, but don't forget to change it everywhere later. Next step is to create the table for our tasks:

CREATE TABLE `tasks` (
  `id` varchar(36) NOT NULL DEFAULT '',
  `summary` text DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

As you can see, we have added an 'id' field as well, though we didn't have it on the UI side. Also worth pointing out that we will use GUIDs as identifiers and not an auto-incremented value. Though it takes more space to store it, it will save us some headache as our URIs won't be enumerable.


For this purpose I just used the command line to execute it, I haven't prepared any database versioning framework, yet. Then we'll create the Azure Function project using C# as the language:

mkdir BlogTaskBackend
cd BlogTaskBackend/
func init --source-control=true --worker-runtime=dotnet

Func is the Azure Functions CLI tool, available for most operating systems. When it completes, add the following NuGet dependencies:

dotnet add package MySqlConnector
dotnet add package Microsoft.Azure.Functions.Extensions
dotnet add package Microsoft.Extensions.Http

Next, we create a Startup class, that will help us using dependency injection in our project:

//Startup.cs
using BlogTaskBackend;
using BlogTaskBackend.Configuration;
using Microsoft.Azure.Functions.Extensions.DependencyInjection;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;

[assembly: FunctionsStartup(typeof(Startup))]
namespace BlogTaskBackend
{
    
    public class Startup: FunctionsStartup
    {
        public override void Configure(IFunctionsHostBuilder builder)
        {
            builder.Services.AddOptions<DatabaseOptions>()
                .Configure<IConfiguration>((settings, configuration) =>
                {
                    configuration.GetSection("Database").Bind(settings);
                });
        }
    }
}

The only thing we will inject this time is the database settings object, so let's create the class that will hold the properties:

//DatabaseOptions.cs
namespace BlogTaskBackend.Configuration
{
    public class DatabaseOptions
    {
        public string Server { get; set; }
        public string Database { get; set; }
        public string UserId { get; set; }
        public string Password { get; set; }
        public string SslMode { get; set; }
    }
}

Add the actual settings to our local setup:

//local.settings.json
{
    "IsEncrypted": false,
    "Values": {
        "AzureWebJobsStorage": "UseDevelopmentStorage=true",
        "FUNCTIONS_WORKER_RUNTIME": "dotnet",
        "Database:Server": "localhost",
        "Database:Database": "blog_task_db",
        "Database:UserId": "root",
        "Database:Password": "s0m3s3cr3tp455w0rd",
        "Database:SslMode": "None"
    },
    "Host": {
        "LocalHttpPort": 7071,
        "CORS": "*"
    }
}

And last but not least, create a class that represents the Task on the server-side:

//Model/BlogTask.cs
using System;

namespace BlogTaskBackend.Model
{
    public class BlogTask
    {
        public string Id { get; set; } = Guid.NewGuid().ToString().ToUpper();
        public string Summary { get; set; }
    }
}


Step 2 - Endpoint for creating a task


As you can see, we have added an 'id' field as well, though we didn't have it on the UI side. Also worth pointing out that we will use GUIDs as identifiers and not an auto-incremented value. Though it takes more space to store it, it will save us some headache as our URIs won't be enumerable.


In case you want to start the project locally just run:

func start

It will fire up the functions container on the 7071 port by default.


The next step is to create our first actual function. You can do this by entering the following command on the command line:

func new --language=C# --template=http --name=BlogTaskCreate

This will create the BlogTaskCreate.cs file, with a class filled from a template. We will replace it with the following code:

//BlogTaskCreate.cs
using System;
using System.Net.Http;
using System.Threading.Tasks;
using BlogTaskBackend.Configuration;
using BlogTaskBackend.Model;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.Http;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using MySql.Data.MySqlClient;
using Newtonsoft.Json;

namespace BlogTaskBackend
{
    public class BlogTaskCreate
    {
        private readonly DatabaseOptions _databaseOptions;

        public BlogTaskCreate(IOptions<DatabaseOptions> databaseOptions)
        {
            _databaseOptions = databaseOptions.Value;
        }
        
        [FunctionName("BlogTaskCreate")]
        public async Task<IActionResult> Create(
            [HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "tasks")] HttpRequestMessage req,
            ILogger log)
        {
            var task = JsonConvert.DeserializeObject<BlogTask>(await req.Content.ReadAsStringAsync());
            MySqlSslMode.TryParse(_databaseOptions.SslMode, out MySqlSslMode sslMode);
            var builder = new MySqlConnectionStringBuilder
                        {
                            Server = _databaseOptions.Server,
                            Database = _databaseOptions.Database,
                            UserID = _databaseOptions.UserId,
                            Password = _databaseOptions.Password,
                            SslMode = sslMode,
                        };

            await using (var conn = new MySqlConnection(builder.ConnectionString))
            {
                await conn.OpenAsync();

                await using (var command = conn.CreateCommand())
                {
                    command.CommandText = 
      @"INSERT INTO tasks (id, summary) VALUES (@id, @summary);";
                    command.Parameters
                        .AddWithValue("@id", task.Id);
                    command.Parameters
                        .AddWithValue("@summary", task.Summary);
                    var _ = await command.ExecuteNonQueryAsync();
                }
            }

            return new CreatedResult($"/api/tasks/{task.Id}", task);
        }
    }
}

What we have changed other than implementing the actual save method and returning the REST conform 201 - Created response are:

  1. We have removed the static modifier from both the class and the Run method.

  2. This enables us to use a constructor with injected dependencies. Currently, we only need the database settings.

  3. We have changed the Authorization level to Anonymous as we don't want to do Azure authentication. It has no difference when running it locally, but if you plan to deploy on Azure, you need to send a 'code' query parameter with a secret key generated by the Azure portal.

  4. We have changed the request method argument to 'post' only.

  5. Added a custom Route element instead of null, so the endpoint will be 'tasks' instead of something generated from the class name.

  6. Changed the request argument from being HttpRequest to HttpRequestMessage. It has better content reading methods.

The rest should be quite straightforward. We read the new task information from the request, then insert it into the table we just created above. Please note, that the id of the task is autogenerated when we instantiate a new BlogTask object if we don't specify it elsewhere. And we don't have it in the received request. The database handling code is simply a modified version of the Azure MySQL tutorial, I haven't checked for optimization issues yet, as this is not part of this blog right now.


I have used Postman to test the endpoint after running it locally. As we can see, in the result the object is sent back with the id field filled:

Step 3 - Endpoint to get the list of tasks


Great, we can store the tasks into the database. Now it's time to get back a list of all the tasks we have stored. There are lots of ways to expand on this, we could have a separate method in the same function class, but I have chosen to create a separate class for each endpoint. This is mainly a design decision and I don't see a preferable pattern here. Anyways we have already named our class BlogTaskCreate, and putting a getter method there would be confusing, or we should rename it.


So just like above, we create our new function with the same command but different name this time:

func new --language=C# --template=http --name=BlogTaskList

And we will use the following code here:

//BlogTaskList.cs
using System.Collections.Generic;
using System.Net.Http;
using System.Threading.Tasks;
using BlogTaskBackend.Configuration;
using BlogTaskBackend.Model;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.Http;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using MySql.Data.MySqlClient;

namespace BlogTaskBackend
{
    public class BlogTaskList
    {
        private readonly DatabaseOptions _databaseOptions;

        public BlogTaskList(IOptions<DatabaseOptions> databaseOptions)
        {
            _databaseOptions = databaseOptions.Value;
        }

        [FunctionName("BlogTaskList")]
        public async Task<IActionResult> Run(
            [HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "tasks")] HttpRequestMessage req,
            ILogger log)
        {
            var result = new List<BlogTask>();
            MySqlSslMode.TryParse(_databaseOptions.SslMode, out MySqlSslMode sslMode);
            var builder = new MySqlConnectionStringBuilder
            {
                Server = _databaseOptions.Server,
                Database = _databaseOptions.Database,
                UserID = _databaseOptions.UserId,
                Password = _databaseOptions.Password,
                SslMode = sslMode,
            };

            await using var conn = new MySqlConnection(builder.ConnectionString);
            await conn.OpenAsync();
            await using var command = conn.CreateCommand();
            
            command.CommandText = @"SELECT id, summary FROM tasks";
            var reader = await command.ExecuteReaderAsync();
            while (await reader.ReadAsync())
            {
                result.Add(new BlogTask
                {
                    Id = reader.GetString(0),
                    Summary = reader.GetString(1)
                });
            }
            return new OkObjectResult(result);
        }
    }
}

The basic idea is the same as above. We have removed the static modifier and changed the same settings except for the request method which is now 'get'. This time we read the data from the database, fill a list of BlogTasks with it and return a 200 - OK response. If everything is fine then after a restart of the local container you should see something similar to the following:

Yes, I have ended up with two tasks because I have accidentally clicked the send button twice. But anyways, the function returns the expected JSON array of tasks.


Step 4 - Endpoint to update the status of a task


The status update was an exercise as the basic example just used a simple checkbox. If the checkbox was ticked, it meant the task was completed. Now let's expand on this and use the four statuses: new, open, in progress and done.


For this we need to modify the database first:

ALTER TABLE tasks ADD COLUMN status enum('new','open','in progress','done') default 'new';

We have created a new column in the tasks table as an enum to accept only the required values and defaulted to 'new', so the already existing records and records saved without specifying the status field will all be saved as 'new'. This provides backward compatibility, so anyone using tasks without the status can do so in the future as well.


Now modify our server-side class that holds the task properties:

//Model/BlogTask.cs
public string Status { get; set; }

Create a new function to handle status updates:

func new --language=C# --template=http --name=BlogTaskUpdateStatus

And modify the generated template to do the actual update:

//BlogTaskUpdateStatus
using System.Net.Http;
using System.Threading.Tasks;
using BlogTaskBackend.Configuration;
using BlogTaskBackend.Model;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.Http;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using MySql.Data.MySqlClient;
using Newtonsoft.Json;

namespace BlogTaskBackend
{
    public class BlogTaskUpdateStatus
    {
        private readonly DatabaseOptions _databaseOptions;

        public BlogTaskUpdateStatus(IOptions<DatabaseOptions> databaseOptions)
        {
            _databaseOptions = databaseOptions.Value;
        }
        
        [FunctionName("BlogTaskUpdateStatus")]
        public async Task<IActionResult> Run(
            [HttpTrigger(AuthorizationLevel.Anonymous, "patch", Route = "tasks/{id}")] HttpRequestMessage req,
            string id, ILogger log)
        {
            var task = JsonConvert.DeserializeObject<BlogTask>(await req.Content.ReadAsStringAsync());
            MySqlSslMode.TryParse(_databaseOptions.SslMode, out MySqlSslMode sslMode);
            var builder = new MySqlConnectionStringBuilder
            {
                Server = _databaseOptions.Server,
                Database = _databaseOptions.Database,
                UserID = _databaseOptions.UserId,
                Password = _databaseOptions.Password,
                SslMode = sslMode,
            };
            
            await using var conn = new MySqlConnection(builder.ConnectionString);
            await conn.OpenAsync();
            await using var command = conn.CreateCommand();

            command.CommandText = @"UPDATE tasks SET status = @status WHERE id = @id";
            command.Parameters.AddWithValue("@id", id);
            command.Parameters.AddWithValue("@status", task.Status);
            var _ = await command.ExecuteNonQueryAsync();
            
            return new NoContentResult();
        }
    }
}

As you can see we will use the 'patch' method for this purpose. With a patch, we can send any subset of the properties of the task we want to update, and only those properties should be modified. If we used 'put' instead, then we must send back the whole task object, regardless of what changed, and we are required to update every field.


If we test the above, then we should find that it can update the status field returning 204 - No content:

But we cannot use this new status until we update our list as well:

//BlogTaskList.cs
...
command.CommandText = @"SELECT id, summary, status FROM tasks";
var reader = await command.ExecuteReaderAsync();
while (await reader.ReadAsync())
{
    result.Add(new BlogTask
    {
        Id = reader.GetString(0),
        Summary = reader.GetString(1),
        Status = reader.GetString(2)
    });
} 
...

This will return the following result:

As we can see, the status of one task has changed to 'done'. And the id of this task is exactly what we have used in the URL above for the patch.


Wrap it up


We have created a set of functions that can be deployed to Azure and can handle the task of storing, reading and updating BlogTask objects in a database. Now we have a frontend a backend both running fine on their own. Next time we will connect them and start analyzing what we have done and see if we can identify any design issues.


Until then think of the question: don't we need to replicate the enumeration in the C# code as well?

42 views

©2020 by Gabor's blog. Proudly created with Wix.com