Entity Framework con .NET 6 minimal web API
Nel post precedente .NET 6 minimal web API ho mostrato una API base senza accesso ad un database.
Nella vita reale qualsiasi API usa un database.
Il modo migliore per aggiungere il supporto per qualsiasi database è quello di utilizzare Entity Framework.
e si aggiunge lo using del namespaces all'inizio
poi il servizio DbContext (dopo la variabile builder)
e il DbContext
con
e ovviamente aggiungiamo la connection string sul file appsettings.json
infine creiamo la migration iniziale
e creiamo/aggiorniamo il database
se tutto va a buon fine viene visualizzato un messaggio simile
Durante l'aggiunta di una migration o l'aggiornamento del database, compare un errore simile a questo (StopTheHostException)
L'esempio completo è disponibile anche su Git Hub - Sgart.Net.MinimalAPI.
Vedi anche .NET 6 minimal web API.
Nella vita reale qualsiasi API usa un database.
Il modo migliore per aggiungere il supporto per qualsiasi database è quello di utilizzare Entity Framework.
NuGet
Dopo aver creato il progetto, la prima attività da fare è quella di aggiungere i necessari pacchetti NuGet per il supporto in memoryDOS / Batch file: NuGet
dotnet add package Microsoft.EntityFrameworkCore.InMemory --version 6.0.6
dotnet add package Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore --version 6.0.6
Entity Framework
Per aggiungere il supporto a Entity Framework, si parte dal codice baseC#: Program.cs
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/", () => "Sgart.it ciao");
app.Run();
C#: Program.cs
using Microsoft.EntityFrameworkCore;
C#: Program.cs
// registro EF DB context usando InMemoryDatabase, add services to the container.
// Note: InMemoryDatabase da usare solo per Demo, non usare in produzione
builder.Services.AddDbContext<TodoDbContext>(opt => opt.UseInMemoryDatabase("SgartTodoList"));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
Per l'esempio uso un database in memoria utile per realizzare velocemente delle demo. Da non usare in produzione.
in coda a tutto, si aggiunge la classe che rappresenta l'oggetto da persistere su databaseC#: Program.cs
class Todo
{
public int TodoId { get; set; }
public string? Text { get; set; }
public bool Completed { get; set; }
};
C#: Program.cs
class TodoDbContext : DbContext
{
public TodoDbContext(DbContextOptions<TodoDbContext> options)
: base(options) { }
public DbSet<Todo> Todos => Set<Todo>();
}
API
A questo punto l'applicazione è configurata, si possono creare le API che implementeranno le classiche operazioni CRUD usando il TodoDbContext- GET /todo => elenco di tutti gli items
- GET /todo/completed => elenco di tutti gli items completati
- GET /todo/contains?text=<stringa> => elenco di tutti gli items che contengono la stringa passata
- GET /todo/{id} => ritorna il singolo item identificato dall'id passato in url
- POST /todo => inserisce un nuovo item (i valori andranno passati nel body in formato JSON)
- PUT /todo/{id} => modifica l'item identificato dall'id passato in url (i valori andranno passati nel body in formato JSON)
- DELETE /todo/{id} => cancella l'item identificato dall'id passato in url
C#: Program.cs
// API Todo basate su TodoDbContext
app.MapGet("/todo", async (TodoDbContext db) => await db.Todos.ToListAsync());
app.MapGet("/todo/completed", async (TodoDbContext db) => await db.Todos.Where(x => x.Completed == true).ToListAsync());
// /todo/text/contains?text=ciao
app.MapGet("/todo/contains", async (string text, TodoDbContext db) =>
await db.Todos
.Where(x => x.Text != null && x.Text.Contains(text, StringComparison.InvariantCultureIgnoreCase))
.ToListAsync());
app.MapGet("/todo/{todoId}", async (int todoId, TodoDbContext db) =>
await db.Todos.FindAsync(todoId) is Todo todo
? Results.Ok(todo)
: Results.NotFound());
app.MapPost("/todo", async (TodoInputDTO inputTodo, TodoDbContext db) =>
{
// validare sempre i parametri di ingresso
if (string.IsNullOrWhiteSpace(inputTodo.Text))
return Results.BadRequest("Invalid Text");
var todo = new Todo
{
Text = inputTodo.Text,
Completed = inputTodo.Completed
};
db.Todos.Add(todo);
await db.SaveChangesAsync();
return Results.Created($"/todo/{todo.TodoId}", todo);
});
app.MapPut("/todo/{id}", async (int id, TodoInputDTO inputTodo, TodoDbContext db) =>
{
var todo = await db.Todos.FindAsync(id);
if (todo is null)
return Results.NotFound();
// validare sempre i parametri di ingresso
if (string.IsNullOrWhiteSpace(inputTodo.Text))
return Results.BadRequest("Invalid Text");
todo.Text = inputTodo.Text;
todo.Completed = inputTodo.Completed;
await db.SaveChangesAsync();
return Results.NoContent();
});
app.MapDelete("/todo/{id}", async (int id, TodoDbContext db) =>
{
if (await db.Todos.FindAsync(id) is Todo todo)
{
db.Todos.Remove(todo);
await db.SaveChangesAsync();
return Results.Ok(todo);
}
return Results.NotFound();
});
Nella chiamata delle API ricordarsi di aggiungere l'header content-type: application/json
Database
Su un'applicazione di produzione ovviamente non useremo un db in memoria, ma opteremo per uno reale come SQL Server, quindi aggiungiamo dei nuovi pacchetti per supportare questo scenarioDOS / Batch file: NuGet
dotnet add package Microsoft.EntityFrameworkCore.SqlServer --version 6.0.6
dotnet add package Microsoft.EntityFrameworkCore.Design --version 6.0.6
Per un elenco di tutti i database supportati vedi Provider di database.
e sostituiamo la rigaC#: Program.cs
builder.Services.AddDbContext<TodoDbContext>(opt => opt.UseInMemoryDatabase("SgartTodoList"));
C#: Program.cs
builder.Services.AddDbContext<TodoDbContext>(option =>
{
option.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection"));
});
JSON: appsettings.json
{
"ConnectionStrings": {
"DefaultConnection": "Server=(localdb)\\mssqllocaldb;Database=SgartNetMinimalApi;Trusted_Connection=True;MultipleActiveResultSets=true",
"DefaultConnection_NotUsed_Trusted": "Server=ServerName1;Database=SgartNetMinimalApi;Trusted_Connection=True;MultipleActiveResultSets=true"
},
...
}
DOS / Batch file
dotnet ef migrations add [migration name]
DOS / Batch file
dotnet ef database update
Errori
Se compare un errore simile a questoThe Entity Framework tools version '5.0.10' is older than that of the runtime '6.0.6'. Update the tools for the latest features and bug fixes. See https://aka.ms/AAc1fbw
for more information.
eseguiDOS / Batch file
dotnet tool update --global dotnet-ef
Text
Lo strumento 'dotnet-ef' è stato aggiornato dalla versione '5.0.10' alla versione '6.0.6'.
Durante l'aggiunta di una migration o l'aggiornamento del database, compare un errore simile a questo (StopTheHostException)
Build started...
Build succeeded.
2022-07-10 19:12:05.8114|ERROR|Program|Stopped program because of exception|Microsoft.Extensions.Hosting.HostFactoryResolver+HostingListener+StopTheHostException: Exception of type 'Microsoft.Extensions.Hosting.HostFactoryResolver+HostingListener+StopTheHostException' was thrown.
at Microsoft.Extensions.Hosting.HostFactoryResolver.HostingListener.OnNext(KeyValuePair`2 value)
at System.Diagnostics.DiagnosticListener.Write(String name, Object value)
at Microsoft.Extensions.Hosting.HostBuilder.Build()
at Microsoft.AspNetCore.Builder.WebApplicationBuilder.Build()
at Program.<Main>$(String[] args) in D:\PROJECTS\Sgart.Net.MinimalAPI\Program.cs:line 33
Done. To undo this action, use 'ef migrations remove'
sembra "normale" la migration viene creata e il database viene creato... da approfondire.Build succeeded.
2022-07-10 19:12:05.8114|ERROR|Program|Stopped program because of exception|Microsoft.Extensions.Hosting.HostFactoryResolver+HostingListener+StopTheHostException: Exception of type 'Microsoft.Extensions.Hosting.HostFactoryResolver+HostingListener+StopTheHostException' was thrown.
at Microsoft.Extensions.Hosting.HostFactoryResolver.HostingListener.OnNext(KeyValuePair`2 value)
at System.Diagnostics.DiagnosticListener.Write(String name, Object value)
at Microsoft.Extensions.Hosting.HostBuilder.Build()
at Microsoft.AspNetCore.Builder.WebApplicationBuilder.Build()
at Program.<Main>$(String[] args) in D:\PROJECTS\Sgart.Net.MinimalAPI\Program.cs:line 33
Done. To undo this action, use 'ef migrations remove'
API completa
Il codice completo dell'API con Entity Framework e supporto di NLog è questoC#: Program.cs
using Microsoft.AspNetCore.Mvc;
using NLog;
using NLog.Web;
using Microsoft.EntityFrameworkCore;
// imposto NLog per leggere da appsettings.json
var logger = NLog.LogManager.Setup().LoadConfigurationFromAppSettings().GetCurrentClassLogger();
logger.Debug("Sgart.it demo init");
try
{
var builder = WebApplication.CreateBuilder(args);
// registro EF DB context
// usando InMemoryDatabase, add services to the container.
// Note: InMemoryDatabase da usare solo per Demo, non usare in produzione
//builder.Services.AddDbContext<TodoDbContext>(opt => opt.UseInMemoryDatabase("SgartTodoList"));
// oppure usando un DB reale
builder.Services.AddDbContext<TodoDbContext>(option =>
{
option.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection"));
});
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
// Setup NLog for Dependency injection
builder.Logging.ClearProviders();
builder.Host.UseNLog();
var app = builder.Build();
app.MapGet("/", () => "Sgart.it ciao");
// API Todo basate su TodoDbContext
app.MapGet("/todo", async (TodoDbContext db) => await db.Todos.ToListAsync());
app.MapGet("/todo/completed", async (TodoDbContext db) => await db.Todos.Where(x => x.Completed == true).ToListAsync());
// /todo/text/contains?text=ciao
app.MapGet("/todo/contains", async (string text, TodoDbContext db) =>
await db.Todos
.Where(x => x.Text != null && x.Text.Contains(text, StringComparison.InvariantCultureIgnoreCase))
.ToListAsync());
app.MapGet("/todo/{todoId}", async (int todoId, TodoDbContext db) =>
await db.Todos.FindAsync(todoId) is Todo todo
? Results.Ok(todo)
: Results.NotFound());
app.MapPost("/todo", async (TodoInputDTO inputTodo, TodoDbContext db) =>
{
// validare sempre i parametri di ingresso
if (string.IsNullOrWhiteSpace(inputTodo.Text))
return Results.BadRequest("Invalid Text");
var todo = new Todo
{
Text = inputTodo.Text,
Completed = inputTodo.Completed
};
db.Todos.Add(todo);
await db.SaveChangesAsync();
return Results.Created($"/todo/{todo.TodoId}", todo);
});
app.MapPut("/todo/{id}", async (int id, TodoInputDTO inputTodo, TodoDbContext db) =>
{
var todo = await db.Todos.FindAsync(id);
if (todo is null)
return Results.NotFound();
// validare sempre i parametri di ingresso
if (string.IsNullOrWhiteSpace(inputTodo.Text))
return Results.BadRequest("Invalid Text");
todo.Text = inputTodo.Text;
todo.Completed = inputTodo.Completed;
await db.SaveChangesAsync();
return Results.NoContent();
});
app.MapDelete("/todo/{id}", async (int id, TodoDbContext db) =>
{
if (await db.Todos.FindAsync(id) is Todo todo)
{
db.Todos.Remove(todo);
await db.SaveChangesAsync();
return Results.Ok(todo);
}
return Results.NotFound();
});
app.Run();
}
catch (Exception exception)
{
// NLog: catch setup errors
logger.Error(exception, "Stopped program because of exception");
throw;
}
finally
{
// Ensure to flush and stop internal timers/threads before application-exit (Avoid segmentation fault on Linux)
NLog.LogManager.Shutdown();
}
// volendo posso usare direttamente la classe Todo senza creare un record
record TodoInputDTO(string Text, bool Completed);
class Todo
{
public int TodoId { get; set; }
public string? Text { get; set; }
public bool Completed { get; set; }
};
// creo il context per il DB di EF
class TodoDbContext : DbContext
{
public TodoDbContext(DbContextOptions<TodoDbContext> options)
: base(options) { }
public DbSet<Todo> Todos => Set<Todo>();
}
Questo esempio torna utile per fare velocemente dei progetti API JSON.
L'esempio completo è disponibile anche su Git Hub - Sgart.Net.MinimalAPI.
Vedi anche .NET 6 minimal web API.