EF Core with Blazor WebAssembly: Handling Local and Server Data

When I first started mixing EF Core with Blazor WebAssembly, I honestly thought it would be as simple as plugging in my DbContext on the client side and running some queries. I mean, why not, right? Just pull the data from the database and show it directly in the UI.

But reality hit me quickly. Running EF Core on the browser is not the same as running it on a server. You can’t just open a SQL Server connection directly from the client app, it’s not secure, and technically it doesn’t even work. So I had to figure out how to balance local data (things I can store and work with on the client) and server data (the real source of truth, like my SQL Server or PostgreSQL).

This post is about the lessons I learned along the way. Hopefully it can save you a few hours or days of frustration if you’re building Blazor WebAssembly apps with EF Core.

Local vs. Server Data in Blazor WASM

Here’s the thing:

  • Server data is your usual EF Core DbContext connected to SQL Server, PostgreSQL, or any backend DB. That part is straightforward you set up your DbContext in an ASP.NET Core Web API and expose endpoints.
  • Local data is what you can store and query directly in the browser, usually with something like SQLite via EF Core with SQLite provider, or sometimes just an IndexedDB wrapper if you’re going lightweight.

Why does this matter? Because in many projects, you don’t want to call the server every second. Imagine an employee time-tracking system. Do you really need to hit the server API every time the user clicks “Start Shift” or “End Shift”? Not really. You can log that locally first and sync later.

I learned this the hard way when one of our clients asked:

“What happens if the internet goes down? Can employees still log in and out?”

At first, my answer was basically “No.” But that wasn’t good enough. That’s when I started exploring EF Core running locally in Blazor WebAssembly.

Setting Up EF Core with Blazor WASM

On the server side, it’s the usual story. Let’s say you have a simple DbContext for employees and time logs:

C#
public class PayrollDbContext : DbContext
{
    public PayrollDbContext(DbContextOptions<PayrollDbContext> options) : base(options) { }

    public DbSet<Employee> Employees { get; set; }
    public DbSet<TimeLog> TimeLogs { get; set; }
}

public class Employee
{
    public int Id { get; set; }
    public string FullName { get; set; }
}

public class TimeLog
{
    public int Id { get; set; }
    public int EmployeeId { get; set; }
    public DateTime TimeIn { get; set; }
    public DateTime? TimeOut { get; set; }

    public Employee Employee { get; set; }
}

You’d hook this up in your ASP.NET Core backend (Program.cs):

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

Expose the data via a simple API controller:

C#
[ApiController]
[Route("api/[controller]")]
public class TimeLogsController : ControllerBase
{
    private readonly PayrollDbContext _context;

    public TimeLogsController(PayrollDbContext context)
    {
        _context = context;
    }

    [HttpGet("{employeeId}")]
    public async Task<IEnumerable<TimeLog>> GetByEmployee(int employeeId)
    {
        return await _context.TimeLogs
            .Where(t => t.EmployeeId == employeeId)
            .OrderByDescending(t => t.TimeIn)
            .ToListAsync();
    }
}

On the client side, instead of calling the database directly, you’d call this API. But what if the user is offline? That’s where local EF Core with SQLite comes in.

Using EF Core SQLite in Blazor WebAssembly

I’ll be honest: the first time I tried EF Core with SQLite on WASM, I thought it wouldn’t work. Running a real database in the browser sounded crazy. But it does work, and it’s super useful.

You’ll need the SQLite WASM provider:

C#
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="9.0.0" />
<PackageReference Include="SQLitePCLRaw.bundle_wasm" Version="2.1.6" />

Then, in your Blazor WASM Program.cs:

C#
builder.Services.AddDbContextFactory<LocalDbContext>(options =>
    options.UseSqlite("Data Source=local.db"));

And your local context can look like this:

C#
public class LocalDbContext : DbContext
{
    public LocalDbContext(DbContextOptions<LocalDbContext> options) : base(options) { }

    public DbSet<PendingTimeLog> PendingTimeLogs { get; set; }
}

public class PendingTimeLog
{
    public int Id { get; set; }
    public int EmployeeId { get; set; }
    public DateTime TimeIn { get; set; }
    public DateTime? TimeOut { get; set; }
}

So instead of directly saving a TimeLog to the server, you can save it locally first, then sync when the user goes back online.

Real-Life Scenario: Employee Clock-In

In one project, employees often worked in areas with unstable internet. We needed to allow them to clock in/out offline, but still make sure the server eventually got the data.

Here’s how we solved it:

  1. User clicks Clock In.
  2. Blazor WASM app stores the record in LocalDbContext.PendingTimeLogs.
  3. A background sync process (a simple timer) checks every few minutes if the server is reachable.
  4. Once online, it uploads pending logs to the server via API, and clears them from local storage.

Code for saving locally:

C#
using var context = await _dbFactory.CreateDbContextAsync();
context.PendingTimeLogs.Add(new PendingTimeLog
{
    EmployeeId = employeeId,
    TimeIn = DateTime.UtcNow
});
await context.SaveChangesAsync();

Code for syncing:

C#
var pendingLogs = await context.PendingTimeLogs.ToListAsync();
foreach (var log in pendingLogs)
{
    var response = await _http.PostAsJsonAsync("api/timelogs", log);
    if (response.IsSuccessStatusCode)
    {
        context.PendingTimeLogs.Remove(log);
    }
}
await context.SaveChangesAsync();

This way, employees didn’t complain about “the system not working when internet is down” and managers got their reports synced later.

Querying and Business Logic

Of course, you’ll run into situations where you need more than just insert and sync.

For example, managers asked me:

“Can we see total hours worked per employee for this week?”

At first, I wrote a basic LINQ query:

C#
var logs = await _context.TimeLogs
    .Where(t => t.EmployeeId == employeeId 
             && t.TimeIn >= weekStart 
             && t.TimeIn <= weekEnd)
    .ToListAsync();

var totalHours = logs.Sum(t => 
    (t.TimeOut ?? DateTime.UtcNow) - t.TimeIn).TotalHours;

It worked fine, but then came the business rules:

  • Ignore breaks longer than 2 hours.
  • Cap total daily hours at 12.

That’s when my queries started looking more complex. I learned it’s better to push these rules into reusable methods instead of mixing everything in LINQ.

C#
public double CalculateHours(TimeLog log)
{
    var duration = (log.TimeOut ?? DateTime.UtcNow) - log.TimeIn;
    if (duration.TotalHours > 12) duration = TimeSpan.FromHours(12);
    return duration.TotalHours;
}

Then I could apply it:

C#
var total = logs.Sum(CalculateHours);

Lessons I Learned

  1. Don’t rely only on server data. If your app needs to handle offline or semi-offline users, use EF Core with SQLite in Blazor WASM.
  2. Keep business logic separate. Queries should fetch data, methods should enforce rules.
  3. Sync is king. Designing a reliable sync between local and server is more important than writing the “perfect query”.
  4. Start simple. Don’t over-engineer at the beginning. Add rules step by step as business needs grow.

Sources

Here are some references that helped me a lot when figuring things out:

That’s my experience of mixing EF Core with Blazor WebAssembly for handling local and server data. It’s not always pretty, but it’s powerful once you get the hang of it.

Assi Arai
Assi Arai
Articles: 41