Understanding HTTP
Before diving into REST, you should have a solid understanding of the Hypertext Transfer Protocol (HTTP). HTTP was created in 1989 by Tim Berners-Lee at CERN, the European Organization for Nuclear Rseearch. In the early days of computing, researchers would publish their findings in physical journals, which meant lengthy turnaround times from writing to publication. Berners-Lee envisioned a system where documents could be placed on servers and accessed through what we now call browsers.
Scientific papers contain numerous references, and being able to access related documents was crucial for researchers. The internet facilitated this through Hypertext Markup Language (HTML), where hypertext is an electronic document format containing links to other documents. Clicking a link takes you to another paper, and the browser's back button returns you to the original.
URIs and HTTP Methods
Browsers are applications that understand HTTP communication. The first step after opening a browser is entering a Uniform Resource Identifier (URI), which allows communication with a server. While URIs identify resources, you need HTTP methods to tell the server what action to perform on that URI.
The most commonly used method is GET. When you enter a URI in your browser, it executes a GET request on the server. Every time you click a hyperlink in an HTML document, the browser repeats this process with another URI.
Other methods exist for different operations. To submit a new document, you use the POST method to send data to the server along with a URI where it should be stored. To modify a document, such as correcting a typo, you use the PUT method, which overwrites the content at the specified URI. To remove a document, you use the DELETE method with its corresponding URI.
Note: Using GET, POST, PUT, and DELETE follows a convention. There are no strict rules requiring you to follow this pattern, and some REST services use different methods and status codes.
HTTP Status Codes
What happens when you request something from a server that doesn't exist? The server needs a way to communicate this. Beyond returning HTML, servers send status codes indicating the result. When a server successfully processes a request, it typically returns status code 200 (among other success codes—see the complete list at the Wikipedia link provided). When a server cannot find the requested resource, it returns status code 404, meaning "Not Found." The client receives this code and can respond appropriately. When a browser receives 200 ("OK"), it displays the HTML; when it receives 404, it shows a not-found screen; and so on.
Calling Server Functions with REST
Consider the methods discussed earlier. Using POST, you create something on the server. Using GET, you read it back. Using PUT, you update content on the server. Using DELETE, you remove content. These are known as CRUD operations (Create, Read, Update, Delete).
Roy Fielding, the inventor of REST, realized that the HTTP protocol could also handle data stored in databases. For example, using a GET method with the URI http://someserver/categories, the server could execute code to retrieve data from a categories table and return it. The server would use a format better suited for data transmission, such as XML or JSON. Since data comes in many formats, servers need a way to indicate what format they're sending. This is accomplished through HTTP headers.
HTTP Headers
HTTP headers are instructions exchanged between clients and servers. Headers are key-value pairs where both parties agree on the key names. Many standard HTTP headers exist (see the Wikipedia link for the complete list). For instance, servers can use the Content-Type header to tell clients what format to expect. The Accept header flows in the opposite direction—from client to server—politely requesting a specific format. This is called content negotiation. Currently, the most popular format is JavaScript Object Notation (JSON), which is the exchange format you'll use with Blazor.
JSON Format
JSON provides a compact format for transmitting data. Consider the example in the code block below.
{
"publication" : {
"title" : "Building Web Applications",
"chapters" : [ "Getting Started", "Working with Components"]
}
This JSON structure describes a publication that can easily be converted to an in-memory object. The simplest JSON object is a string like "Hello world!", but you can also create complex objects and arrays of JSON objects.
Objects use curly braces containing a comma-separated list of properties. Each property follows a key:value notation. The preceding example contains a publication object whose value is a nested JSON object with two properties: title and chapters. The title is a string "Building Web Applications." Property names are transmitted as strings as well. Finally, the chapters property is a string array, denoted by square brackets.
JSON is used for data transmission between machines but also for configuring tools, such as ASP.NET Core (look at appsettings.json in any server project). JSON is more popular than XML on the web today, largely due to its simplicity.
REST Call Examples
Suppose you need a list of pizzas from a server that exposes them at the URI http://someserver/pizza. To retrieve the list, use the GET method with an Accept header set to application/json to request JSON format.
Perhaps a customer wants to see details for a specific pizza with ID 5. The client can append the ID to the URI and execute a GET. If no pizza exists with that ID, the server returns status code 404.
As a final example, consider sending data from client to server. Suppose a customer has filled out all order details and clicked "Submit." You use the POST method to send the order as JSON to the server (remember, POST indicates insertion). The server can then process the order however it likes—inserting it into a database—and return 201: Created status code. REST recommends returning 201, with the Location header set to the URI of the newly created resource.
Building Simple Microservices with ASP.NET Core
Services and Single Responsibility
A service is software that listens for requests. When it receives one, the service processes it and returns a response. In the previous chapter, you built a menu service that could return a list of pizzas. Services in real-world scenarios work similarly. Consider a bank: you walk in, provide your account number and identification to a teller, and request $100. The teller checks your account; if you have sufficient funds, the teller deducts the amount and gives you cash. If your balance is too low, the teller refuses. In both cases, you receive a response.
Services should follow the single responsibility principle. They should do one thing well and nothing more. For instance, a pizza service allows clients to retrieve, add, update, and delete pizzas—nothing more. One responsibility: PIZZAS.
You can have other services, each with its own responsibility. Services that handle one thing are called microservices.
Pizza Service Implementation
Open the PizzaPlace solution you've been working with in previous chapters. This chapter focuses on the PizzaPlace.Server project, which currently hosts your Blazor client application. You'll enhance this project by adding microservices.
Open Startup.cs and examine the Configure method shown below.
public void Configure(IApplicationBuilder app,
IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseWebAssemblyDebugging();
}
else
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseBlazorFrameworkFiles();
app.UseStaticFiles();
app.UseRouting();
app.UseEndpoints(endpoints =>
{
endpoints.MapRazorPages();
endpoints.MapControllers();
endpoints.MapFallbackToFile("index.html");
});
}
The final line endpoints.MapFallbackToFile("index.html") handles your Blazor client project. Just before it, you see endpoints.MapControllers(), which enables hosting services.
In ASP.NET MVC, HTTP requests are routed to controller classes that inherit from ControllerBase. The controller then executes one of its methods. How does the framework decide which method? The MapControllers method examines the request URL, like /pizzas, and uses the first segment to find a controller with a matching name, such as PizzasController. Since this follows REST conventions, the selected controller chooses the method matching the HTTP verb. If your method is named Get(), it will be called, or you can apply an HttpGet attribute to the method. Method names don't matter when using attributes, and attributes allow you to specify the URL pattern, enabling parameters from the URL to be passed to the method.
The next section is the Controllers folder in the server project. Initially empty, this is where you place your service classes. In ASP.NET terminology, service classes are called controllers, hence the folder name.
In Visual Studio, right-click this folder and select Add > Controller. Choose API Controller - Empty and click Add. Name it PizzasController and click Add again. If using VS Code, right-click the Controllers folder, select Add File, and name it PizzasController.cs.
This creates a new class named PizzasController inheriting from ControllerBase, as shown below.
using Microsoft.AspNetCore.Mvc;
namespace PizzaPlace.Server.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class PizzasController : ControllerBase
{
}
}
The [ApiController] attribute tells the ASP.NET runtime this is a REST service controller. The [Route] attribute specifies the exposed URI as "api/pizzas". The "[controller]" placeholder in the route represents the controller name (Pizzas) without the "Controller" suffix.
Add a GET method to retrieve the pizza list. Currently, you'll hardcode the list, but in the next section, you'll fetch from a database. Modify PizzasController as follows.
using Microsoft.AspNetCore.Mvc;
using PizzaPlace.Shared;
using System.Collections.Generic;
using System.Linq;
namespace PizzaPlace.Server.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class PizzasController : ControllerBase
{
private static readonly List<Pizza> menuItems = new List<Pizza>
{
new Pizza(1, "Pepperoni", 8.99M, Spiciness.Spicy),
new Pizza(2, "Margarita", 7.99M, Spiciness.None),
new Pizza(3, "Diabolo", 9.99M, Spiciness.Hot)
};
[HttpGet("/pizzas")]
public IQueryable<Pizza> GetPizzas()
=> menuItems.AsQueryable();
}
}
Examine this implementation. First, a static hardcoded list of pizzas is declared. The GetPizzas method has the HttpGet("/pizzas") attribute, indicating that when a GET HTTP request arrives at the /pizzas URI, this method should be called. This attribute overrides the class-level Route attribute, so the default api/pizzas won't invoke GetPizzas.
The method returns IQueryable<Pizza>, which ASP.NET Core sends back to the client as a pizza list. The IQueryable<T> interface in .NET represents queryable data, such as database results returned by LINQ queries. Using IQueryable<Pizza> prepares for later database data retrieval.
Note that GetPizzas contains no code for transmitting data to the client—this is handled automatically by ASP.NET Core. By default, your implementation uses JSON, which is exactly what you need. ASP.NET Core supports other formats including custom ones. Clients request formats using the Accept header in requests, like XML or JSON. Here, we use the default JSON format.
Time to test. First, ensure PizzaPlace.Server is the startup project (right-click it and select "Set as Startup Project"). The project should appear in bold.
Run the project and wait for the browser to open. Change the URI in the browser to http://localhost:xxxx/pizzas, where xxxx is the port number in your address bar (the port may differ from other examples). You should see a JSON-encoded pizza list. The implementation works.
The JSON shows an array of objects, each containing properties from your Pizza class (except properties use lowercase, which is the JSON convention). Now you're ready to retrieve data from a real database using Entity Framework Core.
What Is Entity Framework Core?
Entity Framework Core is Microsoft's recommended framework for working with databases. EF is an object-relational mapper (ORM) that allows you to write classes as regular C# classes and then store and retrieve .NET objects from the database without being an SQL expert. It handles querying, inserting, updating, and deleting objects in the database. This concept is called persistence ignorance—your code doesn't need to know how or where data is stored. Entity Framework Core supports SQL Server, SQLite, and other database engines.
Code First Approach
You need to tell Entity Framework Core what data you want to store. Entity Framework Core uses a technique called Code First, where you write code describing your data and how it should be stored in the database. You can then generate the database, tables, and constraints. If you need to change the database, you use Code First Migrations to update the schema.
Using the Code First approach, you describe classes that map to database tables, called entities. You already have a Pizza class in PizzaPlace.Shared describing the Pizzas table in the database, but you need more configuration.
In this section, you'll use SQL Server, or if you don't have access to SQL Server, SQLite. If Visual Studio is installed on Windows, SQL Server is also installed.
To check for SQL Server: Launch Visual Studio and select View > SQL Server Object Explorer. Click "Add SQL Server." Expand the Local node. If SQL Server is installed, it should appear there.
Add Entity Framework Core to the PizzaPlace.Server project. In Visual Studio, right-click the server project and select "Manage NuGet Packages." NuGet is a practical way to install dependencies. It will install Microsoft.EntityFrameworkCore.SqlServer (or the SQLite variant) along with all its dependencies.
Select the Browse tab and search for "Microsoft.EntityFrameworkCore.SqlServer" (or "Microsoft.EntityFrameworkCore.Sqlite" if using SQLite). Select the top result (ensure "nuget.org" is selected as the package source in the NuGet window's top-right corner). Choose the latest stable version from the version dropdown and click Install.
From the command line, set the current folder to the PizzaPlace.Server project location and run:
dotnet add package Microsoft.EntityFrameworkCore.SqlServer
For SQLite, use:
dotnet add package Microsoft.EntityFrameworkCore.Sqlite
Add a new class named PizzaPlaceDbContext to the PizzaPlace.Server project, as shown below. This class represents the database and provides hints about how you want data stored in SQL Server (or another database engine—this code works the same).
using Microsoft.EntityFrameworkCore;
using PizzaPlace.Shared;
namespace PizzaPlace.Server
{
public class PizzaPlaceDbContext : DbContext
{
public PizzaPlaceDbContext(DbContextOptions<PizzaPlaceDbContext> options)
: base(options) { }
public DbSet<Pizza> Pizzas { get; set; } = default!;
protected override void OnModelCreating(
ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
var pizzaEntity = modelBuilder.Entity<Pizza>();
pizzaEntity.HasKey(p => p.Id);
pizzaEntity.Property(p => p.Name)
.HasMaxLength(80);
pizzaEntity.Property(p => p.Price)
.HasColumnType("money");
pizzaEntity.Property(p => p.Spiciness)
.HasConversion<string>();
}
}
}
First, create a constructor for PizzaPlaceDbContext that takes DbContextOptions<PizzaPlaceDbContext> as a parameter. This passes options and database server connection details, which you'll configure shortly.
Next, add a public property of type DbSet<Pizza> to add a table representing your pizzas to the database. DbSet<T> is the collection class Entity Framework Core uses to represent tables in the database—think of it as List<T>. One of EF Core's nice features is that you can use collections instead of writing SQL to communicate with the database.
Finally, override the OnModelCreating method, which takes a modelBuilder parameter. In this method, you describe how each DbSet<T> maps to the database—specifying the table name, column names, data types, etc. Here, you tell the model builder that the Pizza table should have a primary key on the Id property. You specify that the Name property should have a maximum of 80 characters and how the Price property maps to an SQL type (using MONEY). You also tell EF to use HasConversion<string>() to map the Spiciness enum to a string, resulting in readable spice levels instead of numbers. Many defaults are available, so you don't need to configure every property explicitly. For example, .NET's string type maps automatically to the database string type.
Preparing for Code First Migrations
Now configure the PizzaPlace.Server project to use SQL Server (or SQLite) as the database through dependency injection. In ASP.NET Core, you configure DI in the ConfigureServices method of the Startup class, shown below.
public void ConfigureServices(IServiceCollection services)
{
services.AddControllersWithViews();
services.AddRazorPages();
}
The IServiceCollection interface manages ASP.NET Core's dependencies. Services required by controllers and Razor pages are added here.
The Startup class has a constructor that provides access to project configuration files.
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
Add PizzaPlaceDbContext as a dependency in ConfigureServices. For SQL Server, add the following code to the end of ConfigureServices:
public void ConfigureServices(IServiceCollection services)
{
services.AddControllersWithViews();
services.AddRazorPages();
services.AddDbContext<PizzaPlaceDbContext>(options =>
options.UseSqlServer(
Configuration.GetConnectionString("PizzaPlaceDb")));
}
This tells ASP.NET Core you'll use PizzaPlaceDbContext stored in SQL Server. The code looks for a connection string in configuration, which you still need to add.
For SQLite, add this code instead:
public void ConfigureServices(IServiceCollection services)
{
services.AddControllersWithViews();
services.AddRazorPages();
services.AddDbContext<PizzaPlaceDbContext>(options =>
options.UseSqlite(
Configuration.GetConnectionString("PizzaPlaceDbLite")));
}
ASP.NET Core allows configuration settings in many locations: JSON files, environment variables, etc. Your server project already has an appsettings.json configuration file.
You need to add a connection string that enables database access. Connection strings tell your code where to find the database server, which database to use, and what credentials to login with. Update appsettings.json as shown below. This contains two connection strings—one for SQL Server and one for SQLite. The SQL Server connection string uses (localdb)\MSSQLLocalDB, which is a server installed with Visual Studio. The SQLite connection string is simpler, containing just the filename where SQLite will store data.
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
},
"AllowedHosts": "*",
"ConnectionStrings": {
"PizzaPlaceDb": "Server=(localdb)\\MSSQLLocalDB;Database=PizzaPlaceDb;
Trusted_Connection=True;MultipleActiveResultSets=true",
"PizzaPlaceDbLite": "Data Source=PizzaPlace.db"
}
}
Finding Your Database Server Connection String
If you're unsure which connection string to use, you can find SQL Server's connection string in Visual Studio through View > SQL Server Object Explorer.
Click the server icon with a green plus sign to connect to a database. Expand the Local, Network, or Azure nodes to find available database servers. The MSSQLLocalDB database server is recommended. Different servers may require different login methods. When ready, click Connect.
Next, expand the SQL Server node in SQL Server Object Explorer and select your server. Right-click and select Properties. Copy the connection string from the Properties window, paste it into appsettings.json, and change the database name to PizzaPlaceDb.
Creating Your First Code First Migration
You're almost ready to generate the database from code. First, add the Microsoft.EntityFrameworkCore.Design NuGet package to PizzaPlace.Server. This package is needed to perform Code First migrations.
Now create the Code First migration. A migration is a C# class containing changes needed to bring the database schema up (or down) to what the application requires. This is done using a tool called dotnet-ef.
Open the Package Manager Console from Visual Studio's menu (View > Other Windows > Package Manager Console), or use the command line if you prefer. If using VS Code, use the integrated terminal or open a command prompt.
Run the next command from the PizzaPlace.Server directory—the folder containing PizzaPlace.Server.csproj.
You may need to install the global dotnet-ef command-line tool. This tool generates migrations from code and updates the database when you're satisfied with them. Run this command once to install the tool:
dotnet tool install --global dotnet-ef
Now execute this command to create a migration:
dotnet-ef migrations add CreatingPizzaPlaceDb
Here, you use the dotnet-ef tool to add a migration named CreatingPizzaPlaceDb. You can choose any name for migrations, but pick something meaningful. You should see output like:
Build started...
Build succeeded.
Done. To undo this action, use 'ef migrations remove'
If you receive errors or warnings, review the code in your Pizza and PizzaPlaceDbContext classes (compare with the book's source code), ensure all Entity Framework packages use matching versions, and try again.
The tool creates a new Migrations folder in PizzaPlace.Server containing two files with different timestamps.
Open CreatingPizzaDb.cs to see what the tool generated. If using SQLite, see the alternative file provided.
namespace PizzaPlace.Server.Migrations
{
public partial class CreatingPizzaPlaceDb : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "Pizzas",
columns: table => new
{
Id = table.Column<int>(type: "int", nullable: false)
.Annotation("SqlServer:Identity", "1, 1"),
Name = table.Column<string>(type: "nvarchar(80)",
maxLength: 80, nullable: false),
Price = table.Column<decimal>(type: "money",
nullable: false),
Spiciness = table.Column<string>(type:
"nvarchar(max)", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Pizzas", x => x.Id);
});
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "Pizzas");
}
}
}
The migration class has two methods: Up and Down. The Up method upgrades the database schema, creating a new Pizzas table with Id, Name, Price, and Spiciness columns. The Down method downgrades by dropping the table. During development, you'll make incremental schema changes; each change becomes a migration. You can use these migrations to update the database or revert to previous states. You can also apply a series of changes to update a production database to match the development database schema.
Generating the Database
Now generate the database from the migration. In Visual Studio, return to the command line or Package Manager Console (View > Other Windows > Package Manager Console), or in VS Code, open the integrated terminal (View > Terminal). Ensure you're in the folder containing PizzaPlace.Server and type:
dotnet-ef database update
This creates your database. View it in SQL Server Object Explorer by opening View > SQL Server Object Explorer and expanding the PizzaPlaceDb database tree (you may need to refresh: right-click Databases and select Refresh).
Enhancing the Pizza Microservice
Add functionality to the Pizza microservice so it uses the dataabse instead of hardcoded data, and include a method to insert pizzas into the database.
Open the PizzasController class in the Controllers folder of PizzaPlace.Server. Add a constructor that takes PizzaPlaceDbContext as a parameter, as shown below.
using Microsoft.AspNetCore.Mvc;
using PizzaPlace.Shared;
using System.Collections.Generic;
using System.Linq;
namespace PizzaPlace.Server.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class PizzasController : ControllerBase
{
private readonly PizzaPlaceDbContext db;
public PizzasController(PizzaPlaceDbContext db)
{
this.db = db;
}
...
}
}
To communicate with the database, PizzasController needs a PizzaPlaceDbContext instance. As you learned earlier, you accomplish this through the constructor, which saves the reference in a local field.
Remove the hardcoded pizza list and update the GetPizza method to use PizzaPlaceDbContext instead. To retrieve all pizzas, simply use the Pizzas property of PizzaPlaceDbContext. Entity Framework accesses the database when you access the Pizzas property and returns all rows from the Pizza table. Also remove the Route attribute since the GetPizzas method specifies its own URL.
using Microsoft.AspNetCore.Mvc;
using PizzaPlace.Shared;
using System.Collections.Generic;
using System.Linq;
namespace PizzaPlace.Server.Controllers
{
[ApiController]
public class PizzasController : ControllerBase
{
private readonly PizzaPlaceDbContext db;
public PizzasController(PizzaPlaceDbContext db)
{
this.db = db;
}
[HttpGet("/pizzas")]
public IQueryable<Pizza> GetPizzas() => db.Pizzas;
}
}
Add a method to insert new pizzas into the database. Add the InsertPizza method to PizzasController as shown below. This method receives a pizza instance from the client as the POST request body, so you add the HttpPost attribute with the URI to post to. The pizza object travels in the request body, which is why the InsertPizza method's pizza parameter has the FromBody attribute to tell ASP.NET MVC Core to convert the request body into a Pizza instance. The method adds the pizza to the PizzaPlaceDbContext Pizzas table and uses SaveChanges to persist it to the database. Then InsertPizza returns a 201 Created status code with the pizza's URI as the response, following REST conventions.
You can return many possible HTTP status codes from controller methods. Some of the most common have helper methods for easy return: Ok(), NotFound(), and so on. Here, you return 201 – Created. In the next section, you'll use Postman to verify this response.
using Microsoft.AspNetCore.Mvc;
using PizzaPlace.Shared;
using System.Collections.Generic;
using System.Linq;
namespace PizzaPlace.Server.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class PizzasController : ControllerBase
{
private readonly PizzaPlaceDbContext db;
public PizzasController(PizzaPlaceDbContext db)
{
this.db = db;
}
[HttpGet("/pizzas")]
public IQueryable<Pizza> GetPizzas()
=> db.Pizzas;
[HttpPost("/pizzas")]
public IActionResult InsertPizza([FromBody] Pizza pizza)
{
db.Pizzas.Add(pizza);
db.SaveChanges();
return Created($"pizzas/{pizza.Id}", pizza);
}
}
}
This introduction to REST services shows how to build basic functionality. Building production-grade services with all the different methods and best practices could fill an entire book. The goal here is to get you up and running with the fundamentals.