Ask me anything
Ithy Logo
Sign in to save

Developing a Splitwise-Like MVP with C# and Blazor

Comprehensive Guide to Building Web and Mobile Expense Sharing Applications

expense tracking app interface

Key Takeaways

  • User Authentication: Implement secure registration and login using ASP.NET Core Identity.
  • Group and Expense Management: Facilitate creation of groups, addition of expenses, and dynamic balance calculations.
  • Cross-Platform Deployment: Utilize Blazor and .NET MAUI to develop both web and mobile applications seamlessly.

Introduction

Building a Minimum Viable Product (MVP) of a Splitwise-like application involves creating a platform where users can manage shared expenses within groups. Utilizing C# and Blazor, developers can create both web and mobile applications that mirror the core functionalities of Splitwise. This guide provides a step-by-step approach, integrating best practices and comprehensive strategies to ensure a robust and scalable application.


Core Features of the MVP

1. User Authentication

Secure user authentication is the backbone of the application, ensuring that user data remains protected. Implementing user registration and login functionalities using ASP.NET Core Identity provides a robust framework for managing user credentials and roles.

2. Group Management

Users should be able to create and manage groups, invite other members, and organize expenses within these groups. Each group will have a unique name and a list of enrolled members.

3. Expense Management

Allowing users to add, view, and manage expenses is crucial. Each expense should include details such as description, amount, the user who paid, and how the expense is split among group members.

4. Balance Calculation

The application must calculate and display balances for each user within a group, indicating who owes whom and the corresponding amounts. This feature ensures transparency and ease of settling debts.

5. User Interface

A clean and responsive user interface enhances user experience. Utilizing Blazor components along with UI frameworks like MudBlazor or Radzen can expedite the development process and ensure a consistent look and feel across platforms.


Technical Stack

  • Frontend: Blazor (WebAssembly or Server)
  • Backend: ASP.NET Core Web API
  • Database: SQL Server or SQLite
  • Authentication: ASP.NET Core Identity
  • UI Framework: MudBlazor or Radzen

Implementation Steps

1. Project Setup

a. Create a New Blazor Project

Use the .NET CLI to initialize a new Blazor WebAssembly or Blazor Server project:

dotnet new blazorwasm -o SplitwiseMVP

b. Configure the Database

Set up SQL Server or SQLite for data storage. Entity Framework Core can be used for database operations. Install necessary NuGet packages:

dotnet add package Microsoft.EntityFrameworkCore
dotnet add package Microsoft.EntityFrameworkCore.SqlServer
dotnet add package Microsoft.AspNetCore.Identity.EntityFrameworkCore

c. Implement ASP.NET Core Identity

ASP.NET Core Identity handles user authentication and authorization. Configure it in your Startup.cs:

services.AddDefaultIdentity<IdentityUser>()
        .AddEntityFrameworkStores<ApplicationDbContext>();

2. Defining the Application Architecture

Adopt the MVP (Model-View-Presenter) or MVVM (Model-View-ViewModel) architectural pattern to ensure a clean separation of concerns:

  • Model: Represents the data and business logic (e.g., User, Group, Expense).
  • View: The UI components built with Blazor (.razor files).
  • Presenter/ViewModel: Handles the interaction logic between the view and the model.

3. Developing Core Features

a. User Authentication

Implement registration and login pages using Blazor's EditForm component. Utilize ASP.NET Core Identity for managing user data.

// Register.razor
<EditForm Model="registerModel" OnValidSubmit="HandleRegister">
    <InputText @bind-Value="registerModel.Email" placeholder="Email" />
    <InputPassword @bind-Value="registerModel.Password" placeholder="Password" />
    <button type="submit">Register</button>
</EditForm>

b. Group Management

Create a Group model and implement CRUD operations using a dedicated service.

public class Group
{
    public int Id { get; set; }
    public string Name { get; set; }
    public List<User> Members { get; set; } = new List<User>();
}

public interface IGroupService
{
    Task CreateGroup(Group group);
    Task<List<Group>> GetUserGroups(string userId);
    // Additional methods...
}

public class GroupService : IGroupService
{
    // Implementation...
}

c. Expense Management

Define an Expense model and develop functionalities to add, view, and split expenses.

public class Expense
{
    public int Id { get; set; }
    public string Description { get; set; }
    public decimal Amount { get; set; }
    public string PaidBy { get; set; }
    public Dictionary<string, decimal> SplitAmong { get; set; } = new Dictionary<string, decimal>();
}

public interface IExpenseService
{
    Task AddExpense(Expense expense);
    Task<List<Expense>> GetExpenses(int groupId);
    // Additional methods...
}

public class ExpenseService : IExpenseService
{
    // Implementation...
}

d. Balance Calculation

Create a service to calculate and display user balances within a group.

public class BalanceService
{
    public Dictionary<string, decimal> CalculateBalances(List<Expense> expenses)
    {
        var balances = new Dictionary<string, decimal>();

        foreach (var expense in expenses)
        {
            foreach (var (member, amount) in expense.SplitAmong)
            {
                if (!balances.ContainsKey(member))
                    balances[member] = 0;

                balances[member] -= amount;
            }

            if (!balances.ContainsKey(expense.PaidBy))
                balances[expense.PaidBy] = 0;

            balances[expense.PaidBy] += expense.Amount;
        }

        return balances;
    }
}

e. User Interface

Develop responsive UI pages using Blazor components. Incorporate MudBlazor or Radzen for enhanced UI elements.

i. Dashboard

Create a dashboard to display user groups and summary of balances.

<h3>Dashboard</h3>
<div>
    @foreach(var group in userGroups)
    {
        <div class="group-card">
            <h4>@group.Name</h4>
            <p>Members: @group.Members.Count</p>
        </div>
    }
</div>
ii. Add Expense

Implement a form to add new expenses within a group.

<EditForm Model="newExpense" OnValidSubmit="AddExpense">
    <InputText @bind-Value="newExpense.Description" placeholder="Description" />
    <InputNumber @bind-Value="newExpense.Amount" placeholder="Amount" />
    <Select @bind-Value="newExpense.PaidBy">
        @foreach(var member in groupMembers)
        {
            <option value="@member.Id">@member.Name</option>
        }
    </Select>
    <button type="submit">Add Expense</button>
</EditForm>

4. Testing

Ensure all functionalities work as intended by implementing unit tests using frameworks like xUnit or NUnit. Test cases should cover user authentication, group and expense management, and balance calculations.

5. Deployment

Deploy the web application to platforms like Azure or AWS. For mobile deployment, use .NET MAUI or Blazor Hybrid to create cross-platform mobile applications.

6. Enhancements

After establishing the MVP, consider adding advanced features such as notifications, payment integrations, and more sophisticated UI/UX enhancements to improve user engagement.


Detailed Implementation

1. Setting Up the Project

a. Initialize Blazor Project

Create a new Blazor WebAssembly project using the .NET CLI:

dotnet new blazorwasm -o SplitwiseMVP

b. Configure Services

In Program.cs, register necessary services:

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

builder.Services.AddDefaultIdentity<IdentityUser>()
    .AddEntityFrameworkStores<ApplicationDbContext>();

builder.Services.AddScoped<IGroupService, GroupService>();
builder.Services.AddScoped<IExpenseService, ExpenseService>();
builder.Services.AddScoped<BalanceService>();

c. Database Context

Define the database context inheriting from IdentityDbContext:

public class ApplicationDbContext : IdentityDbContext
{
    public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
        : base(options)
    {
    }

    public DbSet<Group> Groups { get; set; }
    public DbSet<Expense> Expenses { get; set; }
}

2. Implementing User Authentication

a. Registration Page

Develop a registration form using Blazor's EditForm component:

<EditForm Model="registerModel" OnValidSubmit="HandleRegistration">
    <DataAnnotationsValidator />
    <InputText @bind-Value="registerModel.Email" placeholder="Email" />
    <InputPassword @bind-Value="registerModel.Password" placeholder="Password" />
    <button type="submit">Register</button>
</EditForm>

@code {
    private RegisterModel registerModel = new RegisterModel();

    private async Task HandleRegistration()
    {
        // Registration logic using UserManager
    }
}

b. Login Page

Create a login form to authenticate users:

<EditForm Model="loginModel" OnValidSubmit="HandleLogin">
    <DataAnnotationsValidator />
    <InputText @bind-Value="loginModel.Email" placeholder="Email" />
    <InputPassword @bind-Value="loginModel.Password" placeholder="Password" />
    <button type="submit">Login</button>
</EditForm>

@code {
    private LoginModel loginModel = new LoginModel();

    private async Task HandleLogin()
    {
        // Authentication logic using SignInManager
    }
}

3. Group Management

a. Group Model

Define the Group class:

public class Group
{
    public int Id { get; set; }
    public string Name { get; set; }
    public ICollection<IdentityUser> Members { get; set; } = new List<IdentityUser>();
    public ICollection<Expense> Expenses { get; set; } = new List<Expense>();
}

b. Group Service

Implement the service to handle group operations:

public class GroupService : IGroupService
{
    private readonly ApplicationDbContext _context;
    private readonly UserManager<IdentityUser> _userManager;

    public GroupService(ApplicationDbContext context, UserManager<IdentityUser> userManager)
    {
        _context = context;
        _userManager = userManager;
    }

    public async Task CreateGroup(Group group, string userId)
    {
        var user = await _userManager.FindByIdAsync(userId);
        group.Members.Add(user);
        _context.Groups.Add(group);
        await _context.SaveChangesAsync();
    }

    public async Task<List<Group>> GetUserGroups(string userId)
    {
        return await _context.Groups
            .Include(g => g.Members)
            .Where(g => g.Members.Any(m => m.Id == userId))
            .ToListAsync();
    }

    // Additional methods...
}

4. Expense Management

a. Expense Model

Define the Expense class:

public class Expense
{
    public int Id { get; set; }
    public string Description { get; set; }
    public decimal Amount { get; set; }
    public string PaidById { get; set; }
    public IdentityUser PaidBy { get; set; }
    public int GroupId { get; set; }
    public Group Group { get; set; }
    public ICollection<ExpenseSplit> Splits { get; set; } = new List<ExpenseSplit>();
}

b. Expense Split Model

Create a model to represent how expenses are split among users:

public class ExpenseSplit
{
    public int Id { get; set; }
    public int ExpenseId { get; set; }
    public Expense Expense { get; set; }
    public string UserId { get; set; }
    public IdentityUser User { get; set; }
    public decimal Amount { get; set; }
}

c. Expense Service

Implement the service to handle expense operations:

public class ExpenseService : IExpenseService
{
    private readonly ApplicationDbContext _context;

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

    public async Task AddExpense(Expense expense)
    {
        _context.Expenses.Add(expense);
        await _context.SaveChangesAsync();
    }

    public async Task<List<Expense>> GetExpenses(int groupId)
    {
        return await _context.Expenses
            .Include(e => e.PaidBy)
            .Include(e => e.Splits)
                .ThenInclude(s => s.User)
            .Where(e => e.GroupId == groupId)
            .ToListAsync();
    }

    // Additional methods...
}

5. Balance Calculation

Implement a service to calculate user balances based on expenses:

public class BalanceService
{
    public Dictionary<string, decimal> CalculateBalances(List<Expense> expenses)
    {
        var balances = new Dictionary<string, decimal>();

        foreach (var expense in expenses)
        {
            foreach (var split in expense.Splits)
            {
                if (!balances.ContainsKey(split.UserId))
                    balances[split.UserId] = 0;

                balances[split.UserId] -= split.Amount;
            }

            if (!balances.ContainsKey(expense.PaidById))
                balances[expense.PaidById] = 0;

            balances[expense.PaidById] += expense.Amount;
        }

        return balances;
    }
}

6. User Interface Development

a. Dashboard Page

Create a dashboard to display user groups and their corresponding balances:

<@page "/dashboard">
<h3>Dashboard</h3>
<div>
    @foreach(var group in userGroups)
    {
        <div class="group-card" @onclick="() => NavigateToGroup(group.Id)">
            <h4>@group.Name</h4>
            <p>Members: @group.Members.Count</p>
            <p>Balance: @GetGroupBalance(group.Id)</p>
        </div>
    }
</div>

@code {
    private List<Group> userGroups;

    protected override async Task OnInitializedAsync()
    {
        userGroups = await GroupService.GetUserGroups(currentUserId);
    }

    private string GetGroupBalance(int groupId)
    {
        var expenses = ExpenseService.GetExpenses(groupId);
        var balances = BalanceService.CalculateBalances(expenses);
        // Format balances for display
    }

    private void NavigateToGroup(int groupId)
    {
        NavigationManager.NavigateTo($"/group/{groupId}");
    }
}

b. Add Expense Component

Implement a form to add new expenses within a group:

<@page "/group/{GroupId}/add-expense">
<h3>Add Expense</h3>
<EditForm Model="newExpense" OnValidSubmit="HandleAddExpense">
    <DataAnnotationsValidator />
    <InputText @bind-Value="newExpense.Description" placeholder="Description" />
    <InputNumber @bind-Value="newExpense.Amount" placeholder="Amount" />
    <Select @bind-Value="newExpense.PaidById">
        @foreach(var member in groupMembers)
        {
            <option value="@member.Id">@member.UserName</option>
        }
    </Select>
    <button type="submit">Add Expense</button>
</EditForm>

@code {
    [Parameter]
    public int GroupId { get; set; }

    private Expense newExpense = new Expense();
    private List<IdentityUser> groupMembers;

    protected override async Task OnInitializedAsync()
    {
        groupMembers = await GroupService.GetGroupMembers(GroupId);
    }

    private async Task HandleAddExpense()
    {
        newExpense.GroupId = GroupId;
        await ExpenseService.AddExpense(newExpense);
        NavigationManager.NavigateTo($"/group/{GroupId}");
    }
}

7. Testing and Quality Assurance

Implement unit tests to validate the functionality of services and components. Utilizing testing frameworks like xUnit or NUnit ensures the reliability and stability of the application.

// Example of a unit test for BalanceService
public class BalanceServiceTests
{
    [Fact]
    public void CalculateBalances_ShouldReturnCorrectBalances()
    {
        // Arrange
        var expenses = new List<Expense>()
        {
            new Expense
            {
                Amount = 100,
                PaidById = "user1",
                Splits = new List<ExpenseSplit>()
                {
                    new ExpenseSplit { UserId = "user1", Amount = 50 },
                    new ExpenseSplit { UserId = "user2", Amount = 50 },
                }
            },
            new Expense
            {
                Amount = 150,
                PaidById = "user2",
                Splits = new List<ExpenseSplit>()
                {
                    new ExpenseSplit { UserId = "user1", Amount = 75 },
                    new ExpenseSplit { UserId = "user2", Amount = 75 },
                }
            }
        };

        var balanceService = new BalanceService();

        // Act
        var balances = balanceService.CalculateBalances(expenses);

        // Assert
        Assert.Equal(25, balances["user1"]);
        Assert.Equal(-25, balances["user2"]);
    }
}

8. Deployment

a. Web Application Deployment

Deploy the web application to Azure Static Web Apps or AWS Elastic Beanstalk. Configure CI/CD pipelines for automated deployments.

b. Mobile Application Deployment

Utilize .NET MAUI or Blazor Hybrid to create mobile applications. Deploy to platforms like Google Play Store and Apple App Store.

<!-- Example of configuring MAUI Blazor App -->
public class MauiProgram
{
    public static MauiApp CreateMauiApp()
    {
        var builder = MauiApp.CreateBuilder();
        builder
            .UseMauiApp<App>()
            .ConfigureFonts(fonts =>
            {
                fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
            });

        builder.Services.AddBlazorWebView();
        builder.Services.AddScoped<IGroupService, GroupService>();
        builder.Services.AddScoped<IExpenseService, ExpenseService>();
        builder.Services.AddScoped<BalanceService>();

        return builder.Build();
    }
}

Sample Code Snippets

Group Model

Defining the Group entity:

public class Group
{
    public int Id { get; set; }
    public string Name { get; set; }
    public List<string> Members { get; set; } = new List<string>();
}

Expense Model

Defining the Expense entity:

public class Expense
{
    public int Id { get; set; }
    public string Description { get; set; }
    public decimal Amount { get; set; }
    public string PaidBy { get; set; }
    public Dictionary<string, decimal> SplitAmong { get; set; } = new Dictionary<string, decimal>();
}

Balance Calculation Example

Calculating user balances:

public class BalanceService
{
    public Dictionary<string, decimal> CalculateBalances(List<Expense> expenses)
    {
        var balances = new Dictionary<string, decimal>();

        foreach (var expense in expenses)
        {
            foreach (var (member, amount) in expense.SplitAmong)
            {
                if (!balances.ContainsKey(member))
                    balances[member] = 0;

                balances[member] -= amount;
            }

            if (!balances.ContainsKey(expense.PaidBy))
                balances[expense.PaidBy] = 0;

            balances[expense.PaidBy] += expense.Amount;
        }

        return balances;
    }
}

Blazor Razor Component Example

@page "/add-expense"

<EditForm Model="newExpense" OnValidSubmit="AddExpense">
    <InputText @bind-Value="newExpense.Description" placeholder="Expense Description" />
    <InputNumber @bind-Value="newExpense.Amount" placeholder="Amount" />
    <button type="submit">Add Expense</button>
</EditForm>

@code {
    private Expense newExpense = new Expense();

    private async Task AddExpense()
    {
        await ExpenseService.AddExpense(newExpense);
    }
}

Conclusion

Developing an MVP for a Splitwise-like application using C# and Blazor involves meticulous planning and execution. By focusing on core functionalities such as user authentication, group and expense management, and balance calculations, developers can create a robust platform. Leveraging Blazor's capabilities for both web and mobile deployment ensures a wide reach and seamless user experience. As the application evolves, integrating advanced features and optimizing performance will further enhance its utility and user satisfaction.


References

By following this comprehensive guide, you can successfully develop a functional MVP of a Splitwise-like application using C# and Blazor, tailored for both web and mobile platforms. This foundation allows for scalability and integration of additional features to meet user demands and enhance overall application performance.



Last updated January 21, 2025

AI-assisted research. Sources, methodology and corrections · Source-checked guides and tools · Explore published answers

Ask Ithy AI