Do you really know where your .NET MAUI app’s data goes? Whether you’re syncing to the cloud, caching locally, or persisting user preferences, managing data is one of the most underestimated challenges in mobile app development.
Let me walk you through the practical strategies I’ve used in real-world .NET MAUI projects to handle both local and remote data like a pro. We’ll cover SQLite with EF Core, secure storage, API consumption, and even offline-first strategies. No fluff. Just code and clarity.
SQLite and Entity Framework Core
Setting Up a Local Database
To use SQLite in .NET MAUI, you typically combine it with Entity Framework Core:
public class AppDbContext : DbContext
{
public DbSet<TaskItem> Tasks { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
string dbPath = Path.Combine(FileSystem.AppDataDirectory, "appdata.db");
optionsBuilder.UseSqlite($"Filename={dbPath}");
}
}
public class TaskItem
{
public int Id { get; set; }
public string Title { get; set; }
public bool IsDone { get; set; }
}
This sets up a local SQLite database stored in the app’s data folder. TaskItem
is a simple model with an auto-incrementing primary key.
SQLite is perfect for structured data that must persist across sessions and works seamlessly across Android, iOS, and Windows. EF Core provides a higher-level abstraction, letting you write LINQ queries and use migrations.
CRUD Operations with SQLite
Example of inserting a new task:
using var db = new AppDbContext();
db.Database.EnsureCreated();
db.Tasks.Add(new TaskItem { Title = "Buy milk", IsDone = false });
db.SaveChanges();
To read tasks:
var items = db.Tasks.ToList();
To update:
var task = db.Tasks.First();
task.IsDone = true;
db.SaveChanges();
To delete:
db.Tasks.Remove(task);
db.SaveChanges();
EF Core also supports more advanced queries, indexing, constraints, and relationship mappings—ideal when your data grows in complexity. Combine this with dependency injection to provide the DbContext
throughout your app in a scalable manner.
Preferences and Secure Storage
Storing User Preferences
Preferences.Set("theme", "dark");
string theme = Preferences.Get("theme", "light");
This stores lightweight values (like theme preferences, toggle states, or simple user settings) that persist across app launches. Preferences are ideal for non-sensitive, frequently accessed data. They are stored in plain text format in the app’s sandboxed storage and offer a straightforward API to get and set values of types like string, int, bool, float, and DateTime.
You can also check for key existence:
if (Preferences.ContainsKey("theme"))
{
Preferences.Remove("theme");
}
It’s great for things like onboarding state (isFirstRun
), locale settings, or UI configurations that should survive app restarts.
Preferences also support composite keys using nameof
or constants to avoid typos and ensure consistency across your app. If you’re managing complex settings, wrap your preference accessors inside a strongly typed settings service for clean and testable code.
Encrypting and Securing Local Data
For credentials, tokens, and any sensitive information, use SecureStorage
:
await SecureStorage.SetAsync("auth_token", token);
string token = await SecureStorage.GetAsync("auth_token");
SecureStorage abstracts away the complexity of dealing with platform-specific encryption mechanisms. On iOS, it stores values in the Keychain. On Android, it uses the Keystore system. This makes it the safest way to handle tokens, user credentials, or encrypted user configuration data.
SecureStorage is asynchronous and may throw exceptions if device security settings (like passcode or biometric auth) are not configured. Always wrap it with exception handling:
try
{
await SecureStorage.SetAsync("auth_token", token);
}
catch (Exception ex)
{
// Handle possible exceptions like user disabling secure storage
Debug.WriteLine("Secure storage unavailable: " + ex.Message);
}
Avoid using Preferences for any data that could compromise the user’s security or privacy if exposed. Stick to SecureStorage for secrets.
You can also design a secure abstraction layer combining SecureStorage with biometric authentication prompts to ensure that sensitive operations are protected by the OS.
Fetching and Consuming REST APIs
Fetching data from the web is central to any connected app. In .NET MAUI, you can easily do this using HttpClient
, which supports all HTTP verbs and handles cookies, headers, and timeout policies.
Using HttpClient to Make API Requests
HttpClient client = new HttpClient();
var response = await client.GetAsync("https://api.example.com/tasks");
response.EnsureSuccessStatusCode();
string json = await response.Content.ReadAsStringAsync();
You can customize headers for authentication:
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
Also consider implementing HttpClientFactory
or reusing HttpClient
instances to avoid socket exhaustion.
JSON Serialization and Deserialization
Use System.Text.Json
for lightweight and fast JSON parsing:
var tasks = JsonSerializer.Deserialize<List<TaskItem>>(json);
You can also customize serialization behavior using JsonSerializerOptions
:
var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true };
var tasks = JsonSerializer.Deserialize<List<TaskItem>>(json, options);
Always handle nulls and errors gracefully:
if (tasks != null)
{
foreach (var item in tasks)
Console.WriteLine(item.Title);
}
Consider creating DTOs (Data Transfer Objects) separate from your domain models for better separation of concerns, especially if your API models differ from your app’s internal logic.
Data Synchronization and Offline Mode
Mobile apps often encounter unreliable network conditions. A robust app must gracefully switch between online and offline modes without frustrating the user. Here’s how to do it right in .NET MAUI.
Implementing Caching
Use local files to temporarily store API data. This allows the app to read recent data even without internet access:
string cachePath = Path.Combine(FileSystem.CacheDirectory, "task_cache.json");
File.WriteAllText(cachePath, json); // On API response
string cachedJson = File.ReadAllText(cachePath); // On offline mode
This approach is simple and effective for storing lists or frequently accessed JSON responses. You can deserialize this cached JSON as a fallback when the app is offline.
Offline-First Approach
Instead of showing a loading spinner while waiting for the API, you can use the offline-first strategy: display cached or local database data immediately, then refresh in the background.
var localTasks = db.Tasks.ToList(); // Show immediately
try
{
var response = await client.GetAsync("https://api.example.com/tasks");
var remoteTasks = JsonSerializer.Deserialize<List<TaskItem>>(await response.Content.ReadAsStringAsync());
if (remoteTasks != null)
{
db.Tasks.RemoveRange(db.Tasks);
db.Tasks.AddRange(remoteTasks);
db.SaveChanges();
}
}
catch
{
// Handle offline gracefully
Console.WriteLine("You're offline, showing cached data.");
}
This model prioritizes user experience and avoids interruptions. Even if the network is unavailable, users can still interact with the data they saw previously. When the network returns, the cache or database is refreshed silently, ensuring minimal friction.
Advanced approaches may involve syncing diffs, handling conflicts, and adding a queue for offline actions (like adding new tasks while offline and syncing later).
FAQ: Common Data Handling Questions in .NET MAUI
Yes, but migrations must be created in a separate .NET Standard or .NET 6 project, then used in MAUI.
It uses the OS-level secure key stores, which is as safe as native platform allows.
Absolutely. Choose what fits your app’s complexity and sync requirements.
Conclusion: Build Smarter, Sync Safer
When you’re building a cross-platform app, your data strategy determines user trust and app stability. Don’t just default to one storage method—combine them for the right use case. Use SQLite for structure, preferences for speed, APIs for freshness, and caching for resilience.
Want your MAUI apps to stay lean and responsive even offline? Start implementing these patterns today and let me know how it goes in the comments!