Now that we have an API to provide catalog items, we can start the process of building a frontend web shop that customers can use to browse the catalog. We'll use Blazor to create this web app.
The provided starting point for this lab is based on the work you completed in the previous lab, however the Catalog API has been extended with more endpoints and the code refactored for better organization.
- Open the
eShop.lab2.slnin Visual Studio or VS Code. - Note there are some changes to the
Catalog.APIproject:- The
Apisdirectory contains theCatalogApi.csfile now, rather than it being in the project root - There is an
Extensiondirectory containing aHostingExtensions.csfile, in which an extension method is defined calledAddApplicationServices. This method is called from theProgram.csfile to setup the application's services. If any other extension methods are required, they can be placed in classes defined in this directory. - There is a
Picsdirectory containing product photos for the items in the catalog. - There is a
CatalogOptionsclass defined inCatalogOptions.csfile in the project root. This class is used to support configurable values used in the project. - The
Apis/CatalogApi.csfile now defines many other endpoints for the Catalog API which will be used to support the frontend experience. Take a moment to read the additional endpoint definitions and implementations.
- The
- The
eShop.ServiceDefaultsproject has some changes too, including more extension methods to help with reading required configuration values, and configuring OpenAPI documents from the configuration. Take a moment to read the contents of the new files. - Launch the AppHost project and navigate to the Swagger UI for the Catalog API to see the additional endpoints that are now available.
The provided starting point for this lab includes a Blazor web project for the frontend of our eShop that we're going to make updates to, but it hasn't yet been composed into the AppHost project.
-
Add a project reference from the
eShop.AppHostproject to theWebAppproject. -
Open the
Program.csfile in theeShop.AppHostproject and add a line to add theWebAppproject to the app model, with a reference to theCatalog.APIproject:builder.AddProject<WebApp>("webapp") .WithReference(catalogApi);
-
Run the AppHost project and verify that the
WebAppproject is now included in the list of running resources on the dashboard. -
Click the first hyperlink for the
WebAppproject in the Endpoints column and verify you see the home page of the eShop website.
To communicate with the Catalog API from the web app, we'll create a service class that uses an HttpClient instance managed by the IHttpClientFactory features.
-
In the
WebAppproject, open theServicesdirectory and inside it create a new fileCatalogService.cs -
Declare a class in this file called
CatalogServicewith a primary constructor that accepts a singleHttpClient httpClientparameter, and a field to store the common base path of the Catalog API endpoints"api/v1/catalog":namespace eShop.WebApp.Services; public class CatalogService(HttpClient httpClient) { private readonly string remoteServiceBaseUrl = "api/v1/catalog/"; }
-
We need a method that will be called to retrieve a page of catalog items from the API. Add an async method called
GetCatalogItemsthat accepts appropriate parameters for the page size, page index, and optional item brand and item type filters, and returnsCatalogResult(this type is already defined in the project), e.g.:public async Task<CatalogResult> GetCatalogItems(int pageIndex, int pageSize, int? brand, int? type) { }
-
The Catalog API represents the catalog items in the URL space categorized optionally by brand and type, e.g.
/items,/items/type/all,/items/type/123/brand/456, etc. Additionally the paging parameters are passed via the query string. This means the URL called by our service needs to be dynamically constructed based on the values of the parameters passed to this method. Create a new method to handle this job calledGetCatalogItemsUri, e.g.:private static string GetAllCatalogItemsUri(string baseUri, int pageIndex, int pageSize, int? brand, int? type) { // Build URLs like: // [base]/items // [base]/items/type/all // [base]/items/type/123/brand/456 // [base]/items/type/123/brand/456?pageSize=9&pageIndex=2 string filterPath; if (type.HasValue) { var brandPath = brand.HasValue ? brand.Value.ToString() : string.Empty; filterPath = $"/type/{type.Value}/brand/{brandPath}"; } else if (brand.HasValue) { var brandPath = brand.HasValue ? brand.Value.ToString() : string.Empty; filterPath = $"/type/all/brand/{brandPath}"; } else { filterPath = string.Empty; } return $"{baseUri}items{filterPath}?pageIndex={pageIndex}&pageSize={pageSize}"; }
-
Using this method, update the body of the
GetCatalogItemsmethod to retrieve the items from the Catalog API using thehttpClientconstructor parameter:public async Task<CatalogResult> GetCatalogItems(int pageIndex, int pageSize, int? brand, int? type) { var uri = GetAllCatalogItemsUri(remoteServiceBaseUrl, pageIndex, pageSize, brand, type); var result = await httpClient.GetFromJsonAsync<CatalogResult>(uri); return result ?? new(0, 0, 0, []); }
-
Register the
CatalogServicewith the application's DI container and configure its base address to point at thecatalog-apiresource by adding a line to theExtensions/HostingExtensions.csfile. Be sure that the host name set in the URIBaseAddress:// HTTP and gRPC client registrations builder.Services.AddHttpClient<CatalogService>(o => o.BaseAddress = new("http://catalog-api"));
-
Ensure the
WebAppproject builds successfully before continuing.
Now that we have a service we can use to easily retrieve the catalog items from the Catalog API, let's update the site's home page to use the service and display some catalog items.
-
In the
WebAppproject, open theComponents/Pages/Catalog.razorfile. Note that this page is configured to be the home page (served from the/path) via the@page "/"directive at the top of the file. -
In order to get an instance of the
CatalogServiceclass in theCatalog.razorfile from the application's DI container, add a line using the@injectdirective near the top of the file (make sure it's after the@pagedirective). Note that the format of this is a bit like a property or field in C#, where the first part is the type name and the second part the member name (in this case the property name is the same as the type name but you could use any valid property name):@inject CatalogService CatalogService
-
Locate the
@codeblock at the bottom of theCatalog.razorfile. This is where the page's fields, parameters, and methods can be defined. Define a constant value for the page size of9(while the API supports different page sizes, the UI we're going to build assumes a fixed page size):const int PageSize = 9;
-
Razor components (including pages) execute according to a pre-defined lifecycle, represented by methods you can override to perform the desired logic needed to implement the component's behavior. Add code to use the injected
CatalogServiceinstance to load catalog items and store them in a field when the component is initializing. Just pass fixed values for the parameters other thanpageSizefor now:CatalogResult? catalogResult; protected override async Task OnInitializedAsync() { catalogResult = await CatalogService.GetCatalogItems(0, PageSize, null, null); }
-
Back in the markup section of the file, add some HTML and use Razor expressions to display the items in the
catalogResultfield. Note that you'll need to check the value is notnullfirst to avoid a compiler error. Use the existingCatalogListItemcomponent defined in the project to handle rendering each item inside the<div class="catalog>element:<div class="catalog"> @if (catalogResult is not null) { <div> <div class="catalog-items"> @foreach (var item in catalogResult.Data) { <CatalogListItem Item="@item" /> } </div> </div> } </div>
-
Run the AppHost project and navigate to the eShop front page. You should see catalog items now but their images aren't rendering correctly:
-
Open the browser developer tools (F12) and navigate to the Network pane then refresh the page. From the failed requests log we can see the images are trying to be loaded from paths like
/product-images/99, where99is the product ID but the site doesn't have these files or any endpoint configured to serve them. Like the product details, the product images are served by the Catalog API. -
YARP is a package for ASP.NET Core applications that provides highly customizable reverse proxying capabilities. We'll use YARP to proxy the product image requests to the frontend site on to the Catalog API. Add a reference to the
Microsoft.Extensions.ServiceDiscovery.Yarppackage, version8.0.1. You can use thedotnetCLI, or Visual Studio NuGet Package Manager, or edit theWebApp.csprojdirectly:<PackageReference Include="Microsoft.Extensions.ServiceDiscovery.Yarp" Version="9.0.0" />
-
To setup the proxying behavior, first add a line to the
AddApplicationServicesmethod in theHostingExtensions.csfile, to add the require services to the application's DI container:builder.Services.AddHttpForwarderWithServiceDiscovery();
-
To setup the proxying endpoint, add a line in
Program.cs, just before the call toapp.Run()at the end of the file, to map a forwarder endpoint from the incoming path to the Catalog API. The first argument is the pattern for the incoming request path, the second argument is the address of the server to forward the request to, and the last argument is the target path on the forwarded server (i.e. the Catalog API):app.MapForwarder("/product-images/{id}", "http://catalog-api", "/api/v1/catalog/items/{id}/pic");
-
Run the AppHost project again and navigate to the store's front page. This time the product images are loaded correctly:
-
Navigate to the Traces page of the dashboard and locate a trace row for one of the product image requests, then click on the View link in the Details column to open the trace details page. Notice the information that is displayed in the waterfall diagram of the various spans that make up the traced operation, including the request from the browser to the
webappresource, the forwarded request fromwebapptocatalog-api(both the outgoing and incoming spans), and finally the database call bycatalog-apito theCatalogDBPostgreSQL resource. Click on any individual span to see all the details included in the trace. See if you can find the actual SQL query that was executed against the database as part of serving the image:
You may have noticed that, when visiting the catalog page, there is a delay before the page is rendered in the browser. This delay is more noticeable when first loading the page after launching the AppHost project, as it takes a bit of time before the various resources involved are fully started. Let's improve this by utilizing the streaming rendering feature in Blazor to display a loading message while waiting for the catalog items to be returned by the backend service.
-
In the
Catalog.razorfile, update the markup to render a simple "Loading..." message if thecatalogResultfield isnull, instead of rendering nothing:<div class="catalog"> @if (catalogResult is null) { <p>Loading...</p> } else { <div> <div class="catalog-items"> @foreach (var item in catalogResult.Data) { <CatalogListItem Item="@item" /> } </div> </div> } </div>
If you try loading the page again at this point, you'll see no change. This is because by default, Blazor doesn't send any response to the browser until the component has completely finished executing, including asynchronous operations started in component lifecycle events, e.g. in the
OnInitializedAsyncmethod. -
Add a directive at the top of the page (after the
@pagedirective) to add theStreamRenderingattribute to the page. This will instruct Blazor to stream the response to the browser as asynchronous operations started by the component complete:@attribute [StreamRendering] -
Load the page again and notice that the loading message is displayed briefly before the catalog items are rendered.
-
Try forcing a longer delay to make the message easier to see when running locally, by adding a call to
await Task.Delay(1000)before the call toCatalogService.GetCatalogItemsin theOnInitializedAsyncmethod:protected override async Task OnInitializedAsync() { await Task.Delay(1000); catalogResult = await CatalogService.GetCatalogItems(0, PageSize, null, null); }
The Catalog API supports returning a subset of matching catalog items to allow for building a page-based UI. Let's update the Catalog.razor page to display navigating through the paged results.
-
Under the
PageSizefield, declare a new component parameter to store the current page number being viewed. The value for this will be bound from the querystring of the request URL, e.g./?page=2, by using theSupplyParameterFromQueryattribute:[SupplyParameterFromQuery] public int? Page { get; set; }
-
Update the call to
CatalogService.GetCatalogItemsto use the value of thePageparameter if it's set (useGetValueOrDefaultto help with this). Note that the API accepts the page as a zero-based index, but as this page is viewed by humans, we'll want to offset that by+1so that the first page is page 1, the second is page 2, and so on:protected override async Task OnInitializedAsync() { catalogResult = await CatalogService.GetCatalogItems(Page.GetValueOrDefault(1) - 1, PageSize, null, null); }
-
To help with rendering a set of hyperlinks for navigating to other pages, add a method called
GetVisiblePageIndexesthat generates a list of page numbers based on theCatalogResultreturned from the call to the Catalog API:static IEnumerable<int> GetVisiblePageIndexes(CatalogResult result) => Enumerable.Range(1, (int)Math.Ceiling(1.0 * result.Count / PageSize));
-
Add markup to render the page links by looping over the page indexes returned from the
GetVisiblePageIndexesmethod and render a hyperlink using the built-inNavLinkcomponent. The CSS defined inCatalog.razor.cssalready includes a styles targeting classespage-linksandactive-pageso use those to style the links. Add this markup just after the closing</div>tag of thecatalog-itemsdiv:<div class="catalog"> @if (catalogResult is null) { <p>Loading...</p> } else { <div> <div class="catalog-items"> @foreach (var item in catalogResult.Data) { <CatalogListItem Item="@item" /> } </div> <div class="page-links"> @foreach (var pageIndex in GetVisiblePageIndexes(catalogResult)) { <NavLink ActiveClass="active-page" Match="@NavLinkMatch.All" href="@Nav.GetUriWithQueryParameter("page", pageIndex == 1 ? null : pageIndex)">@pageIndex</NavLink> } </div> </div> } </div>
-
Reload the home page and navigate through the pages of the catalog using the links rendered under the items. Note that the application must be fully restarted for this to work as the
SupplyParameterFromQueryattribute is not recognized after a hot reload:
The final piece of catalog functionality to add is the ability to filter the items by type and/or brand. The Catalog API already has endpoints to facilitate this so we just need to update CatalogService to call them and consume that from Catalog.razor with some appropriate UI.
-
In the
CatalogService.csfile, add two new methods to allow retrieving catalog item types and brands from the Catalog API:public async Task<IEnumerable<CatalogBrand>> GetBrands() { var uri = $"{remoteServiceBaseUrl}catalogBrands"; var result = await httpClient.GetFromJsonAsync<CatalogBrand[]>(uri); return result ?? []; } public async Task<IEnumerable<CatalogItemType>> GetTypes() { var uri = $"{remoteServiceBaseUrl}catalogTypes"; var result = await httpClient.GetFromJsonAsync<CatalogItemType[]>(uri); return result ?? []; }
-
In
Catalog.razor, declare two new component parameters to capture the brand ID and item type ID from the querystring:[SupplyParameterFromQuery(Name = "brand")] public int? BrandId { get; set; } [SupplyParameterFromQuery(Name = "type")] public int? ItemTypeId { get; set; }
-
Update the
OnInitializedAsyncmethod to pass in the values from theBrandIdandItemTypeIdparameters, replacing thenullvalues being passed before:protected override async Task OnInitializedAsync() { catalogResult = await CatalogService.GetCatalogItems( Page.GetValueOrDefault(1) - 1, PageSize, BrandId, ItemTypeId); }
-
In the markup, add an instance of the
CatalogSearchcomponent (already defined in this project) immediately after the opening<div class="catalog">tag, passing through the values of theBrandIdandItemTypeIdparameters:<div class="catalog"> <CatalogSearch BrandId="@BrandId" ItemTypeId="@ItemTypeId" />
-
Locate and open the
/Components/Catalog/CatalogSearch.razorfile. Update this file so that it populates thecatalogBrandsandcatalogItemTypesfields by calling the methods you just added to theCatalogServiceclass. Reminder, you'll need to use the@injectdirective to get an instance of theCatalogServicebefore you can update theOnInitializedAsyncmethod.For an extra challenge, try calling the methods in parallel before awaiting their results (the
Task.WhenAllmethod might be helpful here). -
Reload the home page and see now that the filter UI is displayed on the left-hand side. Click on the various filter options to verify that the catalog UI functions correctly. Notice how the URL changes as you click through the various filter options and paging links.
If you have time and would like a challenge, try using what you've learned so far and update the Item.razor page to display an item's details. This page is linked to from each item on the catalog page.
Hints:
- The Catalog API provides an endpoint at
/items/{id}to retrieve an item's details via aGETrequest. - You can get image URLs for items from the
IProductImageUrlProviderservice which is already registered in the application's DI container (@inject...). - The
Item.razor.cssfile already defines some styles targeting class names likeitem-detailsanddescriptionthat should help with styling the content. - Think about what should happen if the item ID in the querystring doesn't have a matching catalog item. Displaying a "not found" message and changing the HTTP response status code to
404might be things to consider.






