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

DOS / 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 base

C#: Program.cs

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

DOS / 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 riga

C#: Program.cs

builder.Services.AddDbContext<TodoDbContext>(opt => opt.UseInMemoryDatabase("SgartTodoList"));
con

C#: Program.cs

builder.Services.AddDbContext<TodoDbContext>(option =>
{
    option.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection"));
});
e ovviamente aggiungiamo la connection string sul file appsettings.json

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"
  },
  ...
}
infine creiamo la migration iniziale

DOS / Batch file

dotnet ef migrations add [migration name]
Migrations
Migrations
e creiamo/aggiorniamo il database

DOS / Batch file

dotnet ef database update
CreateDB
CreateDB

Errori

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

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.

L'esempio completo è disponibile anche su Git Hub - Sgart.Net.MinimalAPI.

Vedi anche .NET 6 minimal web API.
Tags:
.NET 65 Entity framework4 .NET66 .NET Core26 SQL Server100 Database75
Potrebbe interessarti anche: