Skip to main content

.NET REST Services

Introduction

REST (Representational State Transfer) is an architectural style for designing networked applications. RESTful services are lightweight, maintainable, and scalable, making them the preferred choice for building APIs in modern web applications. In this guide, we'll explore how to build and consume REST services using .NET technologies, particularly ASP.NET Core.

REST APIs use HTTP methods (GET, POST, PUT, DELETE) to perform CRUD (Create, Read, Update, Delete) operations on resources, which are identified by URLs. They typically return data in JSON or XML format, with JSON being the most common choice today.

REST Principles

Before diving into implementation, let's understand the core principles of REST:

  1. Stateless: Each request contains all information needed to complete it
  2. Client-Server Architecture: Separation of concerns between client and server
  3. Cacheable: Responses must define themselves as cacheable or non-cacheable
  4. Uniform Interface: Resources are identified in requests and manipulated through representations
  5. Layered System: A client cannot ordinarily tell whether it's connected directly to the end server
  6. Code on Demand (optional): Servers can temporarily extend client functionality

Building a REST API with ASP.NET Core

ASP.NET Core provides a robust framework for building RESTful APIs. Let's create a simple book management API to demonstrate the concepts.

Setting Up the Project

First, create a new Web API project:

bash
dotnet new webapi -n BookStoreApi
cd BookStoreApi

Defining the Model

Create a simple Book model:

csharp
// Models/Book.cs
namespace BookStoreApi.Models
{
public class Book
{
public int Id { get; set; }
public string Title { get; set; }
public string Author { get; set; }
public int Year { get; set; }
public string ISBN { get; set; }
}
}

Creating a Controller

Controllers handle HTTP requests and generate responses. Let's create a BookController:

csharp
// Controllers/BooksController.cs
using Microsoft.AspNetCore.Mvc;
using BookStoreApi.Models;
using System.Collections.Generic;
using System.Linq;

namespace BookStoreApi.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class BooksController : ControllerBase
{
private static List<Book> _books = new List<Book>
{
new Book { Id = 1, Title = "The Pragmatic Programmer", Author = "David Thomas, Andrew Hunt", Year = 2019, ISBN = "978-0135957059" },
new Book { Id = 2, Title = "Clean Code", Author = "Robert C. Martin", Year = 2008, ISBN = "978-0132350884" },
new Book { Id = 3, Title = "Design Patterns", Author = "Erich Gamma et al.", Year = 1994, ISBN = "978-0201633610" }
};

// GET: api/books
[HttpGet]
public ActionResult<IEnumerable<Book>> GetBooks()
{
return _books;
}

// GET: api/books/1
[HttpGet("{id}")]
public ActionResult<Book> GetBook(int id)
{
var book = _books.FirstOrDefault(b => b.Id == id);
if (book == null)
{
return NotFound();
}
return book;
}

// POST: api/books
[HttpPost]
public ActionResult<Book> CreateBook(Book book)
{
book.Id = _books.Max(b => b.Id) + 1;
_books.Add(book);
return CreatedAtAction(nameof(GetBook), new { id = book.Id }, book);
}

// PUT: api/books/1
[HttpPut("{id}")]
public IActionResult UpdateBook(int id, Book book)
{
if (id != book.Id)
{
return BadRequest();
}

var existingBook = _books.FirstOrDefault(b => b.Id == id);
if (existingBook == null)
{
return NotFound();
}

var index = _books.IndexOf(existingBook);
_books[index] = book;

return NoContent();
}

// DELETE: api/books/1
[HttpDelete("{id}")]
public IActionResult DeleteBook(int id)
{
var book = _books.FirstOrDefault(b => b.Id == id);
if (book == null)
{
return NotFound();
}

_books.Remove(book);
return NoContent();
}
}
}

Understanding HTTP Methods

Our controller uses different HTTP methods for different operations:

  • GET: Retrieve resources (books)
  • POST: Create a new resource (book)
  • PUT: Update an existing resource (book)
  • DELETE: Remove a resource (book)

Status Codes

REST APIs communicate using HTTP status codes:

  • 200 OK: Request was successful
  • 201 Created: Resource was successfully created
  • 204 No Content: Request successful, no content returned (used for PUT/DELETE)
  • 400 Bad Request: Invalid request format
  • 404 Not Found: Resource not found
  • 500 Internal Server Error: Server error

Testing Our API

You can test the API using tools like Postman, curl, or by creating a simple client. Here are some examples using curl:

Get All Books

bash
curl -X GET https://localhost:5001/api/books

Expected output:

json
[
{
"id": 1,
"title": "The Pragmatic Programmer",
"author": "David Thomas, Andrew Hunt",
"year": 2019,
"isbn": "978-0135957059"
},
{
"id": 2,
"title": "Clean Code",
"author": "Robert C. Martin",
"year": 2008,
"isbn": "978-0132350884"
},
{
"id": 3,
"title": "Design Patterns",
"author": "Erich Gamma et al.",
"year": 1994,
"isbn": "978-0201633610"
}
]

Get a Single Book

bash
curl -X GET https://localhost:5001/api/books/1

Expected output:

json
{
"id": 1,
"title": "The Pragmatic Programmer",
"author": "David Thomas, Andrew Hunt",
"year": 2019,
"isbn": "978-0135957059"
}

Create a New Book

bash
curl -X POST https://localhost:5001/api/books \
-H "Content-Type: application/json" \
-d '{"title":"Code Complete","author":"Steve McConnell","year":2004,"isbn":"978-0735619678"}'

Building a More Realistic API

In real-world scenarios, you'd typically:

  1. Use a database instead of an in-memory collection
  2. Implement proper validation
  3. Add authentication and authorization
  4. Implement error handling
  5. Use DTOs (Data Transfer Objects) to separate your API models from database models

Let's enhance our example with Entity Framework Core to use a database.

Adding Entity Framework Core

First, add the required packages:

bash
dotnet add package Microsoft.EntityFrameworkCore.SqlServer
dotnet add package Microsoft.EntityFrameworkCore.Design

Creating a Database Context

csharp
// Data/BookStoreContext.cs
using Microsoft.EntityFrameworkCore;
using BookStoreApi.Models;

namespace BookStoreApi.Data
{
public class BookStoreContext : DbContext
{
public BookStoreContext(DbContextOptions<BookStoreContext> options)
: base(options)
{
}

public DbSet<Book> Books { get; set; }

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
// Seed data
modelBuilder.Entity<Book>().HasData(
new Book { Id = 1, Title = "The Pragmatic Programmer", Author = "David Thomas, Andrew Hunt", Year = 2019, ISBN = "978-0135957059" },
new Book { Id = 2, Title = "Clean Code", Author = "Robert C. Martin", Year = 2008, ISBN = "978-0132350884" },
new Book { Id = 3, Title = "Design Patterns", Author = "Erich Gamma et al.", Year = 1994, ISBN = "978-0201633610" }
);
}
}
}

Configure Services in Startup

Update Startup.cs to add Entity Framework Core:

csharp
// In ConfigureServices method
services.AddDbContext<BookStoreContext>(options =>
options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));

And add the connection string to appsettings.json:

json
"ConnectionStrings": {
"DefaultConnection": "Server=(localdb)\\mssqllocaldb;Database=BookStore;Trusted_Connection=True;MultipleActiveResultSets=true"
}

Update the Controller to Use EF Core

csharp
// Controllers/BooksController.cs
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using BookStoreApi.Models;
using BookStoreApi.Data;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace BookStoreApi.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class BooksController : ControllerBase
{
private readonly BookStoreContext _context;

public BooksController(BookStoreContext context)
{
_context = context;
}

// GET: api/books
[HttpGet]
public async Task<ActionResult<IEnumerable<Book>>> GetBooks()
{
return await _context.Books.ToListAsync();
}

// GET: api/books/1
[HttpGet("{id}")]
public async Task<ActionResult<Book>> GetBook(int id)
{
var book = await _context.Books.FindAsync(id);

if (book == null)
{
return NotFound();
}

return book;
}

// POST: api/books
[HttpPost]
public async Task<ActionResult<Book>> CreateBook(Book book)
{
_context.Books.Add(book);
await _context.SaveChangesAsync();

return CreatedAtAction(
nameof(GetBook),
new { id = book.Id },
book);
}

// PUT: api/books/1
[HttpPut("{id}")]
public async Task<IActionResult> UpdateBook(int id, Book book)
{
if (id != book.Id)
{
return BadRequest();
}

_context.Entry(book).State = EntityState.Modified;

try
{
await _context.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
if (!_context.Books.Any(b => b.Id == id))
{
return NotFound();
}
else
{
throw;
}
}

return NoContent();
}

// DELETE: api/books/1
[HttpDelete("{id}")]
public async Task<IActionResult> DeleteBook(int id)
{
var book = await _context.Books.FindAsync(id);
if (book == null)
{
return NotFound();
}

_context.Books.Remove(book);
await _context.SaveChangesAsync();

return NoContent();
}
}
}

Consuming REST APIs in .NET

Now let's look at how to consume REST APIs in .NET applications. We'll use HttpClient to interact with our Book API.

Creating a Client Application

csharp
// BookApiClient.cs
using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Net.Http.Json;
using System.Threading.Tasks;
using System.Text.Json;

namespace BookStoreClient
{
public class Book
{
public int Id { get; set; }
public string Title { get; set; }
public string Author { get; set; }
public int Year { get; set; }
public string ISBN { get; set; }
}

public class BookApiClient
{
private readonly HttpClient _httpClient;
private readonly string _baseUrl;

public BookApiClient(string baseUrl)
{
_baseUrl = baseUrl;
_httpClient = new HttpClient();
}

public async Task<List<Book>> GetBooksAsync()
{
var response = await _httpClient.GetAsync($"{_baseUrl}/api/books");
response.EnsureSuccessStatusCode();

return await response.Content.ReadFromJsonAsync<List<Book>>();
}

public async Task<Book> GetBookAsync(int id)
{
var response = await _httpClient.GetAsync($"{_baseUrl}/api/books/{id}");
response.EnsureSuccessStatusCode();

return await response.Content.ReadFromJsonAsync<Book>();
}

public async Task<Book> CreateBookAsync(Book book)
{
var response = await _httpClient.PostAsJsonAsync($"{_baseUrl}/api/books", book);
response.EnsureSuccessStatusCode();

return await response.Content.ReadFromJsonAsync<Book>();
}

public async Task UpdateBookAsync(int id, Book book)
{
var response = await _httpClient.PutAsJsonAsync($"{_baseUrl}/api/books/{id}", book);
response.EnsureSuccessStatusCode();
}

public async Task DeleteBookAsync(int id)
{
var response = await _httpClient.DeleteAsync($"{_baseUrl}/api/books/{id}");
response.EnsureSuccessStatusCode();
}
}

class Program
{
static async Task Main(string[] args)
{
var client = new BookApiClient("https://localhost:5001");

// Get all books
Console.WriteLine("All books:");
var books = await client.GetBooksAsync();
foreach (var book in books)
{
Console.WriteLine($"{book.Id}. {book.Title} by {book.Author} ({book.Year})");
}

// Get a specific book
try
{
Console.WriteLine("\nGetting book with ID 1:");
var book = await client.GetBookAsync(1);
Console.WriteLine($"{book.Id}. {book.Title} by {book.Author} ({book.Year})");
}
catch (HttpRequestException e)
{
Console.WriteLine($"Error getting book: {e.Message}");
}

// Create a new book
try
{
Console.WriteLine("\nCreating a new book:");
var newBook = new Book
{
Title = "C# in Depth",
Author = "Jon Skeet",
Year = 2019,
ISBN = "978-1617294532"
};

var createdBook = await client.CreateBookAsync(newBook);
Console.WriteLine($"Created book with ID: {createdBook.Id}");
}
catch (HttpRequestException e)
{
Console.WriteLine($"Error creating book: {e.Message}");
}

Console.ReadLine();
}
}
}

Best Practices for REST API Development

To build professional-quality REST APIs in .NET, follow these best practices:

1. API Versioning

APIs evolve over time. Use versioning to maintain backward compatibility:

csharp
// In Startup.cs
services.AddApiVersioning(options =>
{
options.DefaultApiVersion = new ApiVersion(1, 0);
options.AssumeDefaultVersionWhenUnspecified = true;
options.ReportApiVersions = true;
});

// In controller
[ApiVersion("1.0")]
[Route("api/v{version:apiVersion}/books")]
[ApiController]
public class BooksController : ControllerBase
{
// Controller code
}

2. Use DTOs for Request/Response

Separate your API contracts from your domain models:

csharp
// DTOs/BookDto.cs
public class BookDto
{
public int Id { get; set; }
public string Title { get; set; }
public string Author { get; set; }
public int Year { get; set; }
public string ISBN { get; set; }
}

// Then use mapping in your controller
// GET: api/books
[HttpGet]
public async Task<ActionResult<IEnumerable<BookDto>>> GetBooks()
{
var books = await _context.Books.ToListAsync();
return books.Select(book => new BookDto
{
Id = book.Id,
Title = book.Title,
Author = book.Author,
Year = book.Year,
ISBN = book.ISBN
}).ToList();
}

Consider using libraries like AutoMapper for more complex mapping scenarios.

3. Implement Proper Error Handling

Create a consistent error response format:

csharp
// Models/ErrorResponse.cs
public class ErrorResponse
{
public string Type { get; set; }
public string Message { get; set; }
public int StatusCode { get; set; }
}

// In a controller action
[HttpGet("{id}")]
public async Task<ActionResult<BookDto>> GetBook(int id)
{
var book = await _context.Books.FindAsync(id);

if (book == null)
{
return NotFound(new ErrorResponse
{
Type = "https://tools.ietf.org/html/rfc7231#section-6.5.4",
Message = $"Book with ID {id} was not found.",
StatusCode = 404
});
}

// Map to DTO and return
}

4. Use Action Filters for Cross-Cutting Concerns

csharp
// Filters/ValidateModelStateAttribute.cs
public class ValidateModelStateAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext context)
{
if (!context.ModelState.IsValid)
{
context.Result = new BadRequestObjectResult(context.ModelState);
}
}
}

5. Add Pagination for Collections

csharp
// Models/PagedResponse.cs
public class PagedResponse<T>
{
public int CurrentPage { get; set; }
public int PageSize { get; set; }
public int TotalCount { get; set; }
public int TotalPages { get; set; }
public List<T> Data { get; set; }
}

// In controller
[HttpGet]
public async Task<ActionResult<PagedResponse<BookDto>>> GetBooks([FromQuery] int page = 1, [FromQuery] int pageSize = 10)
{
var totalCount = await _context.Books.CountAsync();
var totalPages = (int)Math.Ceiling((double)totalCount / pageSize);

var books = await _context.Books
.Skip((page - 1) * pageSize)
.Take(pageSize)
.Select(b => new BookDto
{
Id = b.Id,
Title = b.Title,
Author = b.Author,
Year = b.Year,
ISBN = b.ISBN
})
.ToListAsync();

return new PagedResponse<BookDto>
{
CurrentPage = page,
PageSize = pageSize,
TotalCount = totalCount,
TotalPages = totalPages,
Data = books
};
}

Summary

In this comprehensive guide, we've covered:

  1. The fundamental principles of REST architecture
  2. How to create REST APIs using ASP.NET Core
  3. Implementation of CRUD operations with HTTP methods
  4. Using Entity Framework Core for data persistence
  5. Consuming REST APIs with HttpClient
  6. Best practices for real-world REST API development

REST services are a cornerstone of modern web development, and .NET provides a robust ecosystem for both creating and consuming these services. By understanding the concepts presented here, you're well on your way to building professional-grade REST APIs with .NET.

Additional Resources

Exercises

  1. Extend the BookStore API to include a "Category" entity with a one-to-many relationship to books.
  2. Add filtering capabilities to the GetBooks endpoint (e.g., by author or publication year).
  3. Implement authentication using JWT (JSON Web Tokens) to secure your API.
  4. Create a simple front-end application (using Blazor, React, or Angular) that consumes your BookStore API.
  5. Add caching to your API to improve performance for frequently requested data.

Happy coding!



If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)