Combining Blazor WebAssembly and .Net MVC application - part 2

Note. Before reading this article, please read the first part of this series Combining Blazor WebAssembly And .Net MVC Application here: Combining Blazor WebAssembly And .Net MVC Application - Part 1.

Introduction

In this article, we will continue our journey of combining Blazor WebAssembly and .NET MVC applications in a single project. This is part 2 of a series of articles that explains how to use Blazor components in MVC views and vice versa. If you haven’t read part 1 yet, I recommend you to do so before continuing.

In part 1, we have seen how to create a new Blazor WebAssembly project and add it as a reference to our existing MVC project. We have also seen how to configure our MVC project to serve the Blazor files.

In this part, we will use the Blazor WebAssembly component in the .NET MVC application to create a simple CRUD data application. Continue to use the project in part 1 and the existing FetchData component in the Blazor WebAssembly project.

Add the component to the MVC application to show the list

First, create the Models folder in the Blazor WebAssemply project, then move the WeatherForecast class in the FetchData component to the folder. Add the Id field in the WeatherForecast class.

namespace BlazorApp.Models
{
    public class WeatherForecast
    {
        public string? Id { get; set; }

        public DateOnly Date { get; set; }

        public int TemperatureC { get; set; }

        public string? Summary { get; set; }

        public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
    }
}

Then, in the HomeController.cs in the MVC project, add some code to the simulation data service.

public class HomeController : Controller
{
    // Simulation data service
    private static readonly List<WeatherForecast> weatherForecasts = new()
    {
        new WeatherForecast { Id = Guid.NewGuid().ToString(), Date = DateOnly.Parse("2022-01-06"), TemperatureC = 1, Summary = "Freezing" },
        new WeatherForecast { Id = Guid.NewGuid().ToString(), Date = DateOnly.Parse("2022-01-07"), TemperatureC = 14, Summary = "Bracing" },
        new WeatherForecast { Id = Guid.NewGuid().ToString(), Date = DateOnly.Parse("2022-01-08"), TemperatureC = -13, Summary = "Freezing" },
        new WeatherForecast { Id = Guid.NewGuid().ToString(), Date = DateOnly.Parse("2022-01-09"), TemperatureC = -16, Summary = "Balmy" },
        new WeatherForecast { Id = Guid.NewGuid().ToString(), Date = DateOnly.Parse("2022-01-10"), TemperatureC = -2, Summary = "Chilly" }
    };

    // The API to return data
    public IActionResult WeatherForecasts()
    {
        return Json(weatherForecasts);
    }
}

In the view Index.cshtml, replace code to like this.

<component type="typeof(BlazorApp.Pages.FetchData)" render-mode="WebAssemblyPrerendered"></component>

@section Scripts {
    <script src="_framework/blazor.webassembly.js"></script>
}

Add the HttpClient to the .NET MVC application by this code in the Program.cs file.

//add httpclient
builder.Services.AddScoped(sp => new HttpClient());

This is because the FetchData component uses HttpClient.

@inject HttpClient Http

so we need to add the HttpClient to the .NET MVC project because when the .NET MVC application pre-renders the FetchData component, it must create a HttpClient instance and inject to the component.

Last, in the FetchData component in Blazor WebAssembly, replace the OnInitializedAsync method with this.

protected override async Task OnAfterRenderAsync(bool firstRender)
{
    if (firstRender)
    {
        forecasts = await Http.GetFromJsonAsync<WeatherForecast[]>("Home/WeatherForecasts");
        // Force view update
        StateHasChanged();
    }
}

Run the .NET MVC project and see the result.

Blazor WebAssemply and .NET MVC

Add the Create function

In the .NET MVC project, add create action in HomeController as a create api.

    [HttpPost]
    public IActionResult WeatherForecasts([FromBody] WeatherForecast model)
    {
        if (ModelState.IsValid)
        {
            model.Id = Guid.NewGuid().ToString();
            weatherForecasts.Add(model);
        }
        return Json(weatherForecasts);
    }

Change the FetchData component like this to add the Create method on view.

@using BlazorApp.Models
@inject HttpClient Http

<PageTitle>Weather Forecast</PageTitle>

<h1>Weather Forecast</h1>

<p>This component demonstrates fetching data from the server.</p>

@if (forecasts == null)
{
    <p><em>Loading...</em></p>
}
else
{
    <EditForm Model="@createModel" OnValidSubmit="OnCreateFormSubmit" id="form-create">
        <DataAnnotationsValidator />
        <ValidationSummary />
    </EditForm>

    <table class="table">
        <thead>
            <tr>
                <th>Date</th>
                <th>Temp. (C)</th>
                <th>Temp. (F)</th>
                <th>Summary</th>
                <th>Action</th>
            </tr>
        </thead>
        <tbody>
            <tr>
                <td>
                    <InputDate class="form-control" @bind-Value="@createModel.Date" form="form-create"></InputDate>
                </td>
                <td>
                    <InputNumber class="form-control" @bind-Value="@createModel.TemperatureC" form="form-create"></InputNumber>
                </td>
                <td>
                    @createModel.TemperatureF
                </td>
                <td>
                    <InputText class="form-control" @bind-Value="@createModel.Summary" form="form-create"></InputText>
                </td>
                <td>
                    <button type="submit" class="btn btn-primary" form="form-create">Add</button>
                </td>
            </tr>

            @foreach (var forecast in forecasts)
            {
                <tr>
                    <td>@forecast.Date.ToShortDateString()</td>
                    <td>@forecast.TemperatureC</td>
                    <td>@forecast.TemperatureF</td>
                    <td>@forecast.Summary</td>
                    <td></td>
                </tr>
            }
        </tbody>
    </table>
}

@code {
    private WeatherForecast[]? forecasts;

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            forecasts = await Http.GetFromJsonAsync<WeatherForecast[]>("Home/WeatherForecasts");
            // Force view update
            StateHasChanged();
        }
    }

    WeatherForecast createModel = new();

    async Task OnCreateFormSubmit()
    {
        using var createReq = await Http.PostAsJsonAsync("Home/WeatherForecasts", createModel);
        createReq.EnsureSuccessStatusCode();
        forecasts = await createReq.Content.ReadFromJsonAsync<WeatherForecast[]>();
    }
}

Save the file, then run the .NET MVC application.

Blazor WebAssembly Create function

The Edit function

Add the Edit function is the same as the Create function. First, add the api action in HomeController.

[HttpPut]
public IActionResult WeatherForecasts(string id, [FromBody] WeatherForecast model)
{
    if (ModelState.IsValid)
    {
        var weatherForecast = weatherForecasts.FirstOrDefault(x => x.Id == id);
        if (weatherForecast != null)
        {
            weatherForecast.Date = model.Date;
            weatherForecast.TemperatureC = model.TemperatureC;
            weatherForecast.Summary = model.Summary;
        }
    }
    return Json(weatherForecasts);
}

In the WeatherForecast class, add the IsEditting field to mark the editing entity.

public bool IsEditting { get; set; }

Then edit the FetchData component like this.

@using BlazorApp.Models;
@inject HttpClient Http

<PageTitle>Weather forecast</PageTitle>

<h1>Weather forecast</h1>

<p>This component demonstrates fetching data from the server.</p>

@if (forecasts == null)
{
    <p><em>Loading...</em></p>
}
else
{
    <EditForm Model="@createModel" OnValidSubmit="OnCreateFormSubmit" id="form-create">
        <DataAnnotationsValidator />
        <ValidationSummary />
    </EditForm>

    <EditForm Model="@editModel" OnValidSubmit="OnEditFormSubmit" id="form-edit">
        <DataAnnotationsValidator />
        <ValidationSummary />
    </EditForm>

    <table class="table">
        <thead>
            <tr>
                <th>Date</th>
                <th>Temp. (C)</th>
                <th>Temp. (F)</th>
                <th>Summary</th>
                <th>Action</th>
            </tr>
        </thead>
        <tbody>
            <tr>
                <td>
                    <InputDate class="form-control" @bind-Value="@createModel.Date" form="form-create"></InputDate>
                </td>
                <td>
                    <InputNumber class="form-control" @bind-Value="@createModel.TemperatureC" form="form-create"></InputNumber>
                </td>
                <td>
                    @createModel.TemperatureF
                </td>
                <td>
                    <InputText class="form-control" @bind-Value="@createModel.Summary" form="form-create"></InputText>
                </td>
                <td>
                    <button type="submit" class="btn btn-primary" form="form-create">Add</button>
                </td>
            </tr>

            @foreach (var forecast in forecasts)
            {
                if (forecast.IsEditting)
                {
                    <tr>
                        <td>
                            <InputDate class="form-control" @bind-Value="@editModel.Date" form="form-edit"></InputDate>
                        </td>
                        <td>
                            <InputNumber class="form-control" @bind-Value="@editModel.TemperatureC" form="form-edit"></InputNumber>
                        </td>
                        <td>
                            @editModel.TemperatureF
                        </td>
                        <td>
                            <InputText class="form-control" @bind-Value="@editModel.Summary" form="form-edit"></InputText>
                        </td>
                        <td>
                            <button type="submit" class="btn btn-outline-primary" form="form-edit">Save</button>
                            <button type="button" class="btn btn-outline-secondary" @onclick="() => { forecast.IsEditting = false; }">Cancel</button>
                        </td>
                    </tr>
                }
                else
                {
                    <tr>
                        <td>@forecast.Date.ToShortDateString()</td>
                        <td>@forecast.TemperatureC</td>
                        <td>@forecast.TemperatureF</td>
                        <td>@forecast.Summary</td>
                        @if (forecasts.All(s => !s.IsEditting))
                        {
                            <td>
                                <button type="button" class="btn btn-outline-primary" @onclick="() => { (editModel = forecast).IsEditting = true; }">Edit</button>
                            </td>
                        }
                        else
                        {
                            <td></td>
                        }
                    </tr>
                }
            }
        </tbody>
    </table>
}

@code {
    private WeatherForecast[]? forecasts;

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            forecasts = await Http.GetFromJsonAsync<WeatherForecast[]>("Home/WeatherForecasts");
            //force view update
            StateHasChanged();
        }
    }

    WeatherForecast createModel = new();

    async Task OnCreateFormSubmit()
    {
        using var createReq = await Http.PostAsJsonAsync("Home/WeatherForecasts", createModel);
        createReq.EnsureSuccessStatusCode();
        forecasts = await createReq.Content.ReadFromJsonAsync<WeatherForecast[]>();
    }

    WeatherForecast editModel = new();

    async Task OnEditFormSubmit()
    {
        using var createReq = await Http.PutAsJsonAsync($"Home/WeatherForecasts/{editModel.Id}", editModel);
        createReq.EnsureSuccessStatusCode();
        forecasts = await createReq.Content.ReadFromJsonAsync<WeatherForecast[]>();
    }
}

Now, run the .NET MVC application.

Blazor WebAssemply edit funtion

The Delete function

In the HomeController, add this method.

[HttpDelete]
public IActionResult WeatherForecasts(string id)
{
    var weatherForecast = weatherForecasts.FirstOrDefault(x => x.Id == id);
    if (weatherForecast != null)
    {
        weatherForecasts.Remove(weatherForecast);
    }
    return Json(weatherForecasts);
}

In the FetchData component, add the delete method.

async Task OnDeleteSubmit(string id)
{
    using var createReq = await Http.DeleteAsync($"Home/WeatherForecasts/{id}");
    createReq.EnsureSuccessStatusCode();
    forecasts = await createReq.Content.ReadFromJsonAsync<WeatherForecast[]>();
}

Then add the Delete button after the Edit button.

<td>
    <button type="button" class="btn btn-outline-primary" @onclick="() => { (editModel = forecast).IsEditting = true; }">Edit</button>
    <button type="button" class="btn btn-outline-danger" @onclick="async () => await OnDeleteSubmit(forecast.Id!)">Delete</button>
</td>

Run the application, and done.

Blazor WebAssembly delete function

Use component param to serve data before rendering component

Before rendering the Blazor WebAssembly component, we can pass a param to it to serve data. It means the component has data already before the component is rendered. In the HomeController, change the Index method like this.

public IActionResult Index()
{
    return View(weatherForecasts.ToArray());
}

This code is in the FetchData component and comment OnAfterRenderAsync method.

private WeatherForecast[]? forecasts;

[Parameter] public WeatherForecast[]? initForecasts { get; set; }

protected override void OnInitialized()
{
    forecasts = initForecasts;
}

//protected override async Task OnAfterRenderAsync(bool firstRender)
//{
//    if (firstRender)
//    {
//        forecasts = await Http.GetFromJsonAsync<WeatherForecast[]>("Home/WeatherForecasts");
//        //force view update
//        StateHasChanged();
//    }
//}

Now, pass data to component by component param.

@using BlazorApp.Models;
@model WeatherForecast[]

<component type="typeof(BlazorApp.Pages.FetchData)" render-mode="WebAssemblyPrerendered" param-initForecasts=@Model></component>

Run the application, and we will see the data is loaded without the component call api get data.

Add Antiforgery to more secure

We can use the Antiforgery in Asp.Net to application more secure. First, add this code to the Program.cs of .NET MVC to add Antiforgery and config it option:

//add antiforgery use request header
builder.Services.AddAntiforgery(options =>
{
    options.HeaderName = "X-CSRF-TOKEN";
});

Add [AutoValidateAntiforgeryToken] attribute in which want a method to apply Antiforger.

[HttpPost]
[AutoValidateAntiforgeryToken]
public IActionResult WeatherForecasts([FromBody] WeatherForecast model)
{
    if (ModelState.IsValid)
    {
        model.Id = Guid.NewGuid().ToString();
        weatherForecasts.Add(model);
    }
    return Json(weatherForecasts);
}

Declare antiRequestToken in the FetchData component.

[Parameter] public required string antiRequestToken { get; set; }

Use antiRequestToken in request.

async Task OnCreateFormSubmit()
{
    // Remove if existed and add anti-token in the request header
    Http.DefaultRequestHeaders.Remove("X-CSRF-TOKEN");
    Http.DefaultRequestHeaders.TryAddWithoutValidation("X-CSRF-TOKEN", antiRequestToken);

    using var createReq = await Http.PostAsJsonAsync("Home/WeatherForecasts", createModel);
    createReq.EnsureSuccessStatusCode();
    forecasts = await createReq.Content.ReadFromJsonAsync<WeatherForecast[]>();
}

Pass anti-token from view to component.

@inject Microsoft.AspNetCore.Antiforgery.IAntiforgery antiforgery

@{
    ViewData["Title"] = "Home Page";
    string requestToken = antiforgery.GetAndStoreTokens(Context).RequestToken!;
}

<component type="typeof(BlazorApp.Pages.FetchData)" render-mode="WebAssemblyPrerendered" param-initForecasts=@Model param-antiRequestToken="@requestToken"></component>

Done. Run the application and see the Antiforgery working now.

Conclusion

This blog post has shown how to combine Blazor WebAssembly and .NET MVC applications in a single project. This approach allows us to use the benefits of both technologies, such as the performance and interactivity of Blazor and the flexibility and compatibility of MVC. We have seen how to create a new Blazor WebAssembly project and reference it in the existing MVC project, how to configure the routing and hosting for both frameworks and how to use Blazor components in MVC views and vice versa. We have also learned how to use some of the services provided by Blazor, such as the IJSRuntime or the state management.

I hope you have found this blog post useful and interesting, please share it with your friends. Thank you for reading.