So far in our series, we’ve walked through the intro, wrote our first component, dynamically updated the HTML head from a component, and isolated our service dependencies.
It’s time to address the elephant in the room—why is the image loading so slow?
There’s a few reasons for that. First, we have to wait for the app to load when we refresh the page–and with Blazor WebAssembly, we’re waiting for the .NET runtime to load. On top of that, we’re calling off to a REST API, getting the image source, and sending that to our view. That’s not incredibly efficient.
In this post, we’re going to correct both issues. We’ll first move the Image
component to its own page, then we’re going to use a persistence layer to store and work with our images. This includes hosting our images on Azure Storage and accessing its details using the Azure Cosmos DB serverless offering. This will only help us as we’ll create components to search on and filter our data.
This post contains the following content.
- Move our Image component to its own page
- Integrate Cosmos DB with our application
- Update our tests
- Wrap up
Move our Image component to its own page
To move our Iamge
component away from the default Index
view, rename your Index.razor
and Index.razor.cs
files to Image.razor
and Image.razor.cs
.
In the Image.razor
file, change the route from @page "/"
to @page "/image"
. That keeps it as a routable component, meaning it’ll render whenever we browse to /image
.
Then, in Image.razor.cs
, make sure to rename the partial class to Image
.
Here’s how Image.razor.cs
looks now:
using Client.Services;
using Microsoft.AspNetCore.Components;
using System;
using System.Threading.Tasks;
namespace Client.Pages
{
partial class Image : ComponentBase
{
Data.Image _image;
[Inject]
public IApiClientService ApiClientService { get; set; }
private static string FormatDate(DateTime date) => date.ToLongDateString();
protected override async Task OnInitializedAsync()
{
_image = await ApiClientService.GetImageOfDay();
}
}
}
Create a new Home
component
With that in place, let’s create a new Home
component. Right now, it’ll welcome users to the site and point them to our images component. (If you need a refresher on how the NavigationManager
works, check out my previous post on the topic.)
@page "/"
@inject NavigationManager Navigator
<div class="flex justify-center">
<div class="max-w-md rounded overflow-hidden shadow-lg m-12">
<h1 class="text-4xl m-6">Welcome to Blast Off with Blazor</h1>
<img class="w-full" src="images/armstrong.jpg" />
<p class="m-4">
This is a project to sample various Blazor features and functionality.
We'll have more soon, but right now we are fetching random images.
</p>
<button class="text-center m-4 bg-red-500 hover:bg-red-700 text-white font-bold py-2 px-4 rounded"
@onclick="ToImagePage">
🚀 Image of the Day
</button>
</div>
</div>
@code {
void ToImagePage() => Navigator.NavigateTo("/image");
}
Here’s how the Home
component looks now.
Integrate Cosmos DB with our application
With our first fix out of the way, it’s now time to speed up our image loading time. I’m going to do this in two ways:
- Store the images statically in Azure Storage
- Store image metadata, including the Azure Storage URLs in Cosmos DB
In the past, Cosmos has been incredibly expensive and wouldn’t have been worth the cost for this project. With a new serverless offering (now in preview), it’s a lot more manageable and can easily be run under my monthly Azure credits. While Cosmos excels with intensive, globally-distributed workloads, I’m after a fully-managed NoSQL offering that’ll allow me flexibility if my schema needs change.
In this post, I won’t show you how to create a Cosmos instance, upload our images to Azure Storage, then create a link between the two. This is all documented in a recent post. After I have the data set up, I need to understand how to access it from my application. That’s what we’ll cover.
Now, I could use the Azure Cosmos DB C# client to work with Cosmos. There’s a lot of complexities here, and I don’t need any of that business. I need it for basic CRUD operations. I’m a fan of David Pine’s Azure Cosmos DB Repository .NET SDK, and will be using it here. This allows me to maintain the abstraction layer between the API and the client application, and is super easy to work with.
Update the API
After adding the NuGet package to my Api
and Data
projects, I can start to configure it. There’s a few different ways to wire up your Cosmos details—check out the readme for details—I’ll use the Startup
.
Here’s the Configure
method for my Azure Function in the Api
project:
public override void Configure(IFunctionsHostBuilder builder)
{
builder.Services.AddCosmosRepository(
options =>
{
options.CosmosConnectionString = "my-connection-string";
options.ContainerId = "image";
options.DatabaseId = "APODImages";
});
};
Next, I’ll need to make some changes to the model. Take a look and I’ll describe after:
using Microsoft.Azure.CosmosRepository;
using Newtonsoft.Json;
using System;
namespace Data
{
public class Image : Item
{
[JsonProperty("title")]
public string Title { get; set; }
[JsonProperty("copyright")]
public string Copyright { get; set; }
[JsonProperty("date")]
public DateTime Date { get; set; }
[JsonProperty("explanation")]
public string Explanation { get; set; }
[JsonProperty("url")]
public string Url { get; set; }
}
}
You’ll see that the model now inherits from Item
, which is required by the project. It contains an Id
, a Type
(for filtering implicitly on your behalf), and a partition key property. I’m also using JsonProperty
attributes to match up with the Cosmos fields. Down the line, I might split models between my app and my API but this should work for now.
Now, in ImageGet.cs
, I can call off to Cosmos quite easily. Here I’m calling off to my Cosmos instance. I can query by a random date.
using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.Http;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using Microsoft.Azure.CosmosRepository;
using Data;
namespace Api
{
public class ImageGet
{
readonly IRepository<Image> _imageRepository;
public ImageGet(IRepository<Image> imageRepository) => _imageRepository = imageRepository;
[FunctionName("ImageGet")]
public async Task<IActionResult> Run(
[HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "image")] HttpRequest req,
ILogger log)
{
var imageResponse = _imageRepository.GetAsync
(img => img.Date == GetRandomDate());
return new OkObjectResult(imageResponse.Result);
}
private static DateTime GetRandomDate()
{
var random = new Random();
var startDate = new DateTime(1995, 06, 16);
var range = (DateTime.Today - startDate).Days;
return startDate.AddDays(random.Next(range));
}
}
}
Update the client app
In our ApiClientService
, I’ll need to slightly modify my GetImageOfDay
method. The call returns an IEnumerable<Image>
, so I’ll just grab the result.
public async Task<Image> GetImageOfDay()
{
try
{
var client = _clientFactory.CreateClient("imageofday");
var image = await client.GetFromJsonAsync<IEnumerable<Image>>("api/image");
return image.First();
}
catch (Exception ex)
{
_logger.LogError(ex.Message, ex);
}
return null;
}
The images are now loading much faster!
Update our tests
Thanks to successfully isolating our service last time, the tests for the Image
component don’t need much fixing. Simply changing the component to Image
instead of Index
does the trick:
[Fact]
public void ImageOfDayComponentRendersCorrectly()
{
var mockClient = new Mock<IApiClientService>();
mockClient.Setup(i => i.GetImageOfDay()).ReturnsAsync(GetImage());
using var ctx = new TestContext();
ctx.Services.AddSingleton(mockClient.Object);
IRenderedComponent<Client.Pages.Image> cut = ctx.RenderComponent<Client.Pages.Image>();
var h1Element = cut.Find("h1").TextContent;
var imgElement = cut.Find("img");
var pElement = cut.Find("p");
h1Element.MarkupMatches("My Sample Image");
imgElement.MarkupMatches(@"<img src=""https://nasa.gov""
class=""rounded-lg h-500 w-500 flex items-center justify-center"">");
pElement.MarkupMatches(@"<p class=""text-2xl"">Wednesday, January 1, 2020</p>");
}
We can also add a quick test to our Home
component in a new HomeTest
file, which is similar to how we did our NotFound
component:
[Fact]
public void IndexComponentRendersCorrectly()
{
using var ctx = new TestContext();
var cut = ctx.RenderComponent<Home>();
var h1Element = cut.Find("h1").TextContent;
var buttonElement = cut.Find("button").TextContent;
h1Element.MarkupMatches("Welcome to Blast Off with Blazor");
buttonElement.MarkupMatches("🚀 Image of the Day");
}
Wrap up
In this post, we worked on speeding up the loading of our images. We first moved our Image
component off the home page, then integrated Cosmos DB into our application. Finally, we cleaned up our tests.
Top comments (0)