.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:
- Stateless: Each request contains all information needed to complete it
- Client-Server Architecture: Separation of concerns between client and server
- Cacheable: Responses must define themselves as cacheable or non-cacheable
- Uniform Interface: Resources are identified in requests and manipulated through representations
- Layered System: A client cannot ordinarily tell whether it's connected directly to the end server
- 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:
dotnet new webapi -n BookStoreApi
cd BookStoreApi
Defining the Model
Create a simple Book model:
// 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:
// 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
curl -X GET https://localhost:5001/api/books
Expected output:
[
{
"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
curl -X GET https://localhost:5001/api/books/1
Expected output:
{
"id": 1,
"title": "The Pragmatic Programmer",
"author": "David Thomas, Andrew Hunt",
"year": 2019,
"isbn": "978-0135957059"
}
Create a New Book
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:
- Use a database instead of an in-memory collection
- Implement proper validation
- Add authentication and authorization
- Implement error handling
- 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:
dotnet add package Microsoft.EntityFrameworkCore.SqlServer
dotnet add package Microsoft.EntityFrameworkCore.Design
Creating a Database Context
// 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:
// In ConfigureServices method
services.AddDbContext<BookStoreContext>(options =>
options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));
And add the connection string to appsettings.json
:
"ConnectionStrings": {
"DefaultConnection": "Server=(localdb)\\mssqllocaldb;Database=BookStore;Trusted_Connection=True;MultipleActiveResultSets=true"
}
Update the Controller to Use EF Core
// 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
// 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:
// 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:
// 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:
// 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
// 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
// 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:
- The fundamental principles of REST architecture
- How to create REST APIs using ASP.NET Core
- Implementation of CRUD operations with HTTP methods
- Using Entity Framework Core for data persistence
- Consuming REST APIs with HttpClient
- 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
- Microsoft Learn: Create a web API with ASP.NET Core
- ASP.NET Core Web API documentation
- RESTful API Design Best Practices
- Richardson Maturity Model - For understanding REST API maturity levels
Exercises
- Extend the BookStore API to include a "Category" entity with a one-to-many relationship to books.
- Add filtering capabilities to the GetBooks endpoint (e.g., by author or publication year).
- Implement authentication using JWT (JSON Web Tokens) to secure your API.
- Create a simple front-end application (using Blazor, React, or Angular) that consumes your BookStore API.
- 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! :)