Last time I laid out the justification and overall architecture for a REST based nanoservice, for converting between file extensions and mime types. This time I’ll go into more detail about the actual code for the project. I won’t, however, cover every class, or page, line by line. Even though this is a tiny web service project, there’s still enough code here that we would all get bored if I tried to describe each line. No, instead, I’ll cover things in broader swaths, focusing more on what the code does, and how it fits with the rest of the project, at a high level.
If you want to follow along, my code is available, for free, HERE.
In this article I’ll cover the front end, or Blazor UI part of the nanoservice. In future articles I’ll delve into the business logic, then the repository / database. Finally, I’ll walk through the client NUGET package and provide a quick sample of how to use that client to call this service.
The UI for the service looks like this when it starts:
The login link is directed to an external authentication microservice. In this case, I have it configured to hit my CG.Coral authentication microservice. You could always reconfigure that to point to your own service, if you like.
The API help page is generated by Swagger, and it looks like this:
As we can see, the REST API only has a single endpoint: /api/MimeTypes. That endpoint only accepts a POST verb, containing the extension to be converted to a mime type. This REST method is how clients will interact with our service.
The health checks page is standard ASP.NET, and it looks like this:
For now, the health checks are recorded using the in-memory data store. Later, we might choose to use something else, for storing the health check data. That might make an interesting blog article …
Using the login link, and providing the appropriate credentials, makes the Mime Types link visible, That page looks like this:
As shown, the page allows callers to add, edit, or remove individual mime types. The grid is paged, to keep the list to a manageable size. There are icons on each row of the grid, which allow a caller to edit a mime type. That link shows this popup:
That popup screen has fields for the mime type properties. It also contains another grid for the associated file extensions. There are icons to edit each file extension. That link pops up this screen:
Taken together, these screens are the entire UI for the nanoservice. Of course, most callers won’t ever need to access the UI at all. The UI is really only there for manipulating the mime types and file extensions in the database.
All of the UI screens are built using the MudBlazor NUGET package. That library allows us to use a clean, standards driven, open source library for building out the UI. Pulling MudBlazor into a standard Blazor application is detailed on the MudBlazor website HERE. I won’t cover that part in this article.
Most of the pages in this nanoservice are trivial, so I won’t cover them. I will, however, cover the markup for the MimeTypes page. Here is what that looks like:
@page "/mimetypes" @attribute [Authorize] @using MimeType = Models.MimeType <MudBreadcrumbs Items="_crumbs"></MudBreadcrumbs> <MudText Typo="Typo.h4"> MimeTypes </MudText> <MudText Class="pb-5" Typo="Typo.body1"> Use this page to manage mime types </MudText> <HelpBlock Visible="@(!MimeTypeManager.MimeTypes.AsQueryable().Any())" Content="No mime types created yet!"> <ChildContent> <MudText Color="@Color.Info" Typo="@Typo.body1" Class="pl-3"> Press the button below to create your mime type! </MudText> <MudButton Variant="@Variant.Outlined" StartIcon="@Icons.Outlined.Add" Size="Size.Small" OnClick="OnAddMimeTypeAsync"> Create Mime Type </MudButton> </ChildContent> </HelpBlock> <ErrorBlock Dismissable Content="@_error" /> <InfoBlock Dismissable Content="@_info" /> @if (MimeTypeManager.MimeTypes.AsQueryable().Any()) { <MudCard Elevation="0"> <MudCardContent> <MudGrid Justify="Justify.Center"> <MudItem xs="12" md="10" lg="8"> <MudTable ServerData="@(new Func<TableState, Task<TableData<MimeType>>>(ServerReload))" Elevation="0" Striped Dense @ref="_ref"> <ToolBarContent> <MudButton Variant="Variant.Outlined" StartIcon="@Icons.Outlined.Add" Size="Size.Small" OnClick="OnAddMimeTypeAsync"> Add </MudButton> <MudToolBarSpacer /> <MudTextField ValueChanged="@((string x) => OnSearch(x))" Placeholder="Search" Adornment="Adornment.Start" AdornmentIcon="@Icons.Material.Outlined.Search" IconSize="Size.Small" Class="mt-0"> </MudTextField> </ToolBarContent> <ColGroup> <col /> <col /> <col /> <col style="width: 125px;" /> </ColGroup> <HeaderContent> <MudTh> <MudTableSortLabel SortLabel="Type" T="MimeType"> Type </MudTableSortLabel> </MudTh> <MudTh> <MudTableSortLabel SortLabel="SubType" T="MimeType"> Sub Type </MudTableSortLabel> </MudTh> <MudTh> <MudTableSortLabel SortLabel="Description" T="MimeType"> Description </MudTableSortLabel> </MudTh> <MudTh> </MudTh> </HeaderContent> <RowTemplate> <MudTd DataLabel="Type"> @context.Type </MudTd> <MudTd DataLabel="SubType"> @context.SubType </MudTd> <MudTd DataLabel="Description"> @context.Description </MudTd> <MudTd> <MudTooltip Text="Edit"> <MudIconButton Icon="@Icons.Outlined.Edit" Size="Size.Small" OnClick="@(() => OnEditMimeTypeAsync(context))" /> </MudTooltip> <MudTooltip Text="Delete"> <MudIconButton Icon="@Icons.Outlined.Delete" Size="Size.Small" OnClick="@(() => OnDeleteMimeTypeAsync(context))" Disabled="@(context.Extensions.Count() > 0)" /> </MudTooltip> <MudTooltip Text="Properties"> <MudIconButton Icon="@Icons.Outlined.Settings" Size="Size.Small" OnClick=@(() => OnPropertiesAsync(context)) /> </MudTooltip> </MudTd> </RowTemplate> <PagerContent> <MudTablePager PageSizeOptions="new int[]{10, 20, 50}" /> </PagerContent> </MudTable> </MudItem> </MudGrid> </MudCardContent> </MudCard> }
From top to bottom, the markup starts by identifying the route for the page, as “/mimetypes”. The next line marks the page as visible only to authorized callers. Next, we add the bread crumbs to the page. Then we add the page header. Next, we add a help block to prompt the user, in case we’ve not seeded the database and we would, therefore, display an empty page. As a general rule, I try to avoid ever displaying empty pages because I find them confusing when I encounter them in other people’s websites.
Below that there is markup to display information, or an error. The HelpBlock
, ErrorBlock
, and InfoBlock
components are all part of the CG.Blazor.Components library. The source for that NUGET package can be found HERE.
Next, we make a call to a component named MimeTypeManager
. I’ll cover this component in a future article. For now, understand that the MimeTypesManager.MimeTypes
property contains an object with a method called AsQueryable
. That method returns a queryable sequence of MimeType
models, which are the primary mime type abstraction for the project. So, we quickly use the LINQ Any
method to determine if there are any mime type rows in the database.
If there are mime type rows to display, we add a MudCard
to the page. The card contains a MudGrid
that centers our list of mime types on the page. We display the list of mime types using the MubTable
component. Looking at the markup for the grid, we see that we assign a delegate to the ServerData
property. That delegate contains all the code we’ll need to populate the grid at runtime. I’ll cover that shortly.
The grid contains columns for the mime type properties, and a RowTemplate
block that contains the markup to bind the grid to our MimeType
properties. Finally, the markup contains a MudTablePager
block, which ensures the page displays a pager section, for the grid.
The code behind for the MimeTypes page looks like this:
public partial class Index { private MudTable<MimeType> _ref; private string _error; private string _info; private string _searchString = ""; private AuthenticationState _authState; private readonly List<BreadcrumbItem> _crumbs = new() { new BreadcrumbItem("Home", href: "/"), new BreadcrumbItem("MimeTypes", href: "/mimetypes") }; [Inject] private IMimeTypeManager MimeTypeManager { get; set; } [Inject] private IDialogService DialogService { get; set; } [Inject] private AuthenticationStateProvider AuthenticationStateProvider { get; set; } protected override async Task OnInitializedAsync() { _authState = await AuthenticationStateProvider.GetAuthenticationStateAsync() .ConfigureAwait(false); await base.OnInitializedAsync(); } private async Task OnAddMimeTypeAsync() { try { _error = ""; _info = ""; var model = new MimeType() { CreatedBy = _authState.User.GetEmail(), CreatedDate = DateTime.Now }; var parameters = new DialogParameters { ["Model"] = model, ["Caption"] = "Add Mime Type" }; var dialog = DialogService.Show<MimeTypeEditDialog>( "", parameters ); var result = await dialog.Result.ConfigureAwait(false); if (!result.Cancelled) { _ = await MimeTypeManager.MimeTypes.AddAsync( model ).ConfigureAwait(false); _info = $"Mime Type: '{model.Description}' was created"; } } catch (Exception ex) { _error = ex.Message; } await InvokeAsync(() => StateHasChanged()).ConfigureAwait(false); await InvokeAsync(() => _ref.ReloadServerData()); } private async Task OnPropertiesAsync( MimeType model ) { var parameters = new DialogParameters { ["Model"] = model }; var dialog = DialogService.Show<AuditDialog<MimeType>>( "", parameters ); _ = await dialog.Result.ConfigureAwait(false); } private async Task OnDeleteMimeTypeAsync( MimeType model ) { try { _error = ""; _info = ""; if (model.Extensions.Count() > 0) { await DialogService.ShowMessageBox( "Problem", $"The mime type '{model.Description}' has one or more file extensions associated with it " + $"that MUST be manually removed before the mime type can be deleted.", yesText: "OK!" ); return; } bool? result = await DialogService.ShowMessageBox( "Warning", $"This will delete the mime type: '{model.Description}'.", yesText: "OK!", cancelText: "Cancel" ); if (result != null && result.Value) { await MimeTypeManager.MimeTypes.DeleteAsync( model.Id ).ConfigureAwait(false); _info = $"Mime type: '{model.Description}' was deleted"; } } catch (Exception ex) { _error = ex.Message; } await InvokeAsync(() => StateHasChanged()).ConfigureAwait(false); await InvokeAsync(() => _ref.ReloadServerData()); } private async Task OnEditMimeTypeAsync( MimeType model ) { try { _error = ""; _info = ""; var temp = model.QuickClone(); temp.UpdatedBy = _authState.User.GetEmail(); temp.UpdatedDate = DateTime.Now; var parameters = new DialogParameters { ["Model"] = temp, ["Caption"] = $"Edit '{temp.Description}'" }; var dialog = DialogService.Show<MimeTypeEditDialog>( "", parameters ); var result = await dialog.Result.ConfigureAwait(false); if (!result.Cancelled) { model.Description = temp.Description; model.Type = temp.Type; model.SubType = temp.SubType; model.UpdatedBy = temp.UpdatedBy; model.UpdatedDate = temp.UpdatedDate; model.Extensions = temp.Extensions; _ = await MimeTypeManager.MimeTypes.UpdateAsync( model ).ConfigureAwait(false); _info = $"Mime Type: '{model.Description}' was updated"; } } catch (Exception ex) { _error = ex.Message; } await InvokeAsync(() => StateHasChanged()).ConfigureAwait(false); await InvokeAsync(() => _ref.ReloadServerData()); } private void OnSearch(string text) { _searchString = text; _ref.ReloadServerData().Wait(); } private Task<TableData<MimeType>> ServerReload( TableState state ) { try { var data = MimeTypeManager.MimeTypes.AsQueryable(); if (!string.IsNullOrEmpty(_searchString)) { data = data.Where(x => x.Type.Contains(_searchString) || x.SubType.Contains(_searchString) || x.Description.Contains(_searchString) ); } var totalItems = data.Count(); switch (state.SortLabel) { case "Type": data = data.OrderByDirection( state.SortDirection, o => o.Type ).AsQueryable(); break; case "SubType": data = data.OrderByDirection( state.SortDirection, o => o.SubType ).AsQueryable(); break; case "Description": data = data.OrderByDirection( state.SortDirection, o => o.Description ).AsQueryable(); break; } data = data.Skip(state.Page * state.PageSize) .Take(state.PageSize); var retData = new TableData<MimeType>() { TotalItems = totalItems, Items = data.ToList() }; return Task.FromResult(retData); } catch (Exception ex) { _error = ex.Message; return Task.FromResult(new TableData<MimeType>()); } } }
Again from top to bottom, we start with some fields that are used, internally, by the code-behind: A reference for the table, an error string, an informational string, a search string, the authentication state, and the bread crumbs.
Next, we inject three services we’ll need for the page: The MimeTypeManager
, the DialogService
, and the AuthenticationStateProvider
. The MimeTypeManager
is something I’ll cover in a fiture article. The DialogService
is part of the MudBlazor library; we’ll use that to display our popup dialogs. The AuthenticationStateProvider
is pure Blazor; we’ll use that to get information about the current user.
Next we override the OnInitializeAsync
method, to fetch the authentication state. We’ll need that in order to pass the current user’s email address into our MimeTypeManager
calls, later on.
Next we have the AddMimeTypeAsync
method. This is where we respond whenever the user clicks the “Add” button, on the table. We start this method be clearing out any previous error or informational messages. After that, we create a new MimeType
object. MimeType
is the type we use to model the concept of a mime type. We then create a DialogParameters
object in order to pass our new model instance into the popup dialog we’re about to create. Next we call the Show
method on the DialogService
, which gives us back a Task
for actually displaying the window. We display the window then we check to see if the user cancelled, or not. Assuming they didn’t, we add the new model using the Add method of the MimeTypeManager.MimeTypes
object. Finally, assuming nothing has thrown an error up to this point, we generate an information hint to tell the caller that we added the model. We finish up by forcing Blazor to re-render the page, so any changes show up quickly on the UI.
The next method is called OnPropertiesAsync
. This is how we respond when the caller asks for audit properties on any mime type object, from the UI (the little gear icon). We pass the model into the MudBlazor dialog using the same DialogParameters
object we used in the previous method. We show the dialog and return. It’s not possible for the data to change in this method, so, we don’t have to call anything on the MimeTypeManager
object.
The next method is called OnDeleteMimeTypeAsync
. This is how we respond whenever the user clicks one of the delete icons, for any of the mime type rows, in the table (the little trash can icon). We start this method, same as the last, by clearing and previous error or information messages. Then we check to see if the selected MimeType
has any file extension associated with it. If it does, then we prompt the user, to inform them that they must first manually remove all those file extensions before they can delete the mime type. I did that because my database has a cascade rule set up for that relation, and that rule would generate an error anyway. Also, I want to make it harder to accidentally delete a mime type with file extensions hung off it.
Assuming the mime type doesn’t have any related file extensions, we prompt to make sure the caller really wanted to do, what they’re about to do. After that we call the MimeTypeManager.MimeType.DeleteAsync
method, in order to remove the mime type from the underlying database. I’ll cover the operation of the DeleteAsync
method in a future article. for now, just know that it will delete the mime type for us. We finish the method by forcing Blazor to re-render the page, so any changes show up quickly on the UI.
The next method is called OnEditMimeTypeAsync
. This is how we respond whenever the user clicks one of the little pen icons, on the mime type table. Most of this method is like the other two methods, except that here I make a quick clone of the model before I pass it to the MudBlazor dialog. I do that so I won’t have made any changes to the original model until the caller actually presses the ‘Ok’ button, on the dialog. I use the QuickClone
method to make the cloned copy. That QuickClone
method is part of the CG.Core NUGET package, which is available HERE.
After we clone the model, pass it into the MudBlazor dialog and show it to the caller, we check to make sure the caller didn’t cancel the dialog. Assuming they didn’t, we then call the MimeTypeManager.MimeTypes.UpdateAsync
method, to save the changes. I’ll cover the UpdateAsync
method in a future article, for now, just know it will save any changes that were made to the model, by the caller. We finish the method by forcing Blazor to re-render the page, so any changes show up quickly on the UI.
The next method is named OnSearch
, and this is how we respond whenever the caller attempts to search for a mime type in the table. This method saves the search term and then calls the ReloadServerData
method, on the MudBlazor table reference. That call to ReloadServerData
forces the table to reload it’s data, from the server – applying the search filter in the process.
ServerReload
is probably the most interesting method in this page’s code-behind. This is a callback that’s used by the MudBlazor table, to build up the result set for the table to display. Every time we force Blazor to update the data for the page this method is indirectly called, by the MudBlazor table.
We start by calling the AsQueryable
method on the MimeTypeManager.MimeTypes
object. We’ll cover that method in a future article. For now, just realize that it returns a queryable sequence of MimeType
objects, from the database.
After that we check the search string, to see if we need to apply a filter to the query. If so, we do that with a where clause that checks the value of the various properties in the MimeType
object. This way, if the caller searches for, say, ‘jpg’, then the search will be applied to the mime types’ Type
, SubType
and Description
fields, thereby returning any mime type with anything containing the term ‘jpg’.
After we filter the data, we then grab a quick count of the results. The table will need that, internally, in order to produce the paging section, at the bottom of the control. After we’ve filtered and counted the data, we then use the current sort state of the table to determine whether we need to apply a sort to the data. If so, we do that quickly. Finally, we apply any paging for the data, using the current state of the table’s page control.
Once we’ve built up the query, we then wrap the results (query and count), in a TableData
object, and pass the whole thing back to MudBlazor. The results is that the MudBlazor table takes this queried data and displays it on the UI.
The first popup dialog I’ll cover is a pretty typical MudBlazor dialog. Here is the markup for the FileExtension edit popup dialog:
@using FileExtension = Models.FileExtension <EditForm Model="@Model" OnValidSubmit="OnValidSubmit"> <MudDialog> <TitleContent> <div class="d-flex"> <MudIcon Size="Size.Small" Icon="@Icons.Material.Filled.Edit" class="mr-3"></MudIcon> <MudText>@Caption</MudText> </div> </TitleContent> <DialogContent> <DataAnnotationsValidator /> <MudTextField @bind-Value="@Model.Extension" For="(() => Model.Extension)" Label="Extension" /> </DialogContent> <DialogActions> <MudButton OnClick="Cancel"> Cancel </MudButton> <MudButton Color="Color.Primary" ButtonType="ButtonType.Submit"> Save Changes </MudButton> </DialogActions> </MudDialog> </EditForm>
The dialog itself is wrapped in an EditForm
tag. That tag is actually from ASP.NET, but it can be used in Blazor projects, like this one. The EditForm
tag enable us to force data validation before the form is closed. By wrapping the MudBlazor MudDialog
object in an EditForm
tag, we can force the dialog to validate itself before the user closes the dialog. I follow this approach in every editable dialog in this service. Notice the DataAnnotationsValidator
tag, that enables the dialog to display any validation errors that might happen, as the user interacts with the form. Otherwise, the dialog itself doesn’t really have to do much, for validations to work correctly.
This dialog has a custom title area consisting of an icon and a text string. That’s what’s going on in the TitleContent
tag. The controls for the dialog are within the DialogContent
tag. The buttons that operate the dialog are within the DialogActions
tag. I try to use MudBlazor controls exclusively (where possible) within the dialog, in order to keeps things consistent.
The code-behind for the dialog is shown here:
public partial class FileExtensionEditDialog { [CascadingParameter] MudDialogInstance MudDialog { get; set; } [Parameter] public FileExtension Model { get; set; } [Parameter] public string Caption { get; set; } private void Cancel() { MudDialog.Cancel(); } private void OnValidSubmit(EditContext context) { if (context.Validate()) { MudDialog.Close(DialogResult.Ok(Model.Id)); } } }
From top to bottom, the code-behind starts with parameters for the MudDialog
instance, the Model
instance, and the dialog’s caption. Then we have the Cancel
method which is tied to the cancel button. The OnValidSumit
method is called by the EditForm
, when the model for the form undergoes data validation. In that method, we validate the EditContext
. Assuming that passes, we close the dialog. If the validation fails, we do nothing since the form will display any error messages, for us.
The markup for the next popup dialog is shown here:
@using FileExtension = Models.FileExtension <EditForm Model="@Model" OnValidSubmit="OnValidSubmit"> <MudDialog> <TitleContent> <div class="d-flex"> <MudIcon Size="Size.Small" Icon="@Icons.Material.Filled.Edit" class="mr-3"></MudIcon> <MudText>@Caption</MudText> </div> </TitleContent> <DialogContent> <DataAnnotationsValidator /> <MudTextField @bind-Value="@Model.Type" For="(() => Model.Type)" Label="Type" /> <MudTextField @bind-Value="@Model.SubType" For="(() => Model.SubType)" Label="SubType" /> <MudTextField @bind-Value="@Model.Description" Lines="2" For="(() => Model.Description)" Label="Description" /> @if (Model.Extensions.Count > 0) { <MudTable Items="@(Model.Extensions)" Dense Striped> <ToolBarContent> <MudButton Variant="Variant.Outlined" StartIcon="@Icons.Outlined.Add" Size="Size.Small" OnClick="() => OnAddFileExtensionAsync(Model)"> Add File Extension </MudButton> </ToolBarContent> <ColGroup> <col /> <col style="width: 120px;" /> </ColGroup> <HeaderContent> <MudTh> <MudTableSortLabel SortBy="new Func<FileExtension, object>(x => x.Extension)"> Extension </MudTableSortLabel> </MudTh> <MudTh> </MudTh> </HeaderContent> <RowTemplate Context="context2"> <MudTd DataLabel="Extension"> @context2.Extension </MudTd> <MudTd> <MudTooltip Text="Edit"> <MudIconButton Icon="@Icons.Outlined.Edit" Size="Size.Small" OnClick="@(() => OnEditFileExtensionAsync(context2))" /> </MudTooltip> <MudTooltip Text="Delete"> <MudIconButton Icon="@Icons.Outlined.Delete" Size="Size.Small" OnClick="@(() => OnDeleteFileExtensionAsync(context2))" /> </MudTooltip> <MudTooltip Text="Properties"> <MudIconButton Icon="@Icons.Outlined.Settings" Size="Size.Small" OnClick=@(() => OnFileExtensionsPropertiesAsync(context2)) /> </MudTooltip> </MudTd> </RowTemplate> <PagerContent> <MudTablePager PageSizeOptions="new int[] { 5, 10 }" /> </PagerContent> </MudTable> } else { <br /> <MudButton Variant="Variant.Outlined" StartIcon="@Icons.Outlined.Add" Size="Size.Small" OnClick="() => OnAddFileExtensionAsync(Model)"> Add File Extension </MudButton> } </DialogContent> <DialogActions> <MudButton OnClick="Cancel"> Cancel </MudButton> <MudButton Color="Color.Primary" ButtonType="ButtonType.Submit"> Save Changes </MudButton> </DialogActions> </MudDialog> </EditForm>
This dialog is called when the user adds or edits a mime type. The dialog is much like the one I just covered, in that it is a MudBlazor MudDialog
object, wrapped in an ASP.NET EditForm
. This dialog has a few more controls, including a MudTable
for any FileExtension
objects that are associated with the MimeType
.
The code-behind for the form is shown here:
public partial class MimeTypeEditDialog { private AuthenticationState _authState; [Inject] private IDialogService DialogService { get; set; } [Inject] private AuthenticationStateProvider AuthenticationStateProvider { get; set; } [CascadingParameter] MudDialogInstance MudDialog { get; set; } [Parameter] public MimeType Model { get; set; } [Parameter] public string Caption { get; set; } protected override async Task OnInitializedAsync() { _authState = await AuthenticationStateProvider.GetAuthenticationStateAsync() .ConfigureAwait(false); await base.OnInitializedAsync(); } private void Cancel() { MudDialog.Cancel(); } private void OnValidSubmit(EditContext context) { if (context.Validate()) { MudDialog.Close(DialogResult.Ok(Model.Id)); } } private async Task OnEditFileExtensionAsync( FileExtension model ) { var temp = model.QuickClone(); temp.UpdatedBy = _authState.User.GetEmail(); temp.UpdatedDate = DateTime.Now; var parameters = new DialogParameters { ["Model"] = temp, ["Caption"] = $"Edit '{temp.Extension}'" }; var dialog = DialogService.Show<FileExtensionEditDialog>( "", parameters ); var result = await dialog.Result.ConfigureAwait(false); if (!result.Cancelled) { // Set the changes to the model. model.Extension = temp.Extension; model.UpdatedBy = temp.UpdatedBy; model.UpdatedDate = temp.UpdatedDate; } } private async Task OnDeleteFileExtensionAsync( FileExtension model ) { bool? result = await DialogService.ShowMessageBox( "Warning", $"This will delete the file extension: '{model.Extension}'.", yesText: "OK!", cancelText: "Cancel" ); if (result != null && result.Value) { Model.Extensions.Remove(model); } } private async Task OnAddFileExtensionAsync( MimeType mimeType ) { var model = new FileExtension() { CreatedBy = _authState.User.GetEmail(), CreatedDate = DateTime.Now, MimeTypeId = Model.Id }; var parameters = new DialogParameters { ["Model"] = model, ["Caption"] = $"Add Extension" }; var dialog = DialogService.Show<FileExtensionEditDialog>( "", parameters ); var result = await dialog.Result.ConfigureAwait(false); if (!result.Cancelled) { Model.Extensions.Add(model); } } private async Task OnFileExtensionsPropertiesAsync( FileExtension model ) { var parameters = new DialogParameters { ["Model"] = model }; var dialog = DialogService.Show<AuditDialog<FileExtension>>( "", parameters ); _ = await dialog.Result.ConfigureAwait(false); } }
Most of this code-behind is like what I’ve already covered, so I’ll be brief here. This dialog is used to add or edit a mime type, so one might think we would have a MimeTypeManager object injected into the code-behind. We don’t. That’s because we don’t actually save any changes to the models here, in this dialog. We save all changes in the calling page, and then, only after the user has pressed the ‘Ok’ button, on the dialog. By deferring the saves until then, we sidestep the thorny issue of when we should save changes, and what, if any changes, should be saved on behalf of dialogs that are displayed by this dialog. Yeah, it gets ugly, which is why I just sidestepped the whole mess.
So, looking at this code-behind, we notice that most of it looks familiar, from having looked at the MimeTypes page, and the FileExtensions popup. There are methods here that are tied to elements on the markup, and perform actions when the user interacts with them. These methods popup the FileExtensions dialog (presented earlier), to add, edit, or remove file extensions from a mime type. As mentioned before, the actual saving of changes, to MimeType
objects, from this dialog, are deferred back to the MimeType page, in the code-behind for that page.
The only other thing I’ll cover, from the Blazor potion of this project, is the controller for the REST API. Let’s look at that code now:
[Route("api/[controller]")] [ApiController] public class MimeTypesController : ControllerBase { private readonly IMimeTypeManager _mimeTypeManager; private readonly ILogger<MimeTypesController> _logger; public MimeTypesController( IMimeTypeManager mimeTypeManager, ILogger<MimeTypesController> logger ) { Guard.Instance().ThrowIfNull(mimeTypeManager, nameof(mimeTypeManager)) .ThrowIfNull(logger, nameof(logger)); _mimeTypeManager = mimeTypeManager; _logger = logger; } [AllowAnonymous] [HttpPost] [Produces(MediaTypeNames.Application.Json)] [Consumes(MediaTypeNames.Application.Json)] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status500InternalServerError)] [ProducesResponseType(StatusCodes.Status400BadRequest)] public virtual async Task<IActionResult> FindByExtensionAsync( [FromBody] string extension ) { try { if (false == ModelState.IsValid) { return BadRequest(); } var mimeTypes = await _mimeTypeManager.FindByExtensionAsync( extension ).ConfigureAwait(false); var results = mimeTypes.Select( x => $"{x.Type}/{x.SubType}" ).ToList(); return Ok(results); } catch (Exception ex) { _logger.LogError( ex, "Failed to query for mime type(s) by file extension!", extension ); return Problem( title: $"{nameof(MimeTypesController)} error!", detail: ex.Message, statusCode: StatusCodes.Status500InternalServerError ); } } }
From top to bottom, we see that the controller contains a reference to an instance of IMimeTypeManager
in the _mimeTypeManager
field. I’ll cover the IMimeTypeManager
type in the next article. For now, realize that this object contains the business logic for managing mime types and file extensions. The controller also contains an ILogger
instance, for logging activity and errors. The constructor injects the values for the manager and the logger fields.
The FindByExtensionAsync
method is mapped to the HTTP POST verb, on the api/MimeTypes route. This method begins by validating the model. Provided the model is valid, the method then calls the FindByExtensionAsync
method on the IMimeTypeStore
object to find any mime type associated with the file extension passed in by the caller. Assuming there are mime types returned from that call, the next step strips out everything except the type and subtype, which is formatted into a standard mime type string. Those strings are returned as a JSON array of strings.
Notice that the controller method is not decorated with an Authorized
attribute. That is a deliberate choice, on my part. For now, I can’t think of any reason why I would want to restrict access to mime conversions. If that changes, in the future, I can always lock the controller down.
That about it for the Blazor portion of the nanoservice. To be sure, there are still things I could show. After all, it isn’t completely trivial to stand up a server-side Blazor application, then wire it up to an authentication service, a REST API documentation generator, and a health check UI page. Still, those parts of the application are well known to most Blazor developers, so, I thought I would leave them out of my description. If you’re interested, the code is always available, HERE.
Next time I’ll dive into the CG.Obsidian.Abstractions and CG.Obsidian libraries. Those two pieces contain all the abstractions, models, stores, managers, etc., that together, comprise the business logic for the nanoservice.
Photo by Geran de Klerk on Unsplash