Indice dei contenuti
Se vi siete imbattuti in questo articolo, è probabile che siate sviluppatori ASP.NET Core al lavoro su un'applicazione web configurata per accedere a un database SQL tramite Entity Framework Core - il noto framework ORM (Object-Relational Mapping) open source per ADO. NET di cui abbiamo avuto più volte occasione di parlare in questo blog: in particolare, è probabile che abbiate l'esigenza di eseguire una query SQL su un database diverso da quello utilizzato da EF Core, e/o su tabelle diverse da quelle "mappate" dall'ORM.
Personalmente mi sono trovato ad affrontare questo problema in un paio di occasioni, ad esempio durante lo sviluppo di un'applicazione Web che utilizzava Serilog - un utile strumento che consente di persistere i log di ASP.NET all'interno di un database SQL: nel caso specifico, avevo configurato Serilog per scrivere i propri log su un database diverso rispetto a quello utilizzato da EF Core: la mia esigenza era quindi quella di trovare il modo di far accedere EF Core alla tabella contenente i log record di Serilog, ovvero a un database diverso. In questo articolo spiegherò brevemente come sono riuscito in questo intento grazie ai metodi HasNoKey e FromSqlRaw forniti nativamente da EF Core.
Creare la Entity
La prima cosa che ho fatto è stata creare la entity che avrebbe ospitato i log record, così da poter gestire i suddetti in modo "strutturato": ho chiamato la entity BaseLog in quanto "Log" mi sembrava un nome un pò troppo generico, così da non rischiare di avere problemi di naming conflict che mi avrebbero costretto a specificare il namespace.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
public class BaseLog { public BaseLog() { } [Key] [Required] public long Id { get; set; } public DateTime Timestamp { get; set; } public string LogLevel { get; set; } public string Message { get; set; } public string MessageTemplate { get; set; } public string Exception { get; set; } public string Properties { get; set; } } |
Come si può vedere, la entity non ha nulla di particolare: l'unica differenza di rilievo rispetto alle altre è data dal fatto che, come sappiamo, non deve esser mappata su una tabella del Database, in quanto si riferisce a una tabella presente su un database diverso. Questo significa che dovremo configurarla in modo diverso all'interno del nostro DbContext.
Configurare il DbContext
Come probabilmente già sapete (se utilizzate EF Core), il DbContext è una componente fondamentamentale di EF Core: in estrema sintesi, ciascuna istanza DbContext rappresenta una sessione all'interno di un database che può essere utilizzata per effettuare query di lettura e/o scrittura.
Il metodo standard per configurare una entity mappata su una tabella del database gestito tramite EF Core consiste nell'inserire le seguenti righe nel metodo OnModelCreating della nostra classe DbContext.cs:
1 2 |
modelBuilder.Entity<SampleEntity>().ToTable("SampleEntity"); public DbSet<SampleEntity> SampleEntities { get; set; } |
Le righe di codice di cui sopra dicono a EF Core di eseguire il mapping dell'entità SampleEntity alla tabella del database [SampleEntity], eventualmente anche creandola (nel caso in cui si utilizzi l'approccio Code-First basato sulle migrations) se non esiste ancora; inoltre, la collezione SampleEntities ci consente anche accedere ai record della tabella utilizzando le comodissime fluent API di EF Core, come nell'esempio seguente:
1 |
dbContext.SampleEntities.Where(x => x.Id == "someId"); |
Tuttavia, la entity BaseLog che abbiamo appena creato non deve essere "mappata" su una determinata tabella del database utilizzato da EF Core, in quanto la tabella contenente i record che dovrà gestire appartiene a un database diverso. Proprio per questo motivo dobbiamo adottare una configurazione leggermente diversa:
1 2 |
modelBuilder.Entity<BaseLog>().ToTable("Logs", t => t.ExcludeFromMigrations()).HasNoKey(); public DbSet<BaseLog> Logs { get; set; } |
Come possiamo vedere, abbiamo configurato la entity in un modo leggermente diverso. In estrema sintesi, abbiamo detto a EF Core:
- di mappare la entity BaseLog su una tabella con nome "Logs": questo mapping di fatto non sarà mai utilizzato, come vedremo tra poco: la sua unica funzione è quella di consentirci l'utilizzo dell'overload del metodo ToTable che ci consente di escludere le migration (vedi punto seguente).
- di escludere in modo esplicito la entity BaseLog entity da qualsiasi migration, utilizzando il metodo ExcludeFromMigrations.
- di configurare la entity senza chiave primaria, utilizzando il metodo HasNoKey.
A dire il vero, la tabella BaseLog una sua chiave ce l'ha eccome: si tratta del campo Id. Tuttavia, poiché tale chiave riguarda un database esterno ed è gestita direttamente tramite Serilog, è bene che EF Core la ignori del tutto.
Recuperare i dati
Ora che abbiamo definito la entity BaseLog e abbiamo detto a EF Core come gestirla correttamente, possiamo usarla nel modo seguente:
1 2 3 4 5 |
var logsTable = "Serilog.Logs"; var logList = await context.DbContext.Logs .FromSqlRaw($"SELECT * FROM {logsTable} WHERE LogLevel LIKE 'Error'") .OrderByDescending(l => l.Id) .ToListAsync(); |
Come possiamo vedere, le attività di recupero dei dati sono gestite dal metodo FromSqlRaw, che conente di recuperare i dati da qualsiasi tabella non mappata in modo esplicito a un'entità all'interno di DbContext.
Nell'esempio sopra, Serilog è il nome del database esterno e Logs è il nome della tabella DB (nel database [Serilog]) contenente i record di log che vogliamo gestire con l'entità BaseLog: entrambi sono specificati utilizzando la variabile logsTable, il cui valore è "Serilog.Logs" (usando la tipica sintassi SQL DBName.TableName).
Inutile dire che, affinché il codice di cui sopra funzioni, sarà necessario concedere le autorizzazioni di lettura per il database [Serilog] allo stesso utente DB che stiamo utilizzando nella ConnectionString relativa a EF Core, altrimenti la query non funzionerà per ovvie ragioni legate alla mancanza di permessi/autorizzazioni sufficienti.
Conclusione
Per il momento è tutto: ci auguriamo che questo piccolo tutorial possa essere utile ad altri sviluppatori ASP.NET Core alla ricerca di un modo per utilizzare EF Core per gestire database e/o tabelle esterni senza rinunciare agli enormi vantaggi offerti dalle entity, dall'approccio fortemente tipizzato e dalle fluent API di EF Core.