Feladat 1: Alap infrastruktúra¶
A készülő alkalmazásban az IMDB által naponta frissített, publikus adatok felhasználásával fogjuk filmek adatait kezelni. Fontos, hogy ezt az adathalmazt saját, tanulási célra szabadon használhatjuk, minden más célra kifejezett engedélyt kell kérni.
Az elkészítendő alkalmazásunk egy osztálykönyvtár lesz, amit más kódból (pl. egy REST API-t szolgáltató web API projektből vagy egy szerveroldali renderelést használó projektből) szeretnénk majd elérni. Mivel a kódunk osztálykönyvtár, önmagában nem is futtatható, hanem egy .dll fájlként csak a kódot tartalmazza, amit más, futó .NET alkalmazásból el tudunk majd érni.
Először létrehozzuk az osztálykönyvtárat, amiben az adatbázist reprezántáló DbContextet, a táblákat reprezentáló entitásokat fogjuk definiálni. Itt lesznek még a szükséges migrációk is, amelyeket az adatbázisunkon lefuttatva az adatsémát a modellel szinkronba hozhatjuk. Az adatbázist code first módszertannal modellezzük, tehát először az entitásokat fogjuk definiálni, aztán létrehozzuk a migrációkat, amiket végül lefuttatunk, így jön majd létre az adatbázisban a modellünknek megfelelő séma.
Az osztálykönyvtárunkban található kódot nem tudjuk "csak úgy" futtatni, szükségünk lesz tehát valamilyen projektre, ami képes a kód futtatására. Ehhez ugyanabban a solutionben fogunk létrehozni egy új konzolos alkalmazást.
Emlékeztetőként:
- Amikor az adatmodellt (C# entitásosztályokat) módosítjuk, új migrációt kell létrehozni, erre használhatjuk a
dotnet ef migrations add migrációnévparancsot (a migrációnév nevű migrációnak egyedinek kell lennie, érdemes értelmes nevet adni neki, pl. 'AddTotalCostColumnToProductOrder'). - Az adatbázist a legújabb migrációra a
dotnet ef update databaseparanccsal frissíthetjük. - Ha egy migrációt elrontottunk vagy szeretnénk visszavonni, használjuk a
dotnet ef migrations removeparancsot. Ez a legutolsó migrációt törli. Ezután adjuk hozzá az új migrációt.- Ha a migrációt már alkalmaztuk az adatbázisra, előtte mindenképp futtassuk a
dotnet ef database update migrációnévparancsot, ahol a migrációnév az utolsóelőtti migráció, tehát eggyel korábbi, mint amit törölni szeretnénk. Ezzel az adatbázis az előző migráció hatását visszafordítja, ezután az utolsó migráció a fenti paranccsal törölhető. - Ha maguk a migrációk rendben vannak, de az adatbázis adattartalmát törölni akarjuk (kezdjük elölről), hasznos a
dotnet ef database update 0parancs, ami eldobja az összes migrációban érintett objektumot (pl. táblát), majd egydotnet ef database updateparanccsal lefuttatjuk az összes migrációt. Így egy már használható, de alaphelyzetben lévő adatbázist kapunk.
- Ha a migrációt már alkalmaztuk az adatbázisra, előtte mindenképp futtassuk a
- Mivel a parancsokat a Visual Studio-tól függetlenül futtatjuk, így minden parancsfuttatás előtt érdemes minden nem mentett fájlt elmenteni, vagy fordítani a solutiont.
- Alternatívaként használhatjuk a powershell alapú parancsokat is a Visual Studio Package Manager Console-jából (PMC). Ilyenkor általában kevesebb paramétert kell megadnunk, mert a Visual Studio / PMC állapota alapján töltődnek.
Lássunk neki!
Előkészületek¶
- Telepítsük az EF Core Global Tool-t: adjuk ki az alábbi parancsot egy tetszőleges parancssorban:
dotnet tool install --global dotnet-ef- Ha bármilyen okból kifolyólag korábban már telepítve volt, az
installparancsotupdate-re cserélve frissíthető a tool a legfrissebb stabil verzióra.
- Ha bármilyen okból kifolyólag korábban már telepítve volt, az
- Ezzel használhatók lesznek a
dotnet efparancsok.
- Hozzunk létre egy új .NET (.NET 8 verziójú, a későbbiekben is) C# osztálykönyvtárat (Class library) MovieCatalog.Data néven, MovieCatalog solutionnel egy kedvenc üres munkamappánkban!
- Adjunk a solutionhöz egy új .NET C# konzol projektet is MovieCatalog.Terminal néven!
- Töröljük a létrejött helyőrző fájlt (Class1.cs) az adatréteg projektben!
- Adjunk referenciát a konzolos projektből az adatréteg projektre! Értelemszerűen így a konzolos projektből el fogjuk érni az adatréteg típusait és API-ját, fordítva viszont nem.
- Adjunk referenciát a
Microsoft.Extensions.HostingNuGet csomagra a MovieCatalog.Terminal projektből! - Adjunk referenciát a
Microsoft.EntityFrameworkCore.SqlServerésMicrosoft.EntityFrameworkCore.DesignNuGet csomagokra a MovieCatalog.Data projektből! - Állítsuk be a konzolos projektet Startup projektként, így F5 (Start with Debugging) hatására ez fog elindulni. Ezzel a projekt neve félkövér lesz.
Ha mindent jól csináltunk, az alábbiakat kell látnunk a projektszerkezetben (a verziószámok lehetnek nagyobbak):

Alap infrastruktúra kialakítása, tesztelése¶
-
Hozzunk létre egy új mappát az adatrétegben Entities néven, és adjuk hozzá a
Titleentitást:namespace MovieCatalog.Data.Entities { public class Title { public int Id { get; set; } public string TConst => $"tt{Id.ToString().PadLeft(7, '0')}"; public string PrimaryTitle { get; set; } public Title(string primaryTitle) { PrimaryTitle = primaryTitle; } } }- Láthatjuk, hogy a
TConstmező számított érték, az IMDb elnevezési konvenciója alapjántt1234567formátumban van, de mi csak a számértéket tároljuk majd az adatbázisban.
- Láthatjuk, hogy a
-
Hozzunk létre egy új DbContext típust az adatrétegben
MovieCatalogDbContextnéven, az alábbi tartalommal:
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using MovieCatalog.Data.Entities;
namespace MovieCatalog.Data
{
public class MovieCatalogDbContext : DbContext
{
public MovieCatalogDbContext(ILogger<MovieCatalogDbContext> logger
,DbContextOptions<MovieCatalogDbContext> options) : base(options)
{
Logger = logger;
}
private ILogger<MovieCatalogDbContext> Logger { get; }
public DbSet<Title> Titles => Set<Title>();
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Title>(title =>
{
title.Property(t => t.Id).ValueGeneratedNever();
title.Property(t => t.PrimaryTitle)
.HasMaxLength(500);
title.HasIndex(t => t.PrimaryTitle);
});
}
}
}
A Title entitásunkon konfiguráltuk az Id és PrimaryTitle tulajdonságokat (adatbázistábla mezőket):
- Az
Idnevű mező konvenció szerint adatbázis által generált, mi most viszont kézzel szeretnénk megadni (az IMDb-ből fog érkezni). - A címben gyakran szeretnénk keresni, ezért indexeljük.
- Az EF alapértelmezetten NVARCHAR(max) típusú string mezőket hoz nekünk létre.
- Az indexelés SQL szerveren csak bajosan alkalmazható NVARCHAR(max), azaz nem korlátozott hosszúságú méretű mezőkön (ugyanis azok nem a rekordban, hanem a rekordhoz hivatkozva tárolódnak). Ezért be kell állítanunk a maximális címhosszt, és vannak igen hosszú című filmek/videók.
-
Az EF alapértelmezett konvencióként a mezők nullozhatóságát a leképzendő property típusának nullozhatósága adja. A
stringtípus .NET 6-os verzió óta alapértelmezetten nem nullozhatóként van számon tartva, így az adatbázisbeli kötelezőséget külön nem kell beállítanunk. -
A migráció létrehozásához szükséges a CLI tudtára adni, hogy milyen adatbázismotorra készítse a migrációkat (más migráció készül pl. SQL Serverre mint SQLite-ra). Hozzunk létre egy Design nevű mappát a Data projektben, benne az alábbi Factory osztályt, ami egy
DbContextet tud gyártani nekünk. A factory-t "éles" futás közben nem használja semmi, kizárólag a migrációs fájlok elkészítése miatt szükséges most nekünk. A connection stringet az éles alkalmazás nem ezt a factory-t használva fogja átadni. Láthatjuk, hogy ez az osztály nem is használható (szabályosan) más szerelvényekből, mertinternalláthatóságú. Értelemszerűen a connection string cserélendő, ha nem LocalDB adatbázison készül az alkalmazás, de alapértelmezetten és a laborokban az teljesen megfelelő.
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Design;
using Microsoft.Extensions.Logging;
namespace MovieCatalog.Data.Design
{
internal class MovieCatalogDesignTimeDbContextFactory : IDesignTimeDbContextFactory<MovieCatalogDbContext>
{
public MovieCatalogDbContext CreateDbContext(string[] args) =>
new(new Logger<MovieCatalogDbContext>(new LoggerFactory()),
new DbContextOptionsBuilder<MovieCatalogDbContext>()
.UseSqlServer(@"Server=(localdb)\mssqllocaldb;Database=MovieCatalog")
.Options
);
}
}
- Készítsünk migrációt, majd futtassuk le azt az adatbázison! Terminálban/PowerShell ablakban adjuk ki az alábbi parancsokat (Visual Studio-ban és Code-ban is a
Ctrl+öbillentyűkombináció nyit egy Developer PowerShell ablakot) a Data projekt mappájából: dotnet ef migrations add TitlesTabledotnet ef database update

- Készítsük el a konzol alkalmazást reprezentáló osztályt a Terminal projektben.
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using MovieCatalog.Data;
using System.Threading;
using System.Threading.Tasks;
namespace MovieCatalog.Terminal
{
public class TestConsole : IHostedService
{
public TestConsole(MovieCatalogDbContext dbContext, IHost host, ILogger<TestConsole> logger)
{
DbContext = dbContext;
Host = host;
Logger = logger;
}
private MovieCatalogDbContext DbContext { get; }
private IHost Host { get; }
private ILogger<TestConsole> Logger { get; }
public async Task StartAsync(CancellationToken cancellationToken)
{
Logger.LogInformation("Started.");
// TODO: Ide jön az alkalmazás kódja.
await Host.StopAsync(cancellationToken);
}
public Task StopAsync(CancellationToken cancellationToken)
{
Logger.LogInformation("Stopping...");
return Task.CompletedTask;
}
}
}
- Láthatjuk, hogy a
TestConsoleosztály számít rá, hogy kapni fog valahonnan egyMovieCatalogDbContextpéldányt, tehát felkészültünk arra, hogy a rendszer dependency injectiont használ. -
Érdekesség a
cancellationTokennévre hallgató paraméter. Ez egy aktiválható token, amit átpasszolhatunk további aszinkron kéréseknek, pl. a fentiStopAsync-nak. Ez azt eredményezi, hogy ezek a hívások megvizsgálatják, valaki "nyomott-e mégsemet" a láncban feljebb, és ha igen, akkor abbahagyják a futást. Nem szükséges használni, de szép, szofisztikált pattern, jó tudni róla. Ha egy függvényt írunk, amiCancellationToken-t kap, akkor a tokent illik továbbpasszolni azt minden általunk hívott függvénynek (ha van olyan változata, ami fogad ilyen paramétert). -
Készítsük el a konzolt kiszolgáló részt az alkalmazásban. A legelegánsabb megoldás az ASP.NET-tel analóg módon egy GenericHostBuilder osztály segítségével elkészíteni a hosztkészítő objektumot, majd az megépíteni és elindítani. Cseréljük le a Program.cs fájl teljes tartalmát az alábbira:
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using MovieCatalog.Data;
using MovieCatalog.Terminal;
using IHost host = Host.CreateDefaultBuilder(args)
.ConfigureServices((_, services) =>
services.AddDbContext<MovieCatalogDbContext>(o =>
o.UseSqlServer(@"Server=(localdb)\mssqllocaldb;Database=MovieCatalog"))
.AddHostedService<TestConsole>())
.ConfigureLogging(l => l.AddFilter("Microsoft.EntityFrameworkCore", LogLevel.Warning))
.Build();
await host.RunAsync();
A fenti indítási módszer analóg a Háttéralkalmazásokból tanult indítási móddal az ASP.NET Core kapcsán, a kivétel az indítás módjában rejlik: itt most nem egy HTTP-t kiszolgálni képes hosztot, hanem "csak" egy konzolalkalmazást indítunk.
Ha meg akarjuk nézni az EF által generált SQL-t, állítsuk át a naplózási szintet a ConfigureLogging hívásban LogLevel.Information-re.
Feladat 1¶
Szúrj be egy rekordot a Titles táblába a terminál alkalmazásból, melyben a cím a Neptun kódod! Készíts képernyőképet az ezt megvalósító kódrészletről, valamint igazold annak a tényét, hogy a rekord beszúrásra került az alábbi két módszerrel (mindkettővel!):
- SQL alapú megoldással (pl. *SQL Server Object Explorer*ben futtatott lekérdezéssel), ÉS
- a konzol alkalmazásban történő újbóli lekérdezéssel, a konzolra (
LoggerpéldányraLogInformationhívással) történő kiírással!
Következő feladat¶
Folytasd a következő feladattal.