Nanoservices – Part 2

Nanoservices – Part 2

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>&nbsp;</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"
                                                   [email protected](() => 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>&nbsp;</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"
                                               [email protected](() => 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