.NET .NET Core C# Entity Framework Core Visual Studio Web API

Using Entity Framework Core within a .NET Core Minimal API

Welcome to today’s post.

In today’s post I will show how to use entity framework within a .NET Core 6 minimal web API application.

In a previous post I showed how to create a basic minimal web API application with .NET Core 6.

I will be showing how to create the following variations of a minimal web API application:

  1. With an entity framework core in-memory data context.
  2. With an entity framework core SQL server data context.

With the in-memory approach, the purpose is to implement a data-aware API that performs logically identically to the SQL data aware version, but without the overheads of the physical data dependency, and lightweight enough to run basic tests to confirm its validity.

In both cases, we can run manual tests against the API REST methods using a number of tools. The most obvious one within our development environment is the Swagger Open API interface. Other common tools we can use is the curl command or the popular POSTMAN API test tool.

Basic In-Memory Data Access

To implement an in-memory entity framework minimal web API on top of our existing minimal Web API, we first install the Nuget package manager library Microsoft.EntityFrameworkCore.InMemory:

Once the package is installed, you can declare the library at the top of the source file as shown:

using Microsoft.EntityFrameworkCore;

The option UseInMemoryDatabase() is exposed when using the AddDbContext() application builder extension method. This allows us to configure an in-memory database as shown:

builder.Services.AddDbContext<BookLoanDb>(opt => 
    opt.UseInMemoryDatabase("BookLoan")
);

To implement the HTTP GET, POST, PUT and DELETE REST API calls as endpoints, we can make use of the application builder MapXXX() methods, where XXX = GET, POST, PUT and DELETE.

Before we can do this, we will need to setup two dependencies:

  1. A data context derived from DbContext.
  2. At least one DTO class definition used to hold data from the data context.

A typical DTO (or model view class) that will hold data from the data context can be declared as shown:

public class BookViewModel
{
    public int ID { get; set; }
    public string Title { get; set; }
    public string Author { get; set; }
    public int YearPublished { get; set; }
    public string Genre { get; set; }
    public string Edition { get; set; }
    public string ISBN { get; set; }
    public string Location { get; set; }
    public string MediaType { get; set; }
    public DateTime DateCreated { get; set; }
    public DateTime DateUpdated { get; set; }
}

The data context is quite straightforward, and can be declared as shown:

class BookLoanDb : DbContext
{
    public BookLoanDb(DbContextOptions<BookLoanDb> options)
        : base(options) { }

    public DbSet<BookViewModel> Books => Set<BookViewModel>();
}

As we can see, we need to declare the DTO / data model before the data context in the same source file or include the DTO class as a using declaration within the source for the data context.

The implementation of MapGet() method HTTP endpoint for the HTTP GET request like that shown:

https://localhost:7013/bookitems

is shown below:

app.MapGet("/bookitems", async (BookLoanDb db) =>
{
    var books = await db.Books.ToListAsync();
    if (books is null)
        return Results.NotFound();
    return Results.Ok(books);
});

The implementation of the MapGet() method HTTP endpoint for the HTTP GET request like that shown:

https://localhost:7013/bookitems/1

is shown below:

app.MapGet("/bookitems/{id}", async (int id, BookLoanDb db) =>
{
    var book = await db.Books.FindAsync(id);
    if (book is null)
        return Results.NotFound();
    if (book is BookViewModel)
        return Results.Ok(book);
    return Results.NotFound();
});

The implementation of the MapPost() method HTTP endpoint for the HTTP POST request like that shown:

https://localhost:7013/bookitems

with body (payload) of:

{
    "title": "Test 1",
    "author": "J. Bloggs",
    "yearPublished": 2023,
    "genre": "Fiction",
    "edition": "1",
    "isbn": "AABBCC112233",
    "location": "Sydney",
    "mediaType": "Book",
    "dateCreated": "2023-02-06T13:37:24.605Z",
    "dateUpdated": "2023-02-06T13:37:24.605Z"
}

is shown below:

app.MapPost("/bookitems", async (BookViewModel book, BookLoanDb db) =>
{
    db.Books.Add(book);
    await db.SaveChangesAsync();

    return Results.Created($"/bookitems/{book.ID}", book);
});

With POST, we will get a response code of 201.

Note, depending on each HTTP handler, we have passed in a parameters corresponding to the DTO/model BookViewModel and/or the data context BookLoanDb.

The implementation of MapPut() method HTTP endpoint for the HTTP PUT request like that shown:

https://localhost:7013/bookitems/1

with body (payload) of:

{
    "title": "Test 1",
    "author": "J. Bloggs",
    "yearPublished": 2023,
    "genre": "Fiction",
    "edition": "1",
    "isbn": "SSSSCC112233",
    "location": "Sydney",
    "mediaType": "Book",
    "dateCreated": "2023-02-06T13:37:24.605Z",
    "dateUpdated": "2023-02-06T13:37:24.605Z"
}

is shown below:

app.MapPut("/bookitems/{id}", async (int id, BookViewModel inBook, BookLoanDb db) =>
{
    var book = await db.Books.FindAsync(id);

    if (book is null)
        return Results.NotFound();

    book.ISBN = inBook.ISBN;
    book.Author = inBook.Author;
    book.Edition = inBook.Edition;
    book.Genre = inBook.Genre;
    book.Location = inBook.Location;
    book.MediaType = inBook.MediaType;
    book.Title = inBook.Title;
    book.YearPublished = inBook.YearPublished;
    book.DateUpdated = DateTime.Now;

    await db.SaveChangesAsync();

    return Results.NoContent();
});

In the above request, we just updated the isbn field with the new value from the corresponding isbn field posted payload.

With PUT, we will get a response code of 204.

The implementation of MapDelete() method HTTP endpoint for the HTTP DELETE request like that shown:

https://localhost:7013/bookitems/1

is shown below:

app.MapDelete("/bookitems/{id}", async (int id, BookLoanDb db) =>
{
    var book = await db.Books.FindAsync(id);

    if (book is null)
        return Results.NotFound();

    if (book is BookViewModel)
    {
        db.Books.Remove(book);
        await db.SaveChangesAsync();
        return Results.Ok(book);
    }
    return Results.NotFound();
});

With DELETE, we will get a response code of 200.

When we run our minimal API with the above endpoints implemented, the definitions of each HTTTP endpoint will show up in the Swagger UI as shown:

When we run the in-memory implementation of entity framework core, initially there will be no records in our data context, unless of course we load our data with sample data on application startup.

If we were to execute a POST with a record as shown using the Swagger interface:

Then the default logs in the developer console will update whenever a record is added to the entity within the data context:

And the Swagger interface will show us the response from the server with a response code 201 as shown:

With our in-memory data context, if we were to execute another POST with similar data, then follow up with a GET on all data, the output response will show two records as shown:

I will now show how to setup the minimal web API to execute HTTP requests against a data context from a SQL server data store.

SQL Server Data Access

To install the NuGet package for the entity framework core provider for SQL server data, we search for “microsoft.entityframework.sql”, which will give us the package Microsoft.EntityFrameworkCore.SqlServer:

Once the above NuGet package has been installed, make the following changes and additions to the minimal web API code.

Below the following line we used to create the builder for our web application:

var builder = WebApplication.CreateBuilder(args);

Retrieve the configuration settings using the following line:

var config = builder.Configuration;

Replace the in-memory line containing the extension method UseInMemoryDatabase() with the following line of code that will retrieve the connection string from the configuration settings file and initialise the credentials of the SQL server data context:

builder.Services.AddDbContext<BookLoanDb>(opt => opt.UseSqlServer(
    config.GetConnectionString("AppDbContext"))
);

We will also need to add a connection string within the appSettings.json file as shown:

{
  "ConnectionStrings": {
    "AppDbContext": "Server=localhost;Database=aspnet-BookCatalog;User Id=????;Password=????;Trust Server Certificate=true;"
  }
}

Note, that if you have installed a recent version of Visual Studio like (Visual Studio 2022) that updates the data providers for SQL, you will find that you may have to add one of the following properties to the connection string:

Trust Server Certificate=true 

OR

Use Encryption for Data=Optional

Otherwise, you will get the following error when opening the connection to the SQL data context:

provider: SSL Provider, error: 0 – The certificate chain was issued by an authority that is not trusted.

Once the above is setup and the application is run, the only difference you will notice is the loading speed and speed of data context query operations. These will be noticeably slower than a faster in-memory data context.

We have seen how straightforward it is to setup a data-aware minimal web API using .NET 6 with entity framework core, and how to run some basic tests using the built-in Swagger UI.  

That is all for today’s post.

I hope you have found this post useful and informative.

Social media & sharing icons powered by UltimatelySocial