ASP.NET Core Blazor Server s Entity Framework Core (EF Core)

Tento článek vysvětluje, jak používat Entity Framework Core (EF Core) v Blazor Server aplikacích.

Blazor Server je stavová architektura aplikace. Aplikace udržuje průběžné připojení k serveru a stav uživatele je uložený v paměti serveru v okruhu. Jedním z příkladů stavu uživatele jsou data uchovávaná v instancích služby vkládání závislostí (di) , které jsou vymezeny pro okruh. Jedinečný aplikační model, který poskytuje, Blazor Server vyžaduje zvláštní přístup k použití Entity Framework Core.

Poznámka

Tento článek se zabývá EF Core v Blazor Server aplikacích. Blazor WebAssembly aplikace běží v izolovaném prostoru (sandbox) websestavení, které brání většině přímých připojení k databázi. Spuštění EF Core v Blazor WebAssembly systému je nad rámec tohoto článku.

Ukázková aplikace

Ukázková aplikace byla sestavena jako referenční dokumentace pro Blazor Server aplikace, které používají EF Core. Ukázková aplikace obsahuje mřížku s operacemi řazení a filtrování, odstraňování, přidávání a aktualizace. Ukázka ukazuje použití EF Core ke zpracování optimistické souběžnosti.

Zobrazit nebo stáhnout ukázkový kód (Jak stáhnout)

Ukázka používá místní databázi SQLite , aby ji bylo možné použít na libovolné platformě. ukázka také nakonfiguruje protokolování databáze pro zobrazení SQLch dotazů, které jsou generovány. Nakonfiguruje se v appsettings.Development.json :

{
  "DetailedErrors": true,
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning",
      "Microsoft.Hosting.Lifetime": "Information",
      "Microsoft.EntityFrameworkCore.Database.Command": "Information"
    }
  }
}

Komponenty mřížka, přidání a zobrazení používají vzor "kontext pro operaci", ve kterém je vytvořen kontext pro každou operaci. Součást pro úpravy používá vzor "kontext pro komponentu", kde je vytvořen kontext pro každou součást.

Poznámka

Některé příklady kódu v tomto tématu vyžadují obory názvů a služby, které se nezobrazují. Pokud si chcete prohlédnout plně funkční kód, včetně požadavků @using a @inject direktiv pro Razor Příklady, přečtěte si ukázkovou aplikaci.

Přístup k databázi

EF Core spoléhá na a DbContext jako způsob Konfigurace přístupu k databázi a fungování jako pracovní jednotky. EF Core poskytuje AddDbContext rozšíření pro ASP.NET Core aplikace, které ve výchozím nastavení registrují kontext jako službu s vymezeným oborem . V Blazor Server aplikacích můžou být vymezené registrace služeb problematické, protože instance je sdílená napříč součástmi v rámci okruhu uživatele. DbContext není bezpečné pro přístup z více vláken a není navržené pro souběžné použití. Stávající životnosti jsou z těchto důvodů nevhodná:

  • Singleton sdílí stav napříč všemi uživateli aplikace a vede k nevhodnému souběžnému použití.
  • Obor (výchozí) představuje podobný problém mezi komponentami stejného uživatele.
  • Přechodný výsledek nové instance na žádost; avšak protože komponenty mohou být dlouhodobě dlouhodobé, výsledkem je delší kontext, než může být určena.

Následující doporučení jsou navržená tak, aby poskytovala konzistentní přístup k používání EF Core v Blazor Server aplikacích.

  • Ve výchozím nastavení zvažte použití jednoho kontextu na operaci. Kontext je navržen pro rychlou a nízkou režii vytváření instancí:

    using var context = new MyContext();
    
    return await context.MyEntities.ToListAsync();
    
  • Použití příznaku k zabránění více souběžných operací:

    if (Loading)
    {
        return;
    }
    
    try
    {
        Loading = true;
    
        ...
    }
    finally
    {
        Loading = false;
    }
    

    Umístěte operace za Loading = true; řádek v try bloku.

  • U delších operací, které využijí sledování změn nebo řízení souběžnostiEF Core, nastavte kontext na životní dobu součásti.

Nové instance DbContext

Nejrychlejší způsob, jak vytvořit novou DbContext instanci, je použít new k vytvoření nové instance. Existuje však několik scénářů, které mohou vyžadovat řešení dalších závislostí. Například lze chtít použít DbContextOptions ke konfiguraci kontextu.

Doporučeným řešením pro vytvoření nového DbContext se závislostmi je použití továrny. EF Core 5,0 nebo novější poskytuje integrovaný objekt pro vytváření nových kontextů.

Následující příklad konfiguruje SQLite a povoluje protokolování dat. Kód používá metodu rozšíření ( AddDbContextFactory ) ke konfiguraci objektu pro vytváření databáze pro di a zadání výchozích možností:

builder.Services.AddDbContextFactory<ContactContext>(opt =>
    opt.UseSqlite($"Data Source={nameof(ContactContext.ContactsDb)}.db"));

Továrna je vložena do komponent a slouží k vytváření nových instancí. Například v Pages/Index.razor :

private async Task DeleteContactAsync()
{
    using var context = DbFactory.CreateDbContext();
    Filters.Loading = true;

    if (Wrapper is not null && context.Contacts is not null)
    {
        var contact = await context.Contacts
            .FirstAsync(c => c.Id == Wrapper.DeleteRequestId);

        if (contact is not null)
        {
            context.Contacts?.Remove(contact);
            await context.SaveChangesAsync();
        }
    }

    Filters.Loading = false;

    await ReloadAsync();
}

Poznámka

Wrapper je odkaz na GridWrapper komponentu komponenty. IndexViz součást ( Pages/Index.razor ) v ukázkové aplikaci.

nové DbContext instance lze vytvořit pomocí továrny, která umožňuje konfigurovat připojovací řetězec na DbContext , například při použití modelu ASP.NET Core Identity. Další informace najdete v tématu víceklientská architektura (EF Core dokumentace).

Rozsah pro životní dobu součásti

Možná budete chtít vytvořit DbContext , který existuje pro celou dobu platnosti komponenty. To vám umožní použít ho jako pracovní jednotku a využít vestavěné funkce, jako je sledování změn a řešení souběžnosti. Pomocí továrny můžete vytvořit kontext a sledovat ho po dobu života součásti. Nejprve implementujte IDisposable a založit objekt pro vytváření, jak je znázorněno v Pages/EditContact.razor následujícím příkladu:

@implements IDisposable
@inject IDbContextFactory<ContactContext> DbFactory

Ukázková aplikace zajišťuje, že je kontext vyřazený, když je součást Vyřazená:

public void Dispose()
{
    Context?.Dispose();
}

Nakonec je přepsáno, OnInitializedAsync aby se vytvořil nový kontext. V ukázkové aplikaci OnInitializedAsync načte kontakt ve stejné metodě:

protected override async Task OnInitializedAsync()
{
    Busy = true;

    try
    {
        Context = DbFactory.CreateDbContext();

        if (Context is not null && Context.Contacts is not null)
        {
            var contact = await Context.Contacts.SingleOrDefaultAsync(c => c.Id == ContactId);

            if (contact is not null)
            {
                Contact = contact;
            }
        }
    }
    finally
    {
        Busy = false;
    }

    await base.OnInitializedAsync();
}

Povolit protokolování citlivých dat

EnableSensitiveDataLogging zahrnuje data aplikace ve zprávách výjimek a protokolování rozhraní. Protokolovaná data mohou zahrnovat hodnoty přiřazené vlastnostem instancí entit a hodnot parametrů pro příkazy odesílané do databáze. protokolování dat pomocí EnableSensitiveDataLogging je bezpečnostní riziko, protože může vystavovat hesla a další identifikovatelné osobní údaje (PII), když se zaprotokoluje SQL příkazy provedené proti databázi.

Doporučujeme povolit EnableSensitiveDataLogging pouze vývoj a testování:

#if DEBUG
    services.AddDbContextFactory<ContactContext>(opt =>
        opt.UseSqlite($"Data Source={nameof(ContactContext.ContactsDb)}.db")
        .EnableSensitiveDataLogging());
#else
    services.AddDbContextFactory<ContactContext>(opt =>
        opt.UseSqlite($"Data Source={nameof(ContactContext.ContactsDb)}.db"));
#endif

Další materiály

Blazor Server je stavová architektura aplikace. Aplikace udržuje průběžné připojení k serveru a stav uživatele je uložený v paměti serveru v okruhu. Jedním z příkladů stavu uživatele jsou data uchovávaná v instancích služby vkládání závislostí (di) , které jsou vymezeny pro okruh. Jedinečný aplikační model, který poskytuje, Blazor Server vyžaduje zvláštní přístup k použití Entity Framework Core.

Poznámka

Tento článek se zabývá EF Core v Blazor Server aplikacích. Blazor WebAssembly aplikace běží v izolovaném prostoru (sandbox) websestavení, které brání většině přímých připojení k databázi. Spuštění EF Core v Blazor WebAssembly systému je nad rámec tohoto článku.

Ukázková aplikace

Ukázková aplikace byla sestavena jako referenční dokumentace pro Blazor Server aplikace, které používají EF Core. Ukázková aplikace obsahuje mřížku s operacemi řazení a filtrování, odstraňování, přidávání a aktualizace. Ukázka ukazuje použití EF Core ke zpracování optimistické souběžnosti.

Zobrazit nebo stáhnout ukázkový kód (Jak stáhnout)

Ukázka používá místní databázi SQLite , aby ji bylo možné použít na libovolné platformě. ukázka také nakonfiguruje protokolování databáze pro zobrazení SQLch dotazů, které jsou generovány. Nakonfiguruje se v appsettings.Development.json :

{
  "DetailedErrors": true,
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information",
      "Microsoft.EntityFrameworkCore.Database.Command": "Information"
    }
  }
}

Komponenty mřížka, přidání a zobrazení používají vzor "kontext pro operaci", ve kterém je vytvořen kontext pro každou operaci. Součást pro úpravy používá vzor "kontext pro komponentu", kde je vytvořen kontext pro každou součást.

Poznámka

Některé příklady kódu v tomto tématu vyžadují obory názvů a služby, které se nezobrazují. Pokud si chcete prohlédnout plně funkční kód, včetně požadavků @using a @inject direktiv pro Razor Příklady, přečtěte si ukázkovou aplikaci.

Přístup k databázi

EF Core spoléhá na a DbContext jako způsob Konfigurace přístupu k databázi a fungování jako pracovní jednotky. EF Core poskytuje AddDbContext rozšíření pro ASP.NET Core aplikace, které ve výchozím nastavení registrují kontext jako službu s vymezeným oborem . V Blazor Server aplikacích můžou být vymezené registrace služeb problematické, protože instance je sdílená napříč součástmi v rámci okruhu uživatele. DbContext není bezpečné pro přístup z více vláken a není navržené pro souběžné použití. Stávající životnosti jsou z těchto důvodů nevhodná:

  • Singleton sdílí stav napříč všemi uživateli aplikace a vede k nevhodnému souběžnému použití.
  • Obor (výchozí) představuje podobný problém mezi komponentami stejného uživatele.
  • Přechodný výsledek nové instance na žádost; avšak protože komponenty mohou být dlouhodobě dlouhodobé, výsledkem je delší kontext, než může být určena.

Následující doporučení jsou navržená tak, aby poskytovala konzistentní přístup k používání EF Core v aplikacích Blazor Server .

  • Ve výchozím nastavení zvažte použití jednoho kontextu na operaci. Kontext je navržený pro rychlé vytváření instancí s nízkou režií:

    using var context = new MyContext();
    
    return await context.MyEntities.ToListAsync();
    
  • Pokud chcete zabránit více souběžných operacím, použijte příznak :

    if (Loading)
    {
        return;
    }
    
    try
    {
        Loading = true;
    
        ...
    }
    finally
    {
        Loading = false;
    }
    

    Umístěte operace za Loading = true; řádek v try bloku.

  • Pro dlouhodobější operace, které využívají EF Core sledování změn nebo řízení souběžnosti , vyjád te kontext na dobu života komponenty.

Nové instance DbContext

Nejrychlejší způsob, jak vytvořit novou DbContext instanci, je new pomocí příkazu vytvořit novou instanci. Existuje však několik scénářů, které mohou vyžadovat řešení dalších závislostí. Můžete například chtít použít ke konfiguraci DbContextOptions kontextu.

Doporučeným řešením pro vytvoření nového řešení DbContext se závislostmi je použití továrny. EF Core verze 5.0 nebo novější poskytuje integrovanou továrnu pro vytváření nových kontextů.

Následující příklad nakonfiguruje SQLite a povolí protokolování dat. Kód ke konfiguraci databázové továrny pro vlastníka a poskytnutí výchozích možností používá rozšiřující metodu (AddDbContextFactory):

services.AddDbContextFactory<ContactContext>(opt =>
    opt.UseSqlite($"Data Source={nameof(ContactContext.ContactsDb)}.db"));

Továrna se injektuje do komponent a používá se k vytváření nových instancí. Například v souboru Pages/Index.razor:

private async Task DeleteContactAsync()
{
    using var context = DbFactory.CreateDbContext();

    Filters.Loading = true;

    var contact = await context.Contacts.FirstAsync(
        c => c.Id == Wrapper.DeleteRequestId);

    if (contact != null)
    {
        context.Contacts.Remove(contact);
        await context.SaveChangesAsync();
    }

    Filters.Loading = false;

    await ReloadAsync();
}

Poznámka

Wrapperje odkaz na komponentuGridWrapper. Podívejte se Index na komponentu (Pages/Index.razor) v ukázkové aplikaci.

Nové DbContext instance je možné vytvořit pomocí DbContexttovárny, která umožňuje konfigurovat připojovací řetězec pro , například při použití ASP.NET Core modeluIdentity. Další informace najdete v tématu Vícevrstvá (EF Core dokumentace).

Rozsah životnosti komponenty

Můžete chtít vytvořit objekt , DbContext který existuje po celou dobu životnosti komponenty. To vám umožní ji použít jako jednotku práce a využívat integrované funkce, jako je sledování změn a řešení souběžnosti. Objekt pro vytváření můžete použít k vytvoření kontextu a jeho sledování po celou dobu životnosti komponenty. Nejprve implementujte IDisposable a injektujte továrnu, jak je znázorněno v :Pages/EditContact.razor

@implements IDisposable
@inject IDbContextFactory<ContactContext> DbFactory

Ukázková aplikace zajistí uvolnění kontextu při uvolnění komponenty:

public void Dispose()
{
    Context?.Dispose();
}

Nakonec se OnInitializedAsync přepíše , aby se vytvořil nový kontext. V ukázkové aplikaci načte OnInitializedAsync kontakt stejným způsobem:

protected override async Task OnInitializedAsync()
{
    Busy = true;

    try
    {
        Context = DbFactory.CreateDbContext();
        Contact = await Context.Contacts
            .SingleOrDefaultAsync(c => c.Id == ContactId);
    }
    finally
    {
        Busy = false;
    }

    await base.OnInitializedAsync();
}

Povolení protokolování citlivých dat

EnableSensitiveDataLogging zahrnuje data aplikací do zpráv výjimek a protokolování architektury. Zaprotokolovaná data mohou zahrnovat hodnoty přiřazené vlastnostem instancí entit a hodnoty parametrů pro příkazy odeslané do databáze. Protokolování dat pomocí je EnableSensitiveDataLogging bezpečnostním rizikem, protože může vystavit hesla a další identifikovatelné osobní údaje(PII) při protokolování SQL příkazů prováděných proti databázi.

Doporučujeme povolit pouze pro EnableSensitiveDataLogging vývoj a testování:

#if DEBUG
    services.AddDbContextFactory<ContactContext>(opt =>
        opt.UseSqlite($"Data Source={nameof(ContactContext.ContactsDb)}.db")
        .EnableSensitiveDataLogging());
#else
    services.AddDbContextFactory<ContactContext>(opt =>
        opt.UseSqlite($"Data Source={nameof(ContactContext.ContactsDb)}.db"));
#endif

Další materiály

Blazor Server je architekturou stavových aplikací. Aplikace udržuje průběžné připojení k serveru a stav uživatele se uchovává v paměti serveru v okruhu. Jedním z příkladů stavu uživatele jsou data uchovaná v instancích služby injektáže závislostí (DI), které jsou vymezené na okruh. Jedinečný aplikační model, který Blazor Server poskytuje, vyžaduje zvláštní přístup k používání Entity Framework Core.

Poznámka

Tento článek se EF Core v aplikacích Blazor Server . Blazor WebAssembly Aplikace běží v sandboxu WebAssembly, který brání většině přímých připojení k databázi. Spuštění EF Core v Blazor WebAssembly je nad rámec tohoto článku.

Ukázková aplikace

Ukázková aplikace byla vytvořena jako reference pro aplikace Blazor Server , které používají EF Core. Ukázková aplikace obsahuje mřížku s operacemi řazení a filtrování, odstranění, přidání a aktualizace. Ukázka demonstruje použití EF Core ke zpracování optimistické souběžnosti.

Zobrazení nebo stažení ukázkového kódu (stažení)

Ukázka používá místní databázi SQLite , aby ji bylo možné použít na libovolné platformě. Ukázka také nakonfiguruje protokolování databáze tak, aby SQL vygenerované dotazy. Tato konfigurace se konfiguruje v :appsettings.Development.json

{
  "DetailedErrors": true,
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information",
      "Microsoft.EntityFrameworkCore.Database.Command": "Information"
    }
  }
}

Komponenty mřížky, přidání a zobrazení používají vzor kontext-operace, ve kterém se pro každou operaci vytvoří kontext. Komponenta edit používá vzor "kontext pro jednotlivé komponenty", kdy se pro každou komponentu vytvoří kontext.

Poznámka

Některé příklady kódu v tomto tématu vyžadují obory názvů a služby, které se nezobrazují. Pokud chcete zkontrolovat plně funkční kód, včetně příkladů požadovaných @using@injectRazor direktiv a , podívejte se na ukázkovou aplikaci.

Přístup k databázi

EF Core se jako prostředek DbContext konfigurace přístupu k databázi spoléhá na a funguje jako pracovní jednotka. EF Core poskytuje rozšíření AddDbContext pro ASP.NET Core, které ve výchozím nastavení zaregistrují kontext jako vymezenou službu. V Blazor Server aplikacích to může být problematické, protože instance se sdílí mezi komponentami v rámci okruhu uživatele. DbContext není bezpečná pro přístup z více vláken a není určená pro souběžné použití. Stávající životnost je nevhodná z těchto důvodů:

  • Singleton sdílí stav mezi všemi uživateli aplikace a vede k nevhodnému souběžnýmu použití.
  • Vymezené (výchozí) představuje podobný problém mezi komponentami pro stejného uživatele.
  • Přechodné výsledky mají za výsledek novou instanci na požadavek. Vzhledem k tomu, že komponenty mohou být dlouhodobé, vede to k dlouhodobému kontextu, než je zamýšleno.

Následující doporučení jsou navržená tak, aby poskytovala konzistentní přístup k používání EF Core v aplikacích Blazor Server .

  • Ve výchozím nastavení zvažte použití jednoho kontextu na operaci. Kontext je navržený pro rychlé vytváření instancí s nízkou režií:

    using var context = new MyContext();
    
    return await context.MyEntities.ToListAsync();
    
  • Pokud chcete zabránit více souběžných operacím, použijte příznak :

    if (Loading)
    {
        return;
    }
    
    try
    {
        Loading = true;
    
        ...
    }
    finally
    {
        Loading = false;
    }
    

    Umístěte operace za Loading = true; řádek v try bloku.

  • Pro dlouhodobější operace, které využívají EF Core sledování změn nebo řízení souběžnosti , vyjád te kontext na dobu života komponenty.

Nové instance DbContext

Nejrychlejší způsob, jak vytvořit novou DbContext instanci, je new pomocí příkazu vytvořit novou instanci. Existuje však několik scénářů, které mohou vyžadovat řešení dalších závislostí. Můžete například chtít použít ke konfiguraci DbContextOptions kontextu.

Doporučeným řešením pro vytvoření nového řešení DbContext se závislostmi je použití továrny. Ukázková aplikace implementuje vlastní továrnu v .Data/DbContextFactory.cs

using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;

namespace BlazorServerDbContextExample.Data
{
    public class DbContextFactory<TContext> 
        : IDbContextFactory<TContext> where TContext : DbContext
    {
        private readonly IServiceProvider provider;

        public DbContextFactory(IServiceProvider provider)
        {
            this.provider = provider ?? throw new ArgumentNullException(
                $"{nameof(provider)}: You must configure an instance of " +
                "IServiceProvider");
        }

        public TContext CreateDbContext() => 
            ActivatorUtilities.CreateInstance<TContext>(provider);
    }
}

V předchozí továrně:

Následující příklad nakonfiguruje SQLite a povolí protokolování dat. Kód ke konfiguraci databázové továrny pro vlastníka a poskytnutí výchozích možností používá rozšiřující metodu:

services.AddDbContextFactory<ContactContext>(opt =>
    opt.UseSqlite($"Data Source={nameof(ContactContext.ContactsDb)}.db"));

Továrna se injektuje do komponent a používá se k vytváření nových instancí. Například v souboru Pages/Index.razor:

private async Task DeleteContactAsync()
{
    using var context = DbFactory.CreateDbContext();

    Filters.Loading = true;

    var contact = await context.Contacts.FirstAsync(
        c => c.Id == Wrapper.DeleteRequestId);

    if (contact != null)
    {
        context.Contacts.Remove(contact);
        await context.SaveChangesAsync();
    }

    Filters.Loading = false;

    await ReloadAsync();
}

Poznámka

Wrapperje odkaz na komponentuGridWrapper. Podívejte se Index na komponentu (Pages/Index.razor) v ukázkové aplikaci.

Nové DbContext instance DbContextje možné vytvořit pomocí továrny, která umožňuje nakonfigurovat připojovací řetězec pro , například při použití [modelu ASP.NET CoreIdentity])(xref:security/authentication/customize_identity_model). Další informace najdete v tématu Vícevrstvá (EF Core dokumentace).

Rozsah životnosti komponenty

Můžete chtít vytvořit objekt , DbContext který existuje po celou dobu životnosti komponenty. To vám umožní ji použít jako jednotku práce a využívat integrované funkce, jako je sledování změn a řešení souběžnosti. Objekt pro vytváření můžete použít k vytvoření kontextu a jeho sledování po celou dobu životnosti komponenty. Nejprve implementujte IDisposable a injektujte továrnu, jak je znázorněno v :Pages/EditContact.razor

@implements IDisposable
@inject IDbContextFactory<ContactContext> DbFactory

Ukázková aplikace zajistí uvolnění kontextu při uvolnění komponenty:

public void Dispose()
{
    Context?.Dispose();
}

Nakonec se OnInitializedAsync přepíše , aby se vytvořil nový kontext. V ukázkové aplikaci načte OnInitializedAsync kontakt stejným způsobem:

protected override async Task OnInitializedAsync()
{
    Busy = true;

    try
    {
        Context = DbFactory.CreateDbContext();
        Contact = await Context.Contacts
            .SingleOrDefaultAsync(c => c.Id == ContactId);
    }
    finally
    {
        Busy = false;
    }

    await base.OnInitializedAsync();
}

V předchozím příkladu:

  • Pokud Busy je nastavena na true, mohou začít asynchronní operace. Při Busy nastavení zpět na by falseměly být dokončeny asynchronní operace.
  • Do bloku umístěte další logiku zpracování catch chyb.

Povolení protokolování citlivých dat

EnableSensitiveDataLogging zahrnuje data aplikací do zpráv výjimek a protokolování architektury. Zaprotokolovaná data mohou zahrnovat hodnoty přiřazené vlastnostem instancí entit a hodnoty parametrů pro příkazy odeslané do databáze. Protokolování dat pomocí je EnableSensitiveDataLogging bezpečnostním rizikem, protože může vystavit hesla a další identifikovatelné osobní údaje(PII) při protokolování SQL příkazů prováděných proti databázi.

Doporučujeme povolit pouze pro EnableSensitiveDataLogging vývoj a testování:

#if DEBUG
    services.AddDbContextFactory<ContactContext>(opt =>
        opt.UseSqlite($"Data Source={nameof(ContactContext.ContactsDb)}.db")
        .EnableSensitiveDataLogging());
#else
    services.AddDbContextFactory<ContactContext>(opt =>
        opt.UseSqlite($"Data Source={nameof(ContactContext.ContactsDb)}.db"));
#endif

Další materiály