Stockage Local dans .NET MAUI 10 : SQLite, Preferences et SecureStorage (Guide 2026)
Choisir entre SQLite, Preferences et SecureStorage dans .NET MAUI 10 selon la nature des données. Repository pattern, migrations, chiffrement SQLCipher, pièges de production et bonnes pratiques 2026 avec exemples de code testés.
Le stockage local dans .NET MAUI 10 repose sur trois API complémentaires : SQLite (via sqlite-net-pcl) pour les données relationnelles structurées, Preferences pour les paramètres utilisateur simples de type clé-valeur, et SecureStorage pour les données sensibles chiffrées (tokens, mots de passe) qui exploitent le Keychain iOS et l'Android Keystore. Choisir la bonne API selon la nature de la donnée, c'est la première décision d'architecture d'une application MAUI production-ready.
Utilisez SQLite pour toute donnée relationnelle de plus de quelques dizaines d'entrées, avec la bibliothèque sqlite-net-pcl en mode async (SQLiteAsyncConnection) pour ne jamais bloquer le thread UI.
Preferences stocke des primitives (bool, int, string, double, DateTime) dans NSUserDefaults sur iOS et SharedPreferences sur Android. Parfait pour thèmes, flags et derniers ID consultés.
SecureStorage chiffre les valeurs via le Keychain (iOS) et l'AndroidKeyStore, mais reste limité à ~4 Ko par entrée et ne fonctionne pas dans les émulateurs Android sans Google Play Services.
Le dossier FileSystem.AppDataDirectory est le seul emplacement portable et sauvegardé pour les fichiers de base de données SQLite en 2026.
Les migrations schéma se gèrent avec CreateTableAsync qui applique automatiquement les ALTER TABLE ADD COLUMN, et SQLCipher via sqlite-net-sqlcipher ajoute le chiffrement AES-256 au repos.
Le pattern Repository + injection de dépendances via MauiAppBuilder évite les fuites de connexions et rend la logique testable sans dépendance à la plateforme.
Quelle API de stockage choisir dans .NET MAUI ?
Honnêtement, chaque projet MAUI que j'ai livré ces trois dernières années a commencé par la même erreur. Quelqu'un stocke un token OAuth dans Preferences, ou pire, sérialise 5 000 objets JSON dans un fichier plat parce que « SQLite semblait compliqué ». Le choix de l'API dépend de trois axes : la structure (clé-valeur vs relationnel), la sensibilité (public vs secret), et le volume (kilo-octets vs mégaoctets).
Critère
SQLite (sqlite-net-pcl)
Preferences
SecureStorage
Type de données
Relationnel, complexe
Primitives clé-valeur
Chaînes chiffrées
Volume recommandé
Illimité (Go)
< 100 Ko total
< 4 Ko par entrée
Chiffrement natif
Non (SQLCipher requis)
Non
Oui (Keychain/Keystore)
Async API
Oui (SQLiteAsyncConnection)
Non (synchrone)
Oui
Sauvegarde iCloud/Google
Oui (AppDataDirectory)
Oui
Optionnel (iOS)
Cas d'usage type
Entités métier, listes, cache
Thème, langue, flags
Tokens JWT, mots de passe
Dans l'application de gestion locative sur laquelle je travaille, la règle interne tient en une phrase. Si la donnée survit à une désinstallation elle va dans le backend, si elle a besoin d'être requêtée elle va dans SQLite, si elle tient dans un string et n'est pas secrète elle va dans Preferences, et si sa fuite déclenche un incident de sécurité elle va dans SecureStorage. Cette hiérarchie évite 90 % des débats d'architecture.
SQLite avec sqlite-net-pcl : installation et configuration
La bibliothèque sqlite-net-pcl maintenue par Frank Krueger reste en 2026 l'ORM SQLite de facto pour .NET MAUI. Contrairement à Entity Framework Core (qui fonctionne mais alourdit le démarrage de 300 à 500 ms sur iOS), sqlite-net-pcl compile à ~200 Ko et démarre en moins de 20 ms. Installez le package avec sa dépendance native :
Le bundle bundle_green embarque la version SQLite maintenue par Microsoft, thread-safe et compatible ARM64 Apple Silicon. Évitez bundle_e_sqlite3 sur iOS 18 et supérieur. Il déclenche un rejet App Store depuis mai 2026 à cause d'une utilisation d'API privée déclarée dans le manifeste de confidentialité (j'ai vu deux clients se faire refuser une build là-dessus la même semaine).
Configurez ensuite l'enregistrement du service dans MauiProgram.cs. La connexion doit être un singleton, jamais recréée par requête :
public static MauiApp CreateMauiApp()
{
var builder = MauiApp.CreateBuilder();
builder.UseMauiApp<App>();
// Chemin portable, sauvegardé par iCloud et Google Backup
var dbPath = Path.Combine(FileSystem.AppDataDirectory, "properties.db3");
builder.Services.AddSingleton(sp =>
new SQLiteAsyncConnection(
dbPath,
SQLiteOpenFlags.ReadWrite | SQLiteOpenFlags.Create | SQLiteOpenFlags.SharedCache));
builder.Services.AddSingleton<IPropertyRepository, PropertyRepository>();
return builder.Build();
}
Modèles, repository et injection de dépendances
Un modèle sqlite-net-pcl est une classe POCO annotée. J'insiste toujours pour que l'équipe utilise [PrimaryKey, AutoIncrement] plutôt que des GUID côté client. Les index B-tree SQLite sur un INTEGER sont trois fois plus rapides qu'un TEXT de 36 caractères, et cela devient mesurable au-delà de 10 000 lignes.
using SQLite;
public class Property
{
[PrimaryKey, AutoIncrement]
public int Id { get; set; }
[Indexed, MaxLength(120)]
public string Address { get; set; } = string.Empty;
[Indexed]
public string City { get; set; } = string.Empty;
public decimal MonthlyRent { get; set; }
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
// Colonne ajoutee en v2, sqlite-net l'ajoutera via ALTER TABLE
public bool IsArchived { get; set; }
}
Le repository encapsule la connexion et garantit que CreateTableAsync ne tourne qu'une fois par cycle de vie de l'application. C'est le pattern que je recommande à toute équipe qui structure son code selon les principes MVVM. Jetez un œil à notre guide MVVM avec CommunityToolkit.Mvvm pour l'injecter proprement dans vos ViewModels.
Le piège classique que j'attrape en revue de code : quelqu'un appelle _db.Table<T>().ToList() depuis un command MVVM synchrone. SQLite ouvre alors une transaction sur le thread UI et gèle l'application pendant 200 à 800 ms sur des devices bas de gamme comme un Pixel 6a. Utilisez systématiquement les variantes ...Async et propagez le await jusqu'au binding.
Pour les insertions massives (import CSV, synchronisation initiale), utilisez RunInTransactionAsync. Le gain est réel. Sur un iPhone 15 Pro, insérer 10 000 lignes prend 12 secondes en InsertAsync individuel contre 380 ms dans une transaction unique, un facteur 30.
Preferences : sauvegarder les paramètres utilisateur simples
L'API Microsoft.Maui.Storage.Preferences est votre alliée pour tout ce qui est petit, non sensible et fréquemment lu. Elle s'appuie sur NSUserDefaults (iOS), SharedPreferences (Android) et ApplicationData.Current.LocalSettings (Windows). L'écriture est synchrone et rapide (inutile d'awaiter).
// Ecriture
Preferences.Default.Set("app_theme", "dark");
Preferences.Default.Set("last_property_id", 42);
Preferences.Default.Set("last_sync_utc", DateTime.UtcNow);
// Lecture avec valeur par defaut
var theme = Preferences.Default.Get("app_theme", "light");
var lastId = Preferences.Default.Get("last_property_id", 0);
var lastSync = Preferences.Default.Get("last_sync_utc", DateTime.MinValue);
// Suppression et verification
if (Preferences.Default.ContainsKey("temp_flag"))
Preferences.Default.Remove("temp_flag");
Les types supportés sont : bool, int, long, float, double, string, DateTime. Pour tout ce qui est plus complexe (une liste, un objet), sérialisez en JSON avec System.Text.Json. Mais si vous en arrivez là, c'est probablement que la donnée devrait vivre dans SQLite.
Attention à la performance : Preferences.Get touche le disque à chaque appel sur Android. Si vous lisez la même clé 50 fois par frame de rendu (typiquement dans un binding), mettez la valeur en cache dans un champ statique et invalidez-le sur Set.
SecureStorage : chiffrer tokens et données sensibles
Tout ce qui ferait la une si ça fuitait (token JWT, refresh token, PIN, clé d'API) va dans SecureStorage. L'API délègue au Keychain iOS et à l'AndroidKeyStore (via EncryptedSharedPreferences en interne). Les valeurs sont chiffrées AES-256 avec une clé matérielle sur les devices modernes (Secure Enclave iOS, StrongBox Android 9+).
public sealed class TokenStore
{
private const string AccessKey = "auth_access_token";
private const string RefreshKey = "auth_refresh_token";
public async Task SaveTokensAsync(string access, string refresh)
{
await SecureStorage.Default.SetAsync(AccessKey, access);
await SecureStorage.Default.SetAsync(RefreshKey, refresh);
}
public async Task<(string? access, string? refresh)> ReadTokensAsync()
{
try
{
var access = await SecureStorage.Default.GetAsync(AccessKey);
var refresh = await SecureStorage.Default.GetAsync(RefreshKey);
return (access, refresh);
}
catch (Exception ex) // Keychain corrompu, biometrie revoquee, etc.
{
SecureStorage.Default.RemoveAll();
return (null, null);
}
}
public void SignOut() => SecureStorage.Default.RemoveAll();
}
Une valeur ne doit pas dépasser ~4 Ko sur iOS et 2 Ko sur Android. Au-delà, vous obtenez des erreurs opaques. Pour stocker un objet certificat ou une clé privée volumineuse, chiffrez d'abord en AES avec une clé stockée dans SecureStorage, puis écrivez le blob dans FileSystem.AppDataDirectory.
Chiffrer une base SQLite entière avec SQLCipher
Si votre application traite des données médicales, financières ou soumises au RGPD, chiffrez la base entière avec SQLCipher. Il remplace sqlite-net-pcl par sqlite-net-sqlcipher et ajoute une passphrase à la connexion :
// Recuperer la passphrase depuis SecureStorage
var passphrase = await SecureStorage.Default.GetAsync("db_key")
?? GenerateAndStoreKey();
var options = new SQLiteConnectionString(
dbPath,
SQLiteOpenFlags.ReadWrite | SQLiteOpenFlags.Create,
storeDateTimeAsTicks: true,
key: passphrase);
var db = new SQLiteAsyncConnection(options);
Le coût CPU est de 5 à 15 % sur les lectures et 10 à 25 % sur les écritures selon la taille des pages. Mesurez sur device physique avant de généraliser. Cette approche complète les bonnes pratiques que j'ai décrites dans notre guide performances .NET MAUI 10, notamment pour anticiper l'impact sur le démarrage à froid.
Gérer les migrations de schéma en production
Bonne nouvelle : sqlite-net-pcl gère automatiquement l'ajout de colonnes via CreateTableAsync. Si vous ajoutez une propriété bool IsArchived à votre modèle Property, la bibliothèque exécutera ALTER TABLE Property ADD COLUMN IsArchived INTEGER au prochain démarrage. Ce qu'elle ne fait pas : renommer une colonne, changer un type, supprimer une colonne, gérer une migration de données.
Pour ces cas, versionnez explicitement votre schéma :
private async Task RunMigrationsAsync()
{
await _db.CreateTableAsync<Property>();
await _db.CreateTableAsync<SchemaVersion>();
var current = await _db.Table<SchemaVersion>()
.OrderByDescending(v => v.Version)
.FirstOrDefaultAsync();
var version = current?.Version ?? 0;
if (version < 1)
{
await _db.ExecuteAsync(
"UPDATE Property SET City = TRIM(City) WHERE City LIKE ' %'");
await _db.InsertAsync(new SchemaVersion { Version = 1, AppliedAt = DateTime.UtcNow });
}
if (version < 2)
{
await _db.ExecuteAsync(
"CREATE INDEX IF NOT EXISTS idx_property_city_rent ON Property(City, MonthlyRent)");
await _db.InsertAsync(new SchemaVersion { Version = 2, AppliedAt = DateTime.UtcNow });
}
}
J'exécute toujours les migrations dans une transaction unique et je log la version cible dans Sentry ou App Center avant le premier await. Un utilisateur qui plante en migration, c'est un ticket support garanti. Vous voulez savoir immédiatement quelle version pose problème.
Où sont stockées les données SQLite dans MAUI ?
Le chemin correct en 2026 est FileSystem.AppDataDirectory. Il pointe vers :
iOS : Library/ dans le sandbox de l'application (sauvegardé par iCloud sauf si vous désactivez le drapeau NSURLIsExcludedFromBackupKey).
Android : /data/data/com.votresociete.app/files/ (privé, non lisible par les autres apps, sauvegardé par Google Backup).
Windows : %LOCALAPPDATA%\Packages\...\LocalState\.
N'utilisez jamaisFileSystem.CacheDirectory pour SQLite. Le système peut vider ce dossier sous pression mémoire et vous perdrez la base sans avertissement. Pour un fichier de cache éphémère (miniatures d'image), CacheDirectory reste pertinent, mais pas pour votre base.
Pièges de production et bonnes pratiques 2026
Après six ans à shipper du mobile en production (Kotlin puis MAUI), voici les erreurs récurrentes que je vois passer en revue :
Ne jamais utiliser SQLiteConnection synchrone, même dans un service en arrière-plan. Le thread pool Android est brutalement recyclé à chaque changement d'état d'app.
Toujours enrouler SQLiteException dans une couche métier. Les codes d'erreur (5 = BUSY, 11 = CORRUPT, 14 = CANTOPEN) doivent déclencher une remédiation différente.
Tester avec un fichier réel dans vos tests unitaires. Un mock ne détecte pas une contrainte UNIQUE violée. Notre guide des tests unitaires .NET MAUI détaille cette approche avec des bases SQLite in-memory.
Vérifier la taille de la base au démarrage. Au-delà de 100 Mo, la synchronisation iCloud sur iOS bascule en mode différé et peut échouer silencieusement. Purgez les anciennes lignes.
Éviter InsertAll hors transaction. Le rapport de perf est de 30x à 100x selon le device.
Chiffrer la passphrase SQLCipher elle-même via SecureStorage, pas dans une chaîne codée en dur : un décompilateur trouve les constantes en 30 secondes.
Questions fréquentes
Comment utiliser SQLite dans .NET MAUI 10 ?
Installez les packages sqlite-net-pcl et SQLitePCLRaw.bundle_green, créez un modèle POCO annoté [Table] et [PrimaryKey], puis enregistrez une SQLiteAsyncConnection en singleton dans MauiProgram.cs pointant vers FileSystem.AppDataDirectory. Utilisez systématiquement les méthodes ...Async pour ne pas bloquer le thread UI.
Quelle est la différence entre Preferences et SecureStorage ?
Preferences stocke des primitives clé-valeur en clair dans NSUserDefaults ou SharedPreferences, idéal pour un thème ou une langue. SecureStorage chiffre les valeurs via le Keychain iOS ou l'Android Keystore et sert exclusivement aux secrets (tokens, mots de passe). Ne stockez jamais de token OAuth dans Preferences.
Comment chiffrer une base SQLite dans .NET MAUI ?
Remplacez SQLitePCLRaw.bundle_green par le package sqlite-net-sqlcipher, puis passez une passphrase (stockée dans SecureStorage) au constructeur de SQLiteConnectionString avec le paramètre key. SQLCipher chiffre l'intégralité du fichier en AES-256, avec un surcoût CPU de 5 à 25 % selon les opérations.
Comment gérer les migrations SQLite dans .NET MAUI ?
Pour les ajouts de colonnes, CreateTableAsync<T>() les applique automatiquement via ALTER TABLE. Pour toute autre migration (renommage, changement de type, transformation de données), maintenez une table SchemaVersion et exécutez des scripts SQL versionnés dans une transaction unique au démarrage de l'application.
Preferences est-il thread-safe dans .NET MAUI ?
Oui, Preferences.Default est thread-safe côté API : les appels concurrents ne corrompent pas le fichier. En revanche, l'ordre d'écriture n'est pas garanti entre threads. Si deux threads écrivent la même clé simultanément, la dernière écriture gagne sans notification. Utilisez SQLite avec une transaction si l'ordre importe.
Marcus came into .NET via a winding path: six years writing Android in Kotlin at a London ad-tech firm, two years on a React Native team at a payments company, then a switch to .NET MAUI in 2022 when his current employer (a property-management SaaS in Manchester) consolidated their mobile stack onto C#. He now leads a team of five mobile engineers and owns the MAUI app end to end.
He tends to write the comparison pieces other people avoid: MAUI versus Flutter on real hardware, Shell navigation versus a hand-rolled stack, CommunityToolkit.Mvvm versus ReactiveUI for new projects, and what actually breaks when you move from MAUI 8 to 9. He has the .NET MAUI MVP designation as of 2025 and contributes intermittently to the CommunityToolkit.Maui repo.
Guide pratique pour créer, enregistrer et migrer des handlers personnalisés dans .NET MAUI 10, avec code d'exemple pour iOS et Android et pièges à éviter.
Tutoriel complet pour ajouter les notifications push dans .NET MAUI 10 : configuration FCM Android, APNs iOS avec clé .p8, deep linking Shell et gestion des états premier plan, arrière-plan et arrêté. Avec code C# prêt à copier.
Guide complet de la navigation Shell dans .NET MAUI 10. Routes URI, deep links iOS/Android, injection de dépendances et passage de paramètres typés, avec exemples testés en production et pièges courants à éviter.