X

Hosting Azure Functions backed Blazor application on Azure Storage static website

My previous blog post was an introduction to static website hosting on Azure Storage. This post focuses on how to deploy Blazor application as a Azure static website on Azure Storage and how to use Azure Functions as a server back-end for Blazor application hosted as a static site.

Source code available! Source code for this blog post is available in my GitHub repository gpeipman/BlazorDemo. Feel free to browse the code or run it in your own machine.

Getting started

Blazor applications are WebAssembly applications that run in all major browsers. These applications doesn’t need any back-end services to work but there is support for making AJAX requests to server. It can be service hosted on our own server but it can be also some external service running elsewhere.

I updated my Blazor demo application by adding support for Azure Functions and therefore also for static website on Azure Storage.

NB! This blog post is baed on Blazor demo application. I expect you to go through the code (not much code) to understand it better. Also I expect you to have at least some knowledge about Azure Functions. Good point to start is my blog post Getting started with Azure Functions

Database is needed! Blazor demo application needs also SQL Server database. By default it uses SQL Server LocalDB. It works when using Azure Functions with emulator on dev box. If you plan to put up things on Azure then you need also Azure SQL database. Demo application solution has Books.bacpac file you can use to restore database with sample data.

Service client for Azure Functions

In my demo application I have books client class that communicates with local back-end API implemented as MVC controller. For Azure Functions we need a little bit different client. Blazor client for Azure Functions uses different URL structure. All URL-s have host specified and additionally security code as query parameter (BooksAzureFunctionsClient.cs in BlazorDemo.Client project).

public class BooksAzureFunctionsClient : IBooksClient
{
    private readonly HttpClient _httpClient;

    private const string FunctionsHost = "https://****.azurewebsites.net/api";
    private const string FunctionsKey = "<Your functions key>";

    public BooksAzureFunctionsClient(HttpClient httpClient)
    {
        _httpClient = httpClient;
    }

    public async Task DeleteBook(Book book)
    {
        await DeleteBook(book.Id);
    }

    public async Task DeleteBook(int id)
    {
        var url = FunctionsHost + "/Books/Delete/" + id + "?code=" + FunctionsKey;

        await _httpClient.PostAsync(url, null);
    }

    public async Task<PagedResult<Book>> ListBooks(int page)
    {
        var url = FunctionsHost + "/Books/Index/page/" + page + "?code=" + FunctionsKey;

        return await _httpClient.GetJsonAsync<PagedResult<Book>>(url);
    }

    public async Task<Book> GetBook(int id)
    {
        var url = FunctionsHost + "/Books/Get/" + id + "?code=" + FunctionsKey;

        return await _httpClient.GetJsonAsync<Book>(url);
    }

    public async Task SaveBook(Book book)
    {
        var url = FunctionsHost + "/Books/Save" + "?code=" + FunctionsKey;

        await _httpClient.PostJsonAsync<Book>(url, book);
    }
}

This client is actually more generic as it allows to use also other back-ends where same URL structure works.Supported are even those end-points where security code parameter is not mandatory and we can leave it empty.

Azure functions for data manipulation

When moving to Azure Functions we have to migrate all data manipuation code we have in web application controller. For every action in controller we define a function that Blazor application can call. As we are working on minimalistic demo application the move is simple. Here are functions for data manipulation (Functions.cs file in BlazorDemo.AzureFunctionsBackend project).

public static class Functions
{
    [FunctionName("Index")]
    public static IActionResult Index([HttpTrigger(AuthorizationLevel.Function, "get", "post", Route = "Books/Index/page/{page}")]HttpRequest req,
TraceWriter log, [FromRoute] int page)
    {
        if (page <= 0) page = 1;

        using (var context = (new BooksDbContextFactory()).CreateDbContext())
        {
            var books = context.Books
                                .OrderBy(b => b.Title)
                                .GetPaged(page, 10);

            return new JsonResult(books);
        }
    }

    [FunctionName("Get")]
    public static IActionResult Get([HttpTrigger(AuthorizationLevel.Function, "get", "post", Route = "Books/Get/{id}")]HttpRequest req,
TraceWriter log, [FromRoute]int id)
    {
        using (var context = (new BooksDbContextFactory()).CreateDbContext())
        {
            var book = context.Books.FirstOrDefault(b => b.Id == id);

            return new JsonResult(book);
        }
    }

    [FunctionName("Save")]
    public static IActionResult Save([HttpTrigger(AuthorizationLevel.Function, "get", "post", Route = "Books/Save")]HttpRequest req, TraceWriter log)
    {
        using (var reader = new StreamReader(req.Body, Encoding.UTF8))
        using (var context = (new BooksDbContextFactory()).CreateDbContext())
        {
            var body = reader.ReadToEnd();
            var book = JsonConvert.DeserializeObject<Book>(body);

            context.Update(book);
            context.SaveChanges();
        }

        return null;
    }

    [FunctionName("Delete")]
    public static async Task Delete([HttpTrigger(AuthorizationLevel.Function, "get", "post", Route = "Books/Delete/{id}")]HttpRequest req,
TraceWriter log, [FromRoute]int id)
    {
        using (var context = (new BooksDbContextFactory()).CreateDbContext())
        {
            var book = context.Books.FirstOrDefault(b => b.Id == id);
            if (book == null)
            {
                return;
            }

            context.Books.Remove(book);
            await context.SaveChangesAsync();
        }
    }
}

Before configuring functions on Azure Portal we need to implement DbContext factory used in functions.

DbContext factory for Azure Functions

To communicate with database we need to get connection string to database. When running Azure Functions emulator on local machine we can use local.settings.json file. At server we don’t have this file and application settings are coming in as environment variables. To support this I wrote DbContext factory that works for both cases (BooksDbContextFactory.cs file in AzureFunctionsBackend project).

public class BooksDbContextFactory : IDesignTimeDbContextFactory<BooksDbContext>
{
    private static string _connectionString;

    public BooksDbContext CreateDbContext()
    {
        return CreateDbContext(null);
    }

    public BooksDbContext CreateDbContext(string[] args)
    {
        if (string.IsNullOrEmpty(_connectionString))
        {
            LoadConnectionString();
        }

        var builder = new DbContextOptionsBuilder<BooksDbContext>();
        builder.UseSqlServer(_connectionString);

        return new BooksDbContext(builder.Options);
    }

    private static string ConnectionString
    {
        get
        {
            if(string.IsNullOrEmpty(_connectionString))
            {
                LoadConnectionString();
            }

            return _connectionString;
        }
    }

    private static void LoadConnectionString()
    {
        var builder = new ConfigurationBuilder();
        var settingsPath = Path.Combine(Environment.CurrentDirectory, "local.settings.json");

        builder.AddJsonFile(settingsPath, optional: true);
        builder.AddEnvironmentVariables();

        var configuration = builder.Build();

        _connectionString = configuration.GetConnectionString("DefaultConnection");
    }
}

Now let’s deploy functions to Azure. To make Blazor application work we have to copy Azure functions host and security key to Azure Functions client in Blazor.Client project.

  • Azure Functions host. Open Azure Portal and search for functions account you created for Blazor demo application. On Overview tab there is URL parameter. This is the functions host.
  • Security key. To get security click some function open and select Manage under function name. There is host keys section where keys for all functions are listed.

Configuring Azure Functions in Azure Portal

There are few things left to configure on Azure portal:

  • Cross-Origin Resourse Sharing (CORS) – add all supported host headers (the hosts where Blazor application may onnect to functions) to CORS host list. For testing we can remove all headers and add just one with asterisk (*) as host to allow all clients to connect. (To find CORS settings move to Platform Features => API => CORS).
  • Ćonnection string – add connection string called DefaultConnection to functions account in Azure portal. (To find function app settings move to Platform Features => General Settings => Application Settings => Connection strings).

Our functions are now configured and it’s time to get Blazor application running on Azure Store.

Configuring Azure Functions for emulator in dev box

If you don’t want to use Azure Functions live version you can also go with Azure Functions emulator that is part of Visual Studio tooling. To make things work locally we have to edit local.settings.json file so it looks like shown here. For some reason this file doesn’t make it to source code repository. Just add it to root folder of AzureFunctionsBackend project.

{
  "IsEncrypted": false,
  "Values": {
    "AzureWebJobsStorage": "",
    "AzureWebJobsDashboard": ""
  },
  "Host": {
    "LocalHttpPort": 7071,
    "CORS": "*"
  },
  "ConnectionStrings": {
    "DefaultConnection": "Server=(localdb)\\MSSQLLocalDB;Database=Books;Trusted_Connection=True;MultipleActiveResultSets=true"
  }
}

Publishing Blazor application to Azure static website

As we have now everything ready on Azure we can publish our Blazor application there. If you need better overview of steps below please go through my blog post Introducing static website hosting for Azure Storage. The blog post covers basics of publishing process in detail.

  • Open Visual Studio Code and install Azure Storage extension
  • Set up GPv2 storage account using Azure Portal
  • Publish Blazor application to some folder in your dev box
  • Open BlazorDemo.Client\dist subfolder of this folder in Visual Studio Code
  • Move to Azure Storage workspace, and open newly created storage account and open blobs
  • Right-click on $web container and select Deploy to static website…
  • Pick publish folder and start deployment

When Azure Storage extension is done with publishing it’s time to try out if Blazor application works with Azure Functions back-end. For this we need Azure static website URL and we can take it from Azure portal.

As I did everything right – otherwise there would have been nothing to blog about – I see the following view in my browser.

Wrapping up

Blazor applications are pure browser applications by nature and often they don’t need any background code to work. This blog post demonstrated how to host Blazor application as Azure static website and implement server back-end on Azure Functions that are consumed directly from browser. It is not perhaps so easy to do as building Web API but Azure Functions are the bes fit for small functionalities consumed often.

Liked this post? Empower your friends by sharing it!

View Comments (2)

Related Post