Hvor gemmer man data?

Brugerindstillinger og SQLite-filer — desktop (Windows, macOS, Linux)

Denne vejledning dækker desktop — Windows, macOS og Linux. Mobil (Android og iOS) er særligt: der har apps deres egen aflukkede sandbox-mappe, og man kan ikke selv vælge placering på samme måde. Det behandles i MAUI, Avalonia eller Uno-platform-materialet, ikke her.

Princippet: YourName\AppName

Et program skal næsten altid gemme noget: brugerens indstillinger eller rigtige data som f.eks. en SQLite-database. Konventionen er at oprette sin egen undermappe i brugerens profilmappe — bygget op som YourName\AppName:

C:\Users\mje\AppData\Roaming\YourName\MyApp\            (Windows, indstillinger)
C:\Users\mje\AppData\Local\YourName\MyApp\              (Windows, data)
C:\Users\mje.HEG\Documents\YourName\MyApp\              (Windows, synlige filer)
/home/mje/.config/YourName/MyApp/                       (Linux)
/Users/mje/Library/Application Support/YourName/MyApp/  (macOS)
Ingen specialtegn og mellemrum! Hold dig til almindelige bogstaver og tal (A–Z, a–z, 0–9) i både YourName og AppName. Undgå mellemrum, punktummer, bindestreger, æ/ø/å og andre specialtegn — mappenavne med specialtegn giver problemer på tværs af platforme, kodninger og scripts. MiJenner\Quizzer er fint — M. Jenner\Min Quiz-App æøå er ikke.
Gem ikke data ved siden af programmet. Ligger programmet i C:\Program Files\... har en almindelig bruger ikke skriveadgang — programmet fejler eller kræver administratorrettigheder. Data, der ændrer sig mens programmet kører, hører til i brugerens profilmappe (eller Dokumenter, hvis brugeren selv skal kunne finde filen).

Hvor finder man mappen på de tre platforme?

Man skriver aldrig stierne ovenfor direkte ind i koden — .NET finder dem for dig, uanset hvad brugeren hedder, og uanset platform:

using System;

// Roaming: indstillinger der gerne må følge brugeren mellem maskiner
string roaming = Environment.GetFolderPath(
    Environment.SpecialFolder.ApplicationData);

// Local: data der hører til denne maskine (f.eks. en SQLite-database)
string local = Environment.GetFolderPath(
    Environment.SpecialFolder.LocalApplicationData);

// Dokumenter: filer brugeren selv skal kunne finde og åbne
string docs = Environment.GetFolderPath(
    Environment.SpecialFolder.MyDocuments);
SpecialFolder Windows macOS
Linux
ApplicationData %APPDATA%
(AppData\Roaming)
~/Library/Application Support
~/.config
LocalApplicationData %LOCALAPPDATA%
(AppData\Local)
~/Library/Application Support
~/.local/share
MyDocuments Dokumenter ~/Documents

En lille hjælpeklasse: AppFolders

For at komme hurtigt i gang med selve applikationens funktionalitet, kan man kopiere denne statiske klasse direkte ind i sit projekt. Den beslutter hvor filerne ligger — resten af programmet skal ikke tænke over det:

using System;
using System.IO;

namespace YourNamespace; 

public static class AppFolders
{
    // ── Tilpas disse to til dig og din app ──────────────────
    //    Kun bogstaver og tal — INGEN specialtegn/mellemrum!
    private const string YourName = "MiJenner";
    private const string AppName = "MyApp";

    // ── Vælg strategi her: AppData (skjult) eller Documents (synlig) ──
    public static string SettingsFolder =>
        GetFolder(Environment.SpecialFolder.ApplicationData);
        // GetFolder(Environment.SpecialFolder.LocalApplicationData);
        // GetFolder(Environment.SpecialFolder.MyDocuments);

    public static string DataFolder =>
        GetFolder(Environment.SpecialFolder.ApplicationData);
        // GetFolder(Environment.SpecialFolder.LocalApplicationData);
        // GetFolder(Environment.SpecialFolder.MyDocuments);

    // ── Praktiske genveje ───────────────────────────────────
    public static string SettingsFile =>
        Path.Combine(SettingsFolder, "usersettings.json");

    public static string DatabaseFile =>
        Path.Combine(DataFolder, "database.sqlite");

    // ── Selve maskineriet ───────────────────────────────────
    private static string GetFolder(Environment.SpecialFolder baseFolder)
    {
        string path = Path.Combine(
            Environment.GetFolderPath(baseFolder),
            YourName, AppName);
        Directory.CreateDirectory(path);   // opretter, hvis den ikke findes
        return path;
    }
}
Sådan får du klassen ind i dit projekt:
  1. Højreklik på projektnavnet i Solution Explorer og vælg Add > Class…
  2. Giv den navnet AppFolders.cs
  3. Indsæt koden ovenfor — og ret namespace YourNamespace til dit eget projekts namespace (samme som i Program.cs)
  4. Tilpas YourName og AppName øverst i klassen (måske også database- og settings-filnavnene).

Ændringer sker ét sted: skal indstillingerne i stedet ligge i Dokumenter, kommenteres én linje ud, og en anden ind — og hele programmet flytter med. Resten af koden bruger blot AppFolders.SettingsFile og AppFolders.DatabaseFile og kender slet ikke stien.

Bemærk: Path.Combine sætter selv det rigtige skilletegn ind (\ på Windows, / på macOS/Linux). Saml derfor altid stier med Path.Combine — aldrig med strenge som "YourName\MyApp", der kun virker på Windows.
AppData er skjult — sådan finder du mappen alligevel: Windows' AppData-mappe er som udgangspunkt skjult i Stifinder, og ~/.config er usynlig på Linux. Tre måder at komme derhen:
Gentagelse — fordi det ikke kan nævnes for tit: YourName og AppName må kun indeholde almindelige bogstaver og tal. Ingen specialtegn og mellemrum — heller ikke æ, ø og å.

Brugerindstillinger som JSON

Med klassen på plads er det selve gemme/hente-logikken, man skal koncentrere sig om — JSON-serialisering er indbygget i .NET:

using System.IO;
using System.Text.Json;

namespace YourNamespace;

public class UserSettings
{
    public string Theme { get; set; } = "light";
    public string Language { get; set; } = "da";
}
Sådan tilføjer du klassen i dit projekt:
  1. Tilføj UserSettings-klassen i egen fil med navnet UserSettings.cs, og giv den ovenstående indhold. Ret namespace YourNamespace til det namespace, du bruger i dit projekt.
  2. Der kræves ingen NuGet-pakker — System.Text.Json er indbygget i .NET

Gemme/hente-koden skrives i Program.cs i Main-metoden, hvor det passer ind i programmets flow — f.eks. hent ved opstart og gem når brugeren ændrer en indstilling:

using System;
using System.IO;
using System.Text.Json;

namespace YourNamespace;

internal class Program
{
    static void Main(string[] args)
    {
        // --- gem ---
        var settings = new UserSettings { Theme = "dark" };
        string json = JsonSerializer.Serialize(settings,
            new JsonSerializerOptions { WriteIndented = true });
        File.WriteAllText(AppFolders.SettingsFile, json);

        // --- hent ---
        UserSettings settings2 = File.Exists(AppFolders.SettingsFile)
            ? JsonSerializer.Deserialize<UserSettings>(
                File.ReadAllText(AppFolders.SettingsFile)) ?? new UserSettings()
            : new UserSettings();
    }
}

Første gang programmet køres, findes filen ikke — så bruges klassens standardværdier, og filen oprettes, når brugeren ændrer en indstilling.

SQLite-databasens placering

En SQLite-database er én enkelt fil — og med hjælpeklassen er placeringen allerede afgjort. Selve connection-strengen bygges direkte på AppFolders.DatabaseFile:

string connectionString =
    $"Data Source={AppFolders.DatabaseFile}";

Connection-strengen bruges med Microsoft.Data.Sqlite eller oven på Dapper — se Dapper cheat sheet. SQLite skaber selv filen ved første forbindelse, og mappen er allerede oprettet af AppFolders.

Sådan bruger du koden:
  1. Tilføj NuGet-pakken Microsoft.Data.Sqlite (højreklik på projektet → Manage NuGet Packages…) — eller Dapper + Microsoft.Data.Sqlite, hvis du bruger Dapper
  2. Skriv connection-streng-linjen i Program.cs, før du åbner forbindelsen — resten er almindelig SQL/Dapper-kode
  3. Filen database.sqlite oprettes automatisk ved første forbindelse — du skal ikke lave den selv

Test-app: virker det på din computer?

Virksomheder og organisationer spænder ofte ben med gruppepolitikker, OneDrive-omdirigering af Dokumenter og skrivebeskyttede mapper. Denne lille konsol-app tester alle tre placeringer — AppData\Roaming, AppData\Local og Dokumenter — og rapporterer, hvad der faktisk virker. Testen har to trin for hver placering: først oprettes mappen, dernæst skrives en rigtig test-fil i mappen, som læses tilbage og sammenlignes. Det er to forskellige rettigheder (især på Windows), så begge trin skal sige OK. Opret projektet og kør det:

dotnet new console -n FolderTest
cd FolderTest
dotnet run

Erstat indholdet af Program.cs med dette (hjælpeklassen er inkluderet nederst):

using System;
using System.IO;

namespace FolderTest;

internal class Program
{
    static void Main(string[] args)
    {
        Console.WriteLine("Test af skrive/læse-adgang til app-mapper");
        Console.WriteLine(new string('=', 55));
        Console.WriteLine();

        TestFolder("AppData\\Roaming",
            Environment.SpecialFolder.ApplicationData);
        TestFolder("AppData\\Local",
            Environment.SpecialFolder.LocalApplicationData);
        TestFolder("Dokumenter",
            Environment.SpecialFolder.MyDocuments);
    }

    static void TestFolder(string label,
        Environment.SpecialFolder baseFolder)
    {
        // ── Trin 1: kan mappen oprettes? ────────────────────
        string folder;
        try
        {
            folder = AppFolders.GetTestFolder(baseFolder);
            Console.WriteLine($"{label}: mappe OK");
        }
        catch (Exception ex)
        {
            Console.WriteLine($"{label}: MAPPE-FEJL — {ex.Message}");
            Console.WriteLine();
            return;
        }

        // ── Trin 2: kan der skrives og læses en fil i mappen? ─
        string testFile = Path.Combine(folder, "write-test.tmp");
        string content = $"Test {DateTime.Now:HH:mm:ss}";

        try
        {
            File.WriteAllText(testFile, content);
            string readBack = File.ReadAllText(testFile);
            File.Delete(testFile);

            bool ok = readBack == content;
            Console.WriteLine(ok
                ? $"{label}: fil OK"
                : $"{label}: FIL-FEJL — indhold matcher ikke");
        }
        catch (UnauthorizedAccessException)
        {
            Console.WriteLine($"{label}: INGEN FIL-SKRIVEADGANG");
        }
        catch (Exception ex)
        {
            Console.WriteLine($"{label}: FIL-FEJL — {ex.Message}");
        }
        Console.WriteLine($"  {folder}");
        Console.WriteLine();
    }
}

// ── Hjælpeklassen — samme som ovenfor ────────────────────
public static class AppFolders
{
    // Kun bogstaver og tal — INGEN specialtegn/mellemrum!
    private const string YourName = "MiJenner";
    private const string AppName = "FolderTest";

    public static string SettingsFolder =>
        GetFolder(Environment.SpecialFolder.ApplicationData);
        // GetFolder(Environment.SpecialFolder.MyDocuments);

    public static string DataFolder =>
        GetFolder(Environment.SpecialFolder.LocalApplicationData);
        // GetFolder(Environment.SpecialFolder.MyDocuments);

    public static string SettingsFile =>
        Path.Combine(SettingsFolder, "usersettings.json");

    public static string DatabaseFile =>
        Path.Combine(DataFolder, "appdata.db");

    // Bruges af testen ovenfor — eksponerer alle tre placeringer
    public static string GetTestFolder(Environment.SpecialFolder baseFolder) =>
        GetFolder(baseFolder);

    private static string GetFolder(Environment.SpecialFolder baseFolder)
    {
        string path = Path.Combine(
            Environment.GetFolderPath(baseFolder),
            YourName, AppName);
        Directory.CreateDirectory(path);
        return path;
    }
}

Eksempel på output:

Test af skrive/læse-adgang til app-mapper
=======================================================

AppData\Roaming: mappe OK
AppData\Roaming: fil OK
  C:\Users\mje\AppData\Roaming\MiJenner\FolderTest

AppData\Local: mappe OK
AppData\Local: fil OK
  C:\Users\mje\AppData\Local\MiJenner\FolderTest

Dokumenter: mappe OK
Dokumenter: INGEN FIL-SKRIVEADGANG
  C:\Users\mje.HEG\Documents\MiJenner\FolderTest
Fortolkning: Både mappe og fil skal sige OK, før placeringen kan bruges. Det er to forskellige rettigheder — især på Windows kan en app godt få lov at oprette mappen, men blive blokeret, når den skriver en fil (f.eks. af Windows' ransomware-beskyttelse Controlled Folder Access, der typisk beskytter Dokumenter). Melder en placering INGEN FIL-SKRIVEADGANG eller FEJL, er den spærret — vælg en af de andre strategier i AppFolders. Fejler alle tre, er problemet typisk antivirus eller en særlig stram gruppepolitik. Testfilen write-test.tmp slettes igen efter hver test; mappen bliver liggende.

Huskeregler