Intro
This is part 1 in a series where I will be covering various aspects of building a Web API using the newest version of ASP.NET Core.
In this part we will look at building a basic API for a fictive online store. In the following parts we will look at things like:
- Authentication and authorization
- Error logging
- Implementing HATEOAS
The complete basic API from this article can be found on GitHub: https://github.com/juunas11/AspNetCoreApiExample/tree/basic-api.
My setup
- Visual Studio 2015 Update 3
- .NET Core 1.0.1 - VS 2015 Tooling Preview 2
The scenario
Electronics Co is a company that sells various household electronics. They are building a new online store, and want it to be implemented as a nice modern client-heavy application. To support it, they need an API that will give access to the data.
Data model
All entities in the model will have a sequential id as their primary key. It is quite a simple model, with some relations.
Products
- Name
- Category
- Price
Orders
- Customer name, address, and email
- Time the order was made at
- 1-N Order rows
Order rows
- Quantity
- Single product price
- 1-1 Product
Creating the project in Visual Studio
After opening Visual Studio:
- Go to File -> New -> Project...
- From the Web category, pick ASP.NET Core Web Application (.NET Core)
- When asked, pick the Web API template
- Feel free to delete the ValuesController and the Project Readme File
Adding necessary dependencies to the project
We need to add some libraries to the project, including EF Core for talking with the database. I added the following to project.json's dependencies section:
"Microsoft.EntityFrameworkCore": "1.0.1",
"Microsoft.EntityFrameworkCore.SqlServer": "1.0.1",
"Microsoft.EntityFrameworkCore.Tools": {
"version": "1.0.0-preview2-final",
"type": "build"
}
I also added the following to the tools section:
"Microsoft.EntityFrameworkCore.Tools": "1.0.0-preview2-final"
Add the development settings file
We are going to use a local SQL Server database here, so I want a settings file where I can define the local database connection string.
For this purpose I created an appsettings.development.json
file:
{
"ConnectionStrings": {
"DefaultConnection": "Data Source=.;Initial Catalog=ElectronicsStoreDevDb;Integrated Security=True"
}
}
If you wanted, you could also store this connection string in user secrets. It may be a good idea if different people on the team want to have a different connection string for their development database.
Define the data model
Based on my basic design for the data model, I defined classes for each of them. Here is the Order class for example:
public class Order
{
[Key]
public long Id { get; set; }
[Required]
public string CustomerName { get; set; }
[Required]
public string CustomerAddress { get; set; }
[Required]
public string CustomerEmail { get; set; }
public DateTimeOffset CreatedAt { get; set; }
public virtual ICollection<OrderRow> Rows { get; set; }
public Order()
{
}
}
You can refer to the GitHub repo for the other classes.
Define the DbContext
After defining the basic domain classes, you need to define a class
which inherits from the DbContext
class. Mine looks like this:
public class StoreDataContext : DbContext
{
public StoreDataContext(DbContextOptions<StoreDataContext> options)
: base(options)
{
}
public DbSet<Order> Orders { get; set; }
public DbSet<Product> Products { get; set; }
public DbSet<OrderRow> OrderRows { get; set; }
}
The constructor defined here will be important soon.
Modify Startup.cs
We need to the DbContext configuration to Startup.cs's ConfigureServices function. You just need to add a line similar to this:
services.AddDbContext<StoreDataContext>(opts =>
opts.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));
This makes your DbContext available for dependency injection across the app, as well as configuring its connection string.
Creating a database migration
Now that we have everything ready, we can generate our database. Just open Package Manage Console in Visual Studio, and run the command:
Add-Migration Initial
This generates a migration named Initial that defines how the database schema should change to suit the current data model. So it defines what tables to create and so on.
We could run a command to run it, but I prefer my migrations automatic. That's why I modified Startup.cs's Configure function slightly:
public void Configure(
IApplicationBuilder app,
IHostingEnvironment env,
ILoggerFactory loggerFactory,
StoreDataContext db)
{
loggerFactory.AddConsole(Configuration.GetSection("Logging"));
loggerFactory.AddDebug();
db.Database.Migrate();
app.UseMvc();
}
Here we inject the database context into the function, which allows
us to call db.Database.Migrate()
. This automatically runs any
pending migrations upon startup.
Defining the data repository layer
We need a layer of abstraction in the app in the form of the
repository so that the other layers do not need to interact with
e.g. Entity Framework. Here is a snippet from the OrderRepository
:
public class OrderRepository : IOrderRepository
{
private readonly StoreDataContext _db;
public OrderRepository(StoreDataContext db)
{
_db = db;
}
public Order CreateOrder(Order order)
{
order.CreatedAt = DateTimeOffset.Now;
_db.Orders.Add(order);
_db.SaveChanges();
return order;
}
public void DeleteOrder(long id)
{
Order order = GetOrder(id);
if(order != null)
{
_db.Orders.Remove(order);
_db.SaveChanges();
}
}
public List<Order> GetAllOrders()
{
return _db.Orders.AsNoTracking().ToList();
}
public Order GetOrder(long id)
{
return _db.Orders.FirstOrDefault(o => o.Id == id);
}
//Some functions left out for brevity
}
Here I also defined interfaces for the repositories so I could
make them available for dependency injection like so (in ConfigureServices
):
services.AddScoped<IProductRepository, ProductRepository>();
services.AddScoped<IOrderRepository, OrderRepository>();
Defining the controllers
Now that the data access layer is ready, we can finally work on the controllers that give others access to the data.
A typical pattern for defining a controller in Web API is:
[Route("api/[controller]")]
public class OrdersController : Controller
{
}
Controller
is used as the base class for all controllers in ASP.NET Core,
though it is not mandatory. But it provides a lot of useful utilities.
The RouteAttribute
at the top defines that all actions in this controller
will answer to requests starting with api/orders.
Note that [controller] is replaced by the name of the controller class minus the word controller.
We can then define the constructor for the controller, taking the repository from DI:
private readonly IOrderRepository _orders;
public OrdersController(IOrderRepository orderRepository)
{
_orders = orderRepository;
}
A simple action to return all of the orders is then easy to define:
[HttpGet("")]
public IActionResult GetAllOrders()
{
List<Order> orders = _orders.GetAllOrders();
return Ok(orders);
}
The HttpGetAttribute
here filters out any request that is not a GET.
We can also define the route template in its constructor. Here we
have defined it as an empty string, so this action will be hit when:
- The request is a GET request
- The URL is /api/orders
If you run the project at this stage, you should be able to get a proper response back. There might not be any data yet, so you will get an empty array back.
The action for getting a single order can be defined like this:
[HttpGet("{id}")]
public IActionResult GetOrder(long id)
{
Order order = _orders.GetOrder(id);
if (order == null)
{
return NotFound();
}
return Ok(order);
}
Note the route template. This time we define it as {id}. This is so the action responds to requests that are:
- GET requests
- Hit a URL such as /api/orders/2
The id is given to you in the id parameter. Note the parameter name must match for this to work.
The action for order creation with a POST request can look like this:
[HttpPost]
public IActionResult CreateOrder([FromBody] Order order)
{
if (ModelState.IsValid == false)
{
return BadRequest(ModelState);
}
Order createdOrder = _orders.CreateOrder(order);
return CreatedAtAction(
nameof(GetOrder), new { id = createdOrder.Id }, createdOrder);
}
Here we use the HttpPostAttribute
to define this action only
accepts POST requests. We also use the FromBodyAttribute
to
bind the request body's content to the order parameter.
If you have used validation attributes such as [Required]
in
your data model, you can also validate that all of those checks
pass by checking if the model state is valid. If not, we return a 400
to the caller.
Otherwise, we create the order, and return a 201 Created response to the caller, along with a Location header that specifies from where the created header can be gotten from.
After seeing and understanding the above actions, the update and deletion actions should be pretty clear.
[HttpPut("{id}")]
public IActionResult UpdateOrder(long id, [FromBody] Order order)
{
if (ModelState.IsValid == false)
{
return BadRequest(ModelState);
}
try
{
_orders.UpdateOrder(id, order);
return Ok();
}
catch (EntityNotFoundException<Order>)
{
return NotFound();
}
}
[HttpDelete("{id}")]
public IActionResult DeleteOrder(long id)
{
_orders.DeleteOrder(id);
return Ok();
}
Updating is done with PUT, deletion is done with DELETE. Deletion returns a 200 OK no matter what, non-existence of an order could mean it was already deleted. It depends on your needs if you want to instead return a 404 when it is not found.
I left out a few actions from here that deal with order rows, as well as the products controller.
But after implementing the controllers your API is ready to go. Just start it up and use your favorite tool (mine are Postman and Fiddler) to do some requests on your API and see what is returned.
You can find the full project on GitHub: https://github.com/juunas11/AspNetCoreApiExample/tree/basic-api.