In this chapter we'll walk through creating a "Coffee Shop" Web API in ASP.NET Core. When it's complete our API will expose resources for Coffee
and BeanVariety
.
- Coffee - https://localhost:5001/api/coffee
- BeanVariety - https://localhost:5001/api/beanvariety
Review and run this SQL script to create the CoffeeShop
database.
In this chapter we'll be focused on the BeanVariety
entity and you'll work with the Coffee
entity in the exercise.
CREATE TABLE BeanVariety (
Id INTEGER NOT NULL PRIMARY KEY IDENTITY,
[Name] VARCHAR(50) NOT NULL,
Region VARCHAR(255) NOT NULL,
Notes TEXT
);
- Open Visual Studio
- Select "Create a new project"
- In the "Create a new project" dialog, choose the C# "ASP.NET Core web application" option
- Name the project "CoffeeShop"
- In the "Create a new ASP.NET Core web application" dialog choose "API"
- In Solution Explorer, right click the name of the project and select "Manage Nuget Packages". Install the
Microsoft.Data.SqlClient
pacakge
You now have an ASP.NET Core Web API project. Spend some time looking around the code that Visual Studio generated. You'll find several familiar items.
As in an MVC project, a Web API project has an appsettings.json
file to store configuration information for the app. Update the appsettings.json
file to contain the database connection string.
appsettings.json
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
},
"AllowedHosts": "*",
"ConnectionStrings": {
"DefaultConnection": "server=localhost\\SQLExpress;database=CoffeeShop;integrated security=true"
}
}
Models (a,k.a data models) in Web API are exactly the same as in MVC. They are simple classes containing properties that correspond to columns in a database table. We can even use the same DataAnnotations
as we used in MVC.
Create a Models
folder and add a BeanVariety
class.
Models/BeanVariety.cs
using System.ComponentModel.DataAnnotations;
namespace CoffeeShop.Models
{
public class BeanVariety
{
public int Id { get; set; }
[Required]
[StringLength(50, MinimumLength = 3)]
public string Name { get; set; }
[Required]
[StringLength(255, MinimumLength = 3)]
public string Region { get; set; }
public string Notes { get; set; }
}
}
We can use the Repository Pattern when building a Web API just as we did with MVC and console applications.
Create a Repositories
directory and a BeanVarietyRepository
class.
Repositories/BeanVarietyRepository.cs
using System;
using System.Collections.Generic;
using Microsoft.Data.SqlClient;
using Microsoft.Extensions.Configuration;
using CoffeeShop.Models;
namespace CoffeeShop.Repositories
{
public class BeanVarietyRepository
{
private readonly string _connectionString;
public BeanVarietyRepository(IConfiguration configuration)
{
_connectionString = configuration.GetConnectionString("DefaultConnection");
}
private SqlConnection Connection
{
get { return new SqlConnection(_connectionString); }
}
public List<BeanVariety> GetAll()
{
using (var conn = Connection)
{
conn.Open();
using (var cmd = conn.CreateCommand())
{
cmd.CommandText = "SELECT Id, [Name], Region, Notes FROM BeanVariety;";
var reader = cmd.ExecuteReader();
var varieties = new List<BeanVariety>();
while (reader.Read())
{
var variety = new BeanVariety()
{
Id = reader.GetInt32(reader.GetOrdinal("Id")),
Name = reader.GetString(reader.GetOrdinal("Name")),
Region = reader.GetString(reader.GetOrdinal("Region")),
};
if (!reader.IsDBNull(reader.GetOrdinal("Notes")))
{
variety.Notes = reader.GetString(reader.GetOrdinal("Notes"));
}
varieties.Add(variety);
}
reader.Close();
return varieties;
}
}
}
public BeanVariety Get(int id)
{
using (var conn = Connection)
{
conn.Open();
using (var cmd = conn.CreateCommand())
{
cmd.CommandText = @"
SELECT Id, [Name], Region, Notes
FROM BeanVariety
WHERE Id = @id;";
cmd.Parameters.AddWithValue("@id", id);
var reader = cmd.ExecuteReader();
BeanVariety variety = null;
if (reader.Read())
{
variety = new BeanVariety()
{
Id = reader.GetInt32(reader.GetOrdinal("Id")),
Name = reader.GetString(reader.GetOrdinal("Name")),
Region = reader.GetString(reader.GetOrdinal("Region")),
};
if (!reader.IsDBNull(reader.GetOrdinal("Notes")))
{
variety.Notes = reader.GetString(reader.GetOrdinal("Notes"));
}
}
reader.Close();
return variety;
}
}
}
public void Add(BeanVariety variety)
{
using (var conn = Connection)
{
conn.Open();
using (var cmd = conn.CreateCommand())
{
cmd.CommandText = @"
INSERT INTO BeanVariety ([Name], Region, Notes)
OUTPUT INSERTED.ID
VALUES (@name, @region, @notes)";
cmd.Parameters.AddWithValue("@name", variety.Name);
cmd.Parameters.AddWithValue("@region", variety.Region);
if (variety.Notes == null)
{
cmd.Parameters.AddWithValue("@notes", DBNull.Value);
}
else
{
cmd.Parameters.AddWithValue("@notes", variety.Notes);
}
variety.Id = (int)cmd.ExecuteScalar();
}
}
}
public void Update(BeanVariety variety)
{
using (var conn = Connection)
{
conn.Open();
using (var cmd = conn.CreateCommand())
{
cmd.CommandText = @"
UPDATE BeanVariety
SET [Name] = @name,
Region = @region,
Notes = @notes
WHERE Id = @id";
cmd.Parameters.AddWithValue("@id", variety.Id);
cmd.Parameters.AddWithValue("@name", variety.Name);
cmd.Parameters.AddWithValue("@region", variety.Region);
if (variety.Notes == null)
{
cmd.Parameters.AddWithValue("@notes", DBNull.Value);
}
else
{
cmd.Parameters.AddWithValue("@notes", variety.Notes);
}
cmd.ExecuteNonQuery();
}
}
}
public void Delete(int id)
{
using (var conn = Connection)
{
conn.Open();
using (var cmd = conn.CreateCommand())
{
cmd.CommandText = "DELETE FROM BeanVariety WHERE Id = @id";
cmd.Parameters.AddWithValue("@id", id);
cmd.ExecuteNonQuery();
}
}
}
}
}
NOTE: Look closely at the code above. Do you notice anything different? Yes, we're using the "var" keyword. Happy day!
-
Use the
Extract Interface...
feature of Visual Studio to create theIBeanVarietyRepository
interface. -
Update the
ConfigureServices
method in theStartup
class to register your new repository with ASP.NET.services.AddTransient<IBeanVarietyRepository, BeanVarietyRepository>();
Controllers in Web API are similar to controllers in MVC with a few small differences. They perform the same function in MVC. As in MVC a Web API controller contains methods to respond to HTTP requests.
Create a BeanVarietyController
class in the Controllers
directory.
Controllers/BeanVarietyController.cs
using Microsoft.AspNetCore.Mvc;
using CoffeeShop.Models;
using CoffeeShop.Repositories;
namespace CoffeeShop.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class BeanVarietyController : ControllerBase
{
private readonly IBeanVarietyRepository _beanVarietyRepository;
public BeanVarietyController(IBeanVarietyRepository beanVarietyRepository)
{
_beanVarietyRepository = beanVarietyRepository;
}
// https://localhost:5001/api/beanvariety/
[HttpGet]
public IActionResult Get()
{
return Ok(_beanVarietyRepository.GetAll());
}
// https://localhost:5001/api/beanvariety/5
[HttpGet("{id}")]
public IActionResult Get(int id)
{
var variety = _beanVarietyRepository.Get(id);
if (variety == null)
{
return NotFound();
}
return Ok(variety);
}
// https://localhost:5001/api/beanvariety/
[HttpPost]
public IActionResult Post(BeanVariety beanVariety)
{
_beanVarietyRepository.Add(beanVariety);
return CreatedAtAction("Get", new { id = beanVariety.Id }, beanVariety);
}
// https://localhost:5001/api/beanvariety/5
[HttpPut("{id}")]
public IActionResult Put(int id, BeanVariety beanVariety)
{
if (id != beanVariety.Id)
{
return BadRequest();
}
_beanVarietyRepository.Update(beanVariety);
return NoContent();
}
// https://localhost:5001/api/beanvariety/5
[HttpDelete("{id}")]
public IActionResult Delete(int id)
{
_beanVarietyRepository.Delete(id);
return NoContent();
}
}
}
One key difference is a Web API controller uses all the familiar HTTP verbs that json-server
used.
GET
for retrieving one or more entitiesPOST
for creating a new entityPUT
for updating an entityDELETE
for removing an entity
In the controller above note that we decorate each method (a.k.a. controller action) with an attribute that denotes the HTTP verb that method responds to.
// https://localhost:5001/api/beanvariety/
[HttpGet]
public IActionResult Get() { /* omitted */ }
// https://localhost:5001/api/beanvariety/5
[HttpGet("{id}")]
public IActionResult Get(int id) { /* omitted */ }
// https://localhost:5001/api/beanvariety/
[HttpPost]
public IActionResult Post(BeanVariety beanVariety) { /* omitted */ }
// https://localhost:5001/api/beanvariety/5
[HttpPut("{id}")]
public IActionResult Put(int id, BeanVariety beanVariety) { /* omitted */ }
// https://localhost:5001/api/beanvariety/5
[HttpDelete("{id}")]
public IActionResult Delete(int id) { /* omitted */ }
Some of the [HttpXXX]
attributes refer to {id}
. The id
in this case says this method expects the URL to contain a route parameter with the bean variety's id
. For example in order to delete the bean variety with an id
of 42
we would make a DELETE
request to this URl:
You'll also note that, unlike MVC, we don't have two methods for creating, editing or deleting entities. This is because Web API does not have the concept of Views, so there are no forms to present to the user.
Also, since there is no View, you won't see a call the the View()
method as we did in MVC. Instead you'll see a few other methods. Two common methods are Ok()
and NoContent()
. Ok()
is used when we want to return data. NoContent()
is used to indicate that the action was successful, but we don't have any data to return.
Some final differences from an MVC controller can be seen at the top of the class. We must decorate a Web API controller with a couple of attributes and the controller class should inherit from the ControllerBase
class instead of Controller
.
[Route("api/[controller]")]
[ApiController]
public class BeanVarietyController : ControllerBase
To test a Web API we use the popular Postman tool.
The whole point of building an API is to provide an interface for other code to call into, so it makes perfect sense that we'd want to call our web API from JavaScript.
Create a directory called js-app
in the root directory of your solution (the directory that contains the CoffeeShop.sln
file). Add the following HTML and JavaScript files to the js-app
directory.
js-app/index.html
<!DOCTYPE html>
<html lang="en">
<head>
<title>Coffee Shop</title>
</head>
<body>
<button id="run-button">Run It!</button>
<script src="main.js"></script>
</body>
</html>
js-app/main.js
const url = "https://localhost:5001/api/beanvariety/";
const button = document.querySelector("#run-button");
button.addEventListener("click", () => {
getAllBeanVarieties()
.then(beanVarieties => {
console.log(beanVarieties);
})
});
function getAllBeanVarieties() {
return fetch(url).then(resp => resp.json());
}
Run the app with serve
npx serve -l 3000 .
NOTE: The default port for
serve
is5000
, but our an ASP.NET app is already running on ports5000
and5001
, so we use the-l
(a.k.a. listen) flag to tellserve
to use port3000
.
Open the console and then click the Run It!
button. What do you see?
That's right...a "CORS" error.
Access to fetch at 'https://localhost:5001/api/beanvariety/' from origin 'http://localhost:3000' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource. If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled.
CORS is a browser security feature that prevents JavaScript from talking to APIs without the web server's consent. CORS is extremely important for production applications, but in development we can afford to be a bit more lax. Update the Configure
method in the Startup
class to call app.UseCors()
to configure CORS behavior.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseDeveloperExceptionPage();
// Do not block requests while in development
app.UseCors(options =>
{
options.AllowAnyOrigin();
options.AllowAnyMethod();
options.AllowAnyHeader();
});
}
app.UseHttpsRedirection();
app.UseRouting();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}
Return to the test web page and click the button again. You should see bean variety data print in the console.
- Create the CoffeeShop project outlined in this chapter.
- Create the necessary classes (model, repository and controller) to implement full CRUD functionality for the
/api/coffee
endpoint. Use Postman to test the endpoint.- NOTE: Your
Coffee
model should contain bothBeanVarietyId
andBeanVariety
properties.
- NOTE: Your
- Update the JavaScript and HTML to display all bean varieties in the DOM when the "Run It!" button is clicked.
- Update the JavaScript and HTML with a form for adding a new bean variety to the database.
- Update the JavaScript and HTML to allow a user to perform full CRUD functionality for the Coffee resource.