4

Building a basic Web API on ASP.NET Core

 1 year ago
source link: https://joonasw.net/view/building-a-basic-web-api-on-asp-net-core
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

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

  • 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:

  1. Go to File -> New -> Project...
  2. From the Web category, pick ASP.NET Core Web Application (.NET Core)
  3. When asked, pick the Web API template
  4. 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:

  1. The request is a GET request
  2. 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:

  1. GET requests
  2. 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.

Related links


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK