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.
NuGet
Dopo aver
creato il progetto, la prima attività da fare è quella di aggiungere i necessari pacchetti
NuGet per il supporto
in memorydotnet 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
base var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/", () => "Sgart.it ciao");
app.Run();
e si aggiunge lo
using del
namespaces all'inizio
using Microsoft.EntityFrameworkCore;
poi il servizio
DbContext (dopo la variabile
builder)
// 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
databaseclass Todo
{
public int TodoId { get; set; }
public string? Text { get; set; }
public bool Completed { get; set; }
};
e il
DbContextclass 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
// 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 scenario
dotnet add package Microsoft.EntityFrameworkCore.SqlServer --version 6.0.6
dotnet add package Microsoft.EntityFrameworkCore.Design --version 6.0.6
e sostituiamo la riga
builder.Services.AddDbContext<TodoDbContext>(opt => opt.UseInMemoryDatabase("SgartTodoList"));
con
builder.Services.AddDbContext<TodoDbContext>(option =>
{
option.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection"));
});
e ovviamente aggiungiamo la
connection string sul file
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"
},
...
}
infine creiamo la
migration iniziale
dotnet ef migrations add [migration name]
Migrationse creiamo/aggiorniamo il
databasedotnet ef database update
CreateDBErrori
Se compare un errore simile a questo
The 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.
esegui
dotnet tool update --global dotnet-ef
se tutto va a buon fine viene visualizzato un messaggio simile
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.
API completa
Il codice completo dell'API con
Entity Framework e supporto di
NLog è questo
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.