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 memory
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
C#: Program.cs
using Microsoft.EntityFrameworkCore;
poi il servizio DbContext (dopo la variabile builder)
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 database
C#: Program.cs
class Todo
{
public int TodoId { get; set; }
public string? Text { get; set; }
public bool Completed { get; set; }
};
e il DbContext
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 scenario
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
DOS / Batch file
dotnet tool update --global dotnet-ef
se tutto va a buon fine viene visualizzato un messaggio simile
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.
API completa
Il codice completo dell'API con Entity Framework e supporto di NLog è questo
C#: 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.