Part 2/3  A Full-Stack Expense Tracking App With Blazor, Web API & EF Core and SQL Server Express.

Part 2/3 A Full-Stack Expense Tracking App With Blazor, Web API & EF Core and SQL Server Express.

Working Frontend For Expense Tracking App.

In Part 1 we built a working API that could create, read, update and delete expenses. Part 2 would be moving on from there by creating services to make our code better and also working on the front end.

NOTE: This tutorial uses .NET 6.0 (Long Term Support) and may not work the same for other versions.

Add Services In Server Project

Using best practices we should not have "fat controllers". The controller is supposed to deal only with the HTTP requests while other things are moved to services.

Create Services Folders And files

  • Right-Click On BlazorExpenseTracker.Server -> Add -> New Folder

  • Name The Folder Services.

  • In the Services folder, we create another folder named ExpenseService.

  • Right-Click On ExpenseService folder -> Add -> Class -> Select Interface

  • Name the Interface IExpenseService.cs

namespace BlazorExpenseTracker.Server.Services.ExpenseService
{
    public interface IExpenseService
    {
    }
}
  • Right-Click On ExpenseService folder -> Add -> Class

  • Name the Class ExpenseService.cs

  • Let ExpenseService.cs inherit from IExpenseService.cs

namespace BlazorExpenseTracker.Server.Services.ExpenseService
{
    public class ExpenseService: IExpenseService
    {
    }
}

Register ExpenseService.

In BlazorExpenseTracker.Server/Program.cs

using BlazorExpenseTracker.Server.Data;
using BlazorExpenseTracker.Server.Services.ExpenseService;
using Microsoft.AspNetCore.ResponseCompression;
using Microsoft.EntityFrameworkCore;
using Microsoft.OpenApi.Models;

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.

builder.Services.AddControllersWithViews();
builder.Services.AddRazorPages();

builder.Services.AddDbContext<DataContext>(options =>
{
    options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection"));
});

builder.Services.AddSwaggerGen(option =>
{
    option.SwaggerDoc("v1", new OpenApiInfo { Title = "BlazorExpenseTracker API", Version = "v1" });
});


// Register ExpenseService
builder.Services.AddScoped<IExpenseService, ExpenseService>();


var app = builder.Build();

// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
    app.UseWebAssemblyDebugging();

    app.UseSwagger();
    app.UseSwaggerUI();
}
else
{
    app.UseExceptionHandler("/Error");
    // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
    app.UseHsts();
}

app.UseHttpsRedirection();

app.UseBlazorFrameworkFiles();
app.UseStaticFiles();

app.UseRouting();


app.MapRazorPages();
app.MapControllers();
app.MapFallbackToFile("index.html");

app.Run();

Add IExpenseService Code

In IExpenseService.cs

using BlazorExpenseTracker.Shared.Models;
using System.Collections.Generic;

namespace BlazorExpenseTracker.Server.Services.ExpenseService
{
    public interface IExpenseService
    {

        Task<List<ExpenseModel>> GetExpenseAsync();
        Task<ExpenseModel> CreateExpenseAsync(ExpenseModel expense);
        Task<ExpenseModel> EditExpenseAsync(ExpenseModel expense, int id);
        Task RemoveExpense(int id);


    }
}

In ExpenseService.cs add a constructor and implement the interface. Finaly add the code using _context in each method.

NOTE: Add async to every method

using BlazorExpenseTracker.Server.Data;
using BlazorExpenseTracker.Shared.Models;
using Microsoft.EntityFrameworkCore;

namespace BlazorExpenseTracker.Server.Services.ExpenseService
{
    public class ExpenseService : IExpenseService
    {
        private readonly DataContext _context;

        public ExpenseService(DataContext context)
        {
            _context = context;
        }

        public async Task<ExpenseModel> CreateExpenseAsync(ExpenseModel expense)
        {
            var response = await _context.Expenses.AddAsync(expense);
            await _context.SaveChangesAsync();
            return response.Entity;

        }

        public async Task<ExpenseModel> EditExpenseAsync(ExpenseModel expense, int id)
        {
            ExpenseModel response = null;

            var DbExpense = await _context.Expenses.FirstOrDefaultAsync(e => e.Id == id);

            if (DbExpense != null)
            {
                DbExpense.Amount = expense.Amount;
                DbExpense.Title = expense.Title;
                DbExpense.Description = expense.Description;
                DbExpense.CreatedAt = DateTime.UtcNow;

                await _context.SaveChangesAsync();

                response= DbExpense;

            }
            return response;
        }

        public async Task<List<ExpenseModel>> GetExpenseAsync()
        {
            var response = await _context.Expenses.ToListAsync();
            return response;
        }

        public async Task RemoveExpense(int id)
        {
            var DbExpense = await _context.Expenses.FirstOrDefaultAsync(e => e.Id == id);

            if (DbExpense != null)
            {
                _context.Expenses.Remove(DbExpense);

            }


            await _context.SaveChangesAsync();
        }
    }
}

Update ExpenseController

In ExpenseController.cs use the services created.

  • Remove context and Inject ExpenseService.
using BlazorExpenseTracker.Server.Data;
using BlazorExpenseTracker.Server.Services.ExpenseService;
using BlazorExpenseTracker.Shared.Models;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;

namespace BlazorExpenseTracker.Server.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class ExpensesController : ControllerBase
    {
        private readonly IExpenseService _expenseService;

        public ExpensesController(IExpenseService expenseService )
        {
            _expenseService = expenseService;
        }

        [HttpGet]
        public async Task<ActionResult<List<ExpenseModel>>> GetAllExpensesAsync()
        {
            List<ExpenseModel> response = await _expenseService.GetExpenseAsync();
            return Ok(response);
        }

        [HttpPost]
        public async Task<ActionResult<ExpenseModel>> CreateExpenseAsync(ExpenseModel expense)
        {
            ExpenseModel response = await _expenseService.CreateExpenseAsync(expense);
            return Ok(response);

        }
        [HttpPut]
        [Route("{id}")]
        public async Task<ActionResult<ExpenseModel>> EditExpenseAsync(ExpenseModel expense, int id)
        {
            ExpenseModel response = await _expenseService.EditExpenseAsync(expense, id);

            return Ok(response);
        }

        [HttpDelete]
        [Route("{id}")]
        public async Task RemoveExpense(int id)
        {
            await _expenseService.RemoveExpense(id);

        }
    }
}
  • Run the app and Test the API End Points.

Add Mock Code To Index.razor

First, we would add some placeholder code to BlazorExpenseTracker.Client/Pages/Index.razor to act as mock code to help us see how the page would look.

In BlazorExpenseTracker.Client/Pages/Index.razor

@page "/"

<PageTitle>Home</PageTitle>

<div style="display:flex; flex-direction:column; justify-content:center">
    <div style="display:flex; justify-content:end">
        <a href="#">
            <svg style="width:50px; cursor:pointer " class="svg-icon" viewBox="0 0 20 20">
                <path d="M14.613,10c0,0.23-0.188,0.419-0.419,0.419H10.42v3.774c0,0.23-0.189,0.42-0.42,0.42s-0.419-0.189-0.419-0.42v-3.774H5.806c-0.23,0-0.419-0.189-0.419-0.419s0.189-0.419,0.419-0.419h3.775V5.806c0-0.23,0.189-0.419,0.419-0.419s0.42,0.189,0.42,0.419v3.775h3.774C14.425,9.581,14.613,9.77,14.613,10 M17.969,10c0,4.401-3.567,7.969-7.969,7.969c-4.402,0-7.969-3.567-7.969-7.969c0-4.402,3.567-7.969,7.969-7.969C14.401,2.031,17.969,5.598,17.969,10 M17.13,10c0-3.932-3.198-7.13-7.13-7.13S2.87,6.068,2.87,10c0,3.933,3.198,7.13,7.13,7.13S17.13,13.933,17.13,10"></path>
            </svg>
        </a>
    </div>
    <div style="display:flex; flex-direction:column; text-align:center">
        <p style="color:gray">Total Expenses</p>
        <h2 style="font-weight:600; margin-top:-10px">$100.00</h2>
    </div>

    <div style="display:flex; flex-direction:column; padding-top:64px">
        <div style=" border-bottom: 2px solid lightgray; margin-bottom:12px; ">
            <div style="display:flex; flex-direction: row; align-items:center; ">

                <div style="display:flex; flex-direction: row; width:100%; cursor:pointer ; align-items:center; ">

                    <div style="height:64px; width: 100%;">
                        <p style="font-weight:600">Eat Out</p>
                        <p style="color:gray">Mon 21 Dec 2023 3:30pm</p>

                    </div>
                    <div style="padding-right:12px">
                        $100
                    </div>
                    <div style="padding:16px;">
                        <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-pencil" viewBox="0 0 16 16"> <path d="M12.146.146a.5.5 0 0 1 .708 0l3 3a.5.5 0 0 1 0 .708l-10 10a.5.5 0 0 1-.168.11l-5 2a.5.5 0 0 1-.65-.65l2-5a.5.5 0 0 1 .11-.168l10-10zM11.207 2.5 13.5 4.793 14.793 3.5 12.5 1.207 11.207 2.5zm1.586 3L10.5 3.207 4 9.707V10h.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.5h.293l6.5-6.5zm-9.761 5.175-.106.106-1.528 3.821 3.821-1.528.106-.106A.5.5 0 0 1 5 12.5V12h-.5a.5.5 0 0 1-.5-.5V11h-.5a.5.5 0 0 1-.468-.325z" /> </svg>
                    </div>


                    <div style=" padding:16px; background-color:antiquewhite; cursor:pointer ;">
                        <svg style="width:18px" class="svg-icon" viewBox="0 0 20 20">
                            <path d="M7.083,8.25H5.917v7h1.167V8.25z M18.75,3h-5.834V1.25c0-0.323-0.262-0.583-0.582-0.583H7.667
                            c-0.322,0-0.583,0.261-0.583,0.583V3H1.25C0.928,3,0.667,3.261,0.667,3.583c0,0.323,0.261,0.583,0.583,0.583h1.167v14
                            c0,0.644,0.522,1.166,1.167,1.166h12.833c0.645,0,1.168-0.522,1.168-1.166v-14h1.166c0.322,0,0.584-0.261,0.584-0.583
                            C19.334,3.261,19.072,3,18.75,3z M8.25,1.833h3.5V3h-3.5V1.833z M16.416,17.584c0,0.322-0.262,0.583-0.582,0.583H4.167
                            c-0.322,0-0.583-0.261-0.583-0.583V4.167h12.833V17.584z M14.084,8.25h-1.168v7h1.168V8.25z M10.583,7.083H9.417v8.167h1.167V7.083
                            z"></path>
                        </svg>
                    </div>
                </div>

            </div>

        </div>
    </div>

</div>



@code {



}

Add Services In Client Project

Create Services Folders And files

  • Right-Click On BlazorExpenseTracker.Client -> Add -> New Folder

  • Name The Folder Services.

  • In the Services folder, we create another folder named ExpenseService.

  • Right-Click On ExpenseService folder -> Add -> Class -> Select Interface

namespace BlazorExpenseTracker.Client.Services
{
    public interface IExpenseService
    {
    }
}
  • Right-Click On ExpenseService folder -> Add -> Class

  • Name the Class ExpenseService.cs

  • Let ExpenseService.cs inherit from IExpenseService.cs

namespace BlazorExpenseTracker.Client.Services
{
    public class ExpenseService : IExpenseService
    {
    }
}

Add IExpenseService Code

In IExpenseService.cs

using BlazorExpenseTracker.Shared.Models;

namespace BlazorExpenseTracker.Client.Services.ExpenseService
{
    public interface IExpenseService
    {
        List<ExpenseModel> Expenses { get; set; }
        decimal TotalExpenses { get; set; }

        Task<List<ExpenseModel>> GetExpensesAsync();
        Task<ExpenseModel> CreateExpenseAsync(ExpenseModel expense);
        Task<ExpenseModel> EditExpenseAsync(ExpenseModel expense, int id);
        Task RemoveExpense(int id);
    }
}

In ExpenseService.cs add a constructor and implement the interface.

Add the bodies for each method as shown below.

NOTE: Add async to every method

using BlazorExpenseTracker.Shared.Models;
using System.Net.Http.Json;

namespace BlazorExpenseTracker.Client.Services.ExpenseService
{
    public class ExpenseService : IExpenseService
    {
        private readonly HttpClient _http;

        public ExpenseService(HttpClient http)
        {
            _http = http;
        }

        public List<ExpenseModel> Expenses { get; set; } = new List<ExpenseModel>();
        public decimal TotalExpenses { get; set; } = 0.0M;

        public async Task<ExpenseModel> CreateExpenseAsync(ExpenseModel expense)
        {
            var response = await _http.PostAsJsonAsync<ExpenseModel>("/api/Expenses", expense);
            return await response.Content.ReadFromJsonAsync<ExpenseModel>();
        }

        public async Task<ExpenseModel> EditExpenseAsync(ExpenseModel expense, int id)
        {
            var response = await _http.PutAsJsonAsync<ExpenseModel>($"/api/Expenses/{id}", expense);
            return await response.Content.ReadFromJsonAsync<ExpenseModel>();
        }

        public async Task<List<ExpenseModel>> GetExpensesAsync()
        {
            var response = await _http.GetFromJsonAsync<List<ExpenseModel>>("/api/Expenses");

            if (response != null)
            {
                Expenses = response;
                CalculateTotalExpenses();

            }

            return Expenses;

        }


        public async Task RemoveExpense(int id)
        {
            await _http.DeleteAsync($"/api/Expenses/{id}");
        }

        private void CalculateTotalExpenses()
        {
            TotalExpenses = 0;

            foreach (var expense in Expenses)
            {
                TotalExpenses += expense.Amount;
            }
        }
    }
}

Show List Of Transactions In Index.razor

Register IExpenseService

  • In BlazorExpenseTracker.Client/Program.cs and add global using BlazorExpenseTracker.Client.Service.ExpenseService;
// Register IExpenseService - global using statement 
global using BlazorExpenseTracker.Client.Services.ExpenseService;

using BlazorExpenseTracker.Client;
using Microsoft.AspNetCore.Components.Web;
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;

var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.RootComponents.Add<App>("#app");
builder.RootComponents.Add<HeadOutlet>("head::after");

builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });

// Register IExpenseService
builder.Services.AddScoped<IExpenseService, ExpenseService>();


await builder.Build().RunAsync();

Show Data In Index.razor

In Index.razor

@page "/"
@inject IExpenseService ExpenseService

<PageTitle>Home</PageTitle>

<div style="display:flex; flex-direction:column; justify-content:center">
    <div style="display:flex; justify-content:end">
        <a href="#">
            <svg style="width:50px; cursor:pointer " class="svg-icon" viewBox="0 0 20 20">
                <path d="M14.613,10c0,0.23-0.188,0.419-0.419,0.419H10.42v3.774c0,0.23-0.189,0.42-0.42,0.42s-0.419-0.189-0.419-0.42v-3.774H5.806c-0.23,0-0.419-0.189-0.419-0.419s0.189-0.419,0.419-0.419h3.775V5.806c0-0.23,0.189-0.419,0.419-0.419s0.42,0.189,0.42,0.419v3.775h3.774C14.425,9.581,14.613,9.77,14.613,10 M17.969,10c0,4.401-3.567,7.969-7.969,7.969c-4.402,0-7.969-3.567-7.969-7.969c0-4.402,3.567-7.969,7.969-7.969C14.401,2.031,17.969,5.598,17.969,10 M17.13,10c0-3.932-3.198-7.13-7.13-7.13S2.87,6.068,2.87,10c0,3.933,3.198,7.13,7.13,7.13S17.13,13.933,17.13,10"></path>
            </svg>
        </a>
    </div>
    <div style="display:flex; flex-direction:column; text-align:center">
        <p style="color:gray">Total Expenses</p>
        <h2 style="font-weight:600; margin-top:-10px">$@Decimal.Round(@ExpenseService.TotalExpenses, 2)</h2>
    </div>

    <div style="display:flex; flex-direction:column; padding-top:64px">
        @foreach (var expense in @ExpenseService.Expenses)
        {
            <div style=" border-bottom: 2px solid lightgray; margin-bottom:12px; ">
                <div style="display:flex; flex-direction: row; align-items:center; ">

                    <div style="display:flex; flex-direction: row; width:100%; cursor:pointer ; align-items:center; ">

                        <div style="height:64px; width: 100%;">
                            <p style="font-weight:600">@expense.Title</p>
                            <p style="color:gray">@expense.CreatedAt.ToString("dd MMM yyyy hh:mm tt")</p>

                        </div>
                        <div style="padding-right:12px">
                            $@Decimal.Round(@expense.Amount, 2)
                        </div>
                        <div style="padding:16px;">
                            <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-pencil" viewBox="0 0 16 16"> <path d="M12.146.146a.5.5 0 0 1 .708 0l3 3a.5.5 0 0 1 0 .708l-10 10a.5.5 0 0 1-.168.11l-5 2a.5.5 0 0 1-.65-.65l2-5a.5.5 0 0 1 .11-.168l10-10zM11.207 2.5 13.5 4.793 14.793 3.5 12.5 1.207 11.207 2.5zm1.586 3L10.5 3.207 4 9.707V10h.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.5h.293l6.5-6.5zm-9.761 5.175-.106.106-1.528 3.821 3.821-1.528.106-.106A.5.5 0 0 1 5 12.5V12h-.5a.5.5 0 0 1-.5-.5V11h-.5a.5.5 0 0 1-.468-.325z" /> </svg>
                        </div>


                        <div style=" padding:16px; background-color:antiquewhite; cursor:pointer ;">
                            <svg style="width:18px" class="svg-icon" viewBox="0 0 20 20">
                                <path d="M7.083,8.25H5.917v7h1.167V8.25z M18.75,3h-5.834V1.25c0-0.323-0.262-0.583-0.582-0.583H7.667
                            c-0.322,0-0.583,0.261-0.583,0.583V3H1.25C0.928,3,0.667,3.261,0.667,3.583c0,0.323,0.261,0.583,0.583,0.583h1.167v14
                            c0,0.644,0.522,1.166,1.167,1.166h12.833c0.645,0,1.168-0.522,1.168-1.166v-14h1.166c0.322,0,0.584-0.261,0.584-0.583
                            C19.334,3.261,19.072,3,18.75,3z M8.25,1.833h3.5V3h-3.5V1.833z M16.416,17.584c0,0.322-0.262,0.583-0.582,0.583H4.167
                            c-0.322,0-0.583-0.261-0.583-0.583V4.167h12.833V17.584z M14.084,8.25h-1.168v7h1.168V8.25z M10.583,7.083H9.417v8.167h1.167V7.083
                            z"></path>
                            </svg>
                        </div>
                    </div>

                </div>

            </div>
        }

    </div>

</div>



@code {

    protected override async Task OnInitializedAsync()
    {
        await ExpenseService.GetExpensesAsync();
    }
}