Understanding Circuit Breaker Pattern in ASP.NET Core Web API
In the world of software development, resilience, and fault tolerance are critical aspects of creating robust applications. One of the patterns that play a crucial role in achieving this resilience is the Circuit Breaker pattern. In the context of ASP.NET Core Web API, implementing the Circuit Breaker pattern can significantly enhance the reliability of your API.
What is a Circuit Breaker?
The Circuit Breaker pattern, analogous to its electrical counterpart, monitors for failures and helps prevent cascading failures in distributed systems. It acts as a barrier between a potentially failing service or resource and the rest of the system.
When a service is working as expected, the Circuit Breaker allows requests to pass through. However, if the service encounters an issue or starts to fail, the Circuit Breaker "opens" and prevents further requests from being sent to the failing service for a defined period. This helps to preserve system resources and prevent overload or degradation.
Implementation in ASP.NET Core Web API
Let's consider a real-world use case to illustrate the implementation of the Circuit Breaker pattern in an ASP.NET Core Web API.
Create Asp.Net Core Web API Project
Crete Asp.net core web API project with Command prompt Or Visual Studio
Add the Data Access Layer
Add the data access layer using the library project
Add the Models in the Data Access Layer
Add the model folder and add the class for Book
namespace MicroservicesWithCircutDesignPatternInAspNetCoreWebAPI_DAL.Models
{
public class Book
{
public int Id { get; set; }
public string Title { get; set; }
public string Author { get; set; }
public string ISBN { get; set; }
}
}
Add the Application Database Context Class
using MicroservicesWithCircutDesignPatternInAspNetCoreWebAPI_DAL.Models;
using Microsoft.EntityFrameworkCore;
namespace MicroservicesWithCircutDesignPatternInAspNetCoreWebAPI_DAL.AppDbContext
{
public class ApplicationDbContext : DbContext
{
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
: base(options)
{
}
public DbSet<Book> Books { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
// Define configurations, relationships, constraints here if needed
modelBuilder.Entity<Book>().HasKey(b => b.Id);
modelBuilder.Entity<Book>().Property(b => b.Title).IsRequired();
}
}
}
Add the IRepository Interface
using MicroservicesWithCircutDesignPatternInAspNetCoreWebAPI_DAL.Models;
namespace MicroservicesWithCircutDesignPatternInAspNetCoreWebAPI_DAL.IRepository
{
public interface IBookRepository
{
Task<IEnumerable<Book>> GetAllBooksAsync();
Task<Book> GetBookByIdAsync(int id);
Task<Book> AddBookAsync(Book book);
Task<Book> UpdateBookAsync(int id, Book book);
Task<bool> DeleteBookAsync(int id);
}
}
Implement the Repository Pattern
using MicroservicesWithCircutDesignPatternInAspNetCoreWebAPI_DAL.AppDbContext;
using MicroservicesWithCircutDesignPatternInAspNetCoreWebAPI_DAL.IRepository;
using MicroservicesWithCircutDesignPatternInAspNetCoreWebAPI_DAL.Models;
using Microsoft.EntityFrameworkCore;
namespace MicroservicesWithCircutDesignPatternInAspNetCoreWebAPI_DAL.Repository
{
public class BookRepository : IBookRepository
{
private readonly ApplicationDbContext _dbContext;
public BookRepository(ApplicationDbContext dbContext)
{
_dbContext = dbContext;
}
public async Task<IEnumerable<Book>> GetAllBooksAsync()
{
return await _dbContext.Books.ToListAsync();
}
public async Task<Book> GetBookByIdAsync(int id)
{
return await _dbContext.Books.FindAsync(id);
}
public async Task<Book> AddBookAsync(Book book)
{
_dbContext.Books.AddAsync(book);
await _dbContext.SaveChangesAsync();
return book;
}
public async Task<Book> UpdateBookAsync(int id, Book book)
{
var existingBook = await _dbContext.Books.FindAsync(id);
if(existingBook == null)
return null;
// Update existingBook properties with book
existingBook.Title = book.Title;
existingBook.Author = book.Author;
existingBook.ISBN = book.ISBN;
await _dbContext.SaveChangesAsync();
return existingBook;
}
public async Task<bool> DeleteBookAsync(int id)
{
var bookToDelete = await _dbContext.Books.FindAsync(id);
if(bookToDelete == null)
return false;
_dbContext.Books.Remove(bookToDelete);
await _dbContext.SaveChangesAsync();
return true;
}
}
}
Now Add the Business Access Layer
Add the Bussiness Access Layer
Add the service layer using the library projects in the Asp.net core
Create the Interface for IBookService
using MicroservicesWithCircutDesignPatternInAspNetCoreWebAPI_DAL.Models;
namespace MicroservicesWithCircutDesignPatternInAspNetCoreWebAPI_BAL.IService
{
public interface IBookService
{
Task<IEnumerable<Book>> GetAllBooksAsync();
Task<Book> GetBookByIdAsync(int id);
Task<Book> AddBookAsync(Book book);
Task<Book> UpdateBookAsync(int id, Book book);
Task<bool> DeleteBookAsync(int id);
}
}
Implement the Service with a Circut breaker Design Pattern
using MicroservicesWithCircutDesignPatternInAspNetCoreWebAPI_BAL.IService;
using MicroservicesWithCircutDesignPatternInAspNetCoreWebAPI_DAL.IRepository;
using MicroservicesWithCircutDesignPatternInAspNetCoreWebAPI_DAL.Models;
using Microsoft.Extensions.Logging;
using Polly;
using Polly.CircuitBreaker;
namespace MicroservicesWithCircutDesignPatternInAspNetCoreWebAPI_BAL.Service
{
public class BookService : IBookService
{
private readonly IBookRepository _bookRepository;
private readonly AsyncCircuitBreakerPolicy _circuitBreakerPolicy;
private readonly ILogger<BookService> _logger;
public BookService(IBookRepository bookRepository, ILogger<BookService> logger)
{
_bookRepository = bookRepository;
_logger = logger;
_circuitBreakerPolicy = Policy
.Handle<Exception>()
.CircuitBreakerAsync(3, TimeSpan.FromSeconds(30), OnBreak, OnReset, OnHalfOpen);
}
public async Task<IEnumerable<Book>> GetAllBooksAsync()
{
return await _circuitBreakerPolicy.ExecuteAsync(async () =>
{
try
{
_logger.LogInformation("Executing GetAllBooksAsync method.");
return await _bookRepository.GetAllBooksAsync();
}
catch(Exception ex)
{
_logger.LogError(ex, "Error occurred in GetAllBooksAsync method.");
throw; // Re-throw the exception to propagate it up the call stack
}
});
}
public async Task<Book> GetBookByIdAsync(int id)
{
return await _circuitBreakerPolicy.ExecuteAsync(async () =>
{
try
{
_logger.LogInformation($"Executing GetBookByIdAsync method for book with ID: {id}.");
return await _bookRepository.GetBookByIdAsync(id);
}
catch(Exception ex)
{
_logger.LogError(ex, $"Error occurred in GetBookByIdAsync method for book with ID: {id}.");
throw; // Re-throw the exception to propagate it up the call stack
}
});
}
public async Task<Book> AddBookAsync(Book book)
{
return await _circuitBreakerPolicy.ExecuteAsync(async () =>
{
try
{
_logger.LogInformation("Executing AddBookAsync method.");
return await _bookRepository.AddBookAsync(book);
}
catch(Exception ex)
{
_logger.LogError(ex, "Error occurred in AddBookAsync method.");
throw; // Re-throw the exception to propagate it up the call stack
}
});
}
public async Task<Book> UpdateBookAsync(int id, Book book)
{
return await _circuitBreakerPolicy.ExecuteAsync(async () =>
{
try
{
_logger.LogInformation($"Executing UpdateBookAsync method for book with ID: {id}.");
return await _bookRepository.UpdateBookAsync(id, book);
}
catch(Exception ex)
{
_logger.LogError(ex, $"Error occurred in UpdateBookAsync method for book with ID: {id}.");
throw; // Re-throw the exception to propagate it up the call stack
}
});
}
public async Task<bool> DeleteBookAsync(int id)
{
return await _circuitBreakerPolicy.ExecuteAsync(async () =>
{
try
{
_logger.LogInformation($"Executing DeleteBookAsync method for book with ID: {id}.");
return await _bookRepository.DeleteBookAsync(id);
}
catch(Exception ex)
{
_logger.LogError(ex, $"Error occurred in DeleteBookAsync method for book with ID: {id}.");
throw; // Re-throw the exception to propagate it up the call stack
}
});
}
private void OnBreak(Exception exception, TimeSpan duration)
{
_logger.LogError(exception, $"Circuit is open for {duration.TotalSeconds} seconds due to {exception.Message}");
}
private void OnReset()
{
_logger.LogInformation("Circuit is reset");
}
private void OnHalfOpen()
{
_logger.LogInformation("Circuit is half-open");
}
}
}
This code implements a BookService in an ASP.NET Core Web API application following the Circuit Breaker design pattern using the Polly library. The BookService interacts with a data access layer (IBookRepository) to perform CRUD operations on books (Book model).
The service constructor initializes the BookService with an instance of the book repository and a logger. It also sets up an AsyncCircuitBreakerPolicy using Polly to manage the circuit breaker behavior.
Each method (GetAllBooksAsync, GetBookByIdAsync, AddBookAsync, UpdateBookAsync, DeleteBookAsync) is wrapped within the circuit breaker policy's ExecuteAsync method. This ensures that the operations on the repository are executed within the context of the circuit breaker policy.
The circuit breaker policy is configured to handle exceptions. If the number of exceptions surpasses a threshold (3 in this case) within a specified time window (30 seconds), the circuit breaks, preventing further calls to the repository. Any exceptions are logged during this time, and the circuit remains open for the specified duration.
The OnBreak, OnReset, and OnHalfOpen methods are callback methods that log events when the circuit breaks, resets, or transitions to a half-open state, respectively. These callbacks provide visibility into the state changes of the circuit breaker.
This implementation protects the application from potential failures caused by the data access layer by intelligently managing the flow of requests and preventing them from overwhelming a failing system, thereby enhancing the application's resilience.
Presentation Layer
Add the presentation that will handle the HTTP requests
Add the database connection string
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"ConnectionStrings": {
"DefaultConnection": "YourDatabaseConnectionString"
},
"AllowedHosts": "*"
}
Add the dependency injection
using MicroservicesWithCircutDesignPatternInAspNetCoreWebAPI_BAL.IService;
using MicroservicesWithCircutDesignPatternInAspNetCoreWebAPI_BAL.Service;
using MicroservicesWithCircutDesignPatternInAspNetCoreWebAPI_DAL.AppDbContext;
using MicroservicesWithCircutDesignPatternInAspNetCoreWebAPI_DAL.IRepository;
using MicroservicesWithCircutDesignPatternInAspNetCoreWebAPI_DAL.Repository;
using Microsoft.EntityFrameworkCore;
using Microsoft.OpenApi.Models;
using Polly;
using Polly.Extensions.Http;
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
var configuration = builder.Configuration;
// Add services to the container.
builder.Services.AddDbContext<ApplicationDbContext>(options =>
{
options.UseSqlServer(configuration.GetConnectionString("DefaultConnection")); // Replace with your database provider and connection string
});
builder.Services.AddTransient<IBookService, BookService>();
builder.Services.AddTransient<IBookRepository, BookRepository>();
builder.Services.AddHttpClient<IBookService, BookService>(client =>
{
client.BaseAddress = new Uri("Your_Base_Address_Url"); // Set your base URL here
})
.AddPolicyHandler(GetCircuitBreakerPolicy()); // Apply Circuit Breaker policy
builder.Services.AddControllers();
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new OpenApiInfo { Title = "Microservices Development using Circut Breaker Design Pattern", Version = "v1" });
});
var app = builder.Build();
// Configure the HTTP request pipeline.
if(app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseHttpsRedirection();
app.UseAuthorization();
app.MapControllers();
app.Run();
// Define Circuit Breaker policy
IAsyncPolicy<HttpResponseMessage> GetCircuitBreakerPolicy()
{
return HttpPolicyExtensions
.HandleTransientHttpError()
.CircuitBreakerAsync(3, TimeSpan.FromSeconds(30));
}
Let's break down the code:
Service Registration:
The code starts by creating a builder for the web application and configuring necessary services.
AddDbContext is used to configure and add the Entity Framework Core's DbContext (ApplicationDbContext) with a SQL Server database using a connection string retrieved from the configuration.
AddTransient is used to register dependencies (IBookService, IBookRepository) and their implementations (BookService, BookRepository) as transient services. These services will be instantiated each time they're requested.
AddHttpClient is used to configure an HTTP client for IBookService. It sets the base address for the HTTP requests and applies a circuit breaker policy to handle transient HTTP errors.
Swagger/OpenAPI Configuration:
Services for Swagger/OpenAPI are configured to provide API documentation and exploration tools.
Application Configuration:
The application is built and configured based on the environment (development/production).
In the development environment, Swagger UI is enabled for API documentation.
Circuit Breaker Policy:
The GetCircuitBreakerPolicy method defines a circuit breaker policy for HTTP requests. This policy is configured to handle transient HTTP errors and breaks the circuit if there are 3 consecutive transient errors within a 30-second window.
Application Pipeline:
HTTPS redirection and authorization middleware are added.
Controllers are mapped for handling incoming requests.
Application Execution:
The application is started using the app.Run().
This code sets up an ASP.NET Core Web API application with database context configuration, HTTP client configuration with a circuit breaker policy to handle transient HTTP errors, Swagger for API documentation, and essential middleware for request handling and routing. The circuit breaker policy aims to enhance the application's resilience by intelligently handling transient errors in HTTP requests to external services or APIs.
Add the Controller that handles the HTTP Requests
using MicroservicesWithCircutDesignPatternInAspNetCoreWebAPI_BAL.IService;
using MicroservicesWithCircutDesignPatternInAspNetCoreWebAPI_DAL.Models;
using Microsoft.AspNetCore.Mvc;
namespace MicroservicesWithCircutDesignPatternInAspNetCoreWebAPI.Controllers
{
[Route("api/[controller]/[action]")]
[ApiController]
public class BookController : ControllerBase
{
private readonly IBookService _bookService;
private readonly ILogger<BookController> _logger;
public BookController(IBookService bookService, ILogger<BookController> logger)
{
_bookService = bookService ?? throw new ArgumentNullException(nameof(bookService));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
[HttpGet(nameof(GetAllBooks))]
public async Task<ActionResult<IEnumerable<Book>>> GetAllBooks()
{
try
{
var books = await _bookService.GetAllBooksAsync();
return Ok(books);
}
catch(Exception exception)
{
_logger.LogError(exception, "An error occurred while fetching books.");
return StatusCode(500, "An error occurred while fetching books.");
}
}
[HttpGet(nameof(GetBookById))]
public async Task<ActionResult<Book>> GetBookById(int id)
{
try
{
var book = await _bookService.GetBookByIdAsync(id);
if(book == null)
{
return NotFound();
}
return Ok(book);
}
catch(Exception exception)
{
_logger.LogError(exception, "An error occurred while fetching the book.");
return StatusCode(500, "An error occurred while fetching the book.");
}
}
[HttpPost(nameof(AddBook))]
public async Task<ActionResult<Book>> AddBook(Book book)
{
try
{
var addedBook = await _bookService.AddBookAsync(book);
return CreatedAtAction(nameof(GetBookById), new { id = addedBook.Id }, addedBook);
}
catch(Exception exception)
{
_logger.LogError(exception, "An error occurred while adding the book.");
return StatusCode(500, "An error occurred while adding the book.");
}
}
[HttpPut(nameof(UpdateBook))]
public async Task<ActionResult<Book>> UpdateBook(int id, Book book)
{
try
{
var updatedBook = await _bookService.UpdateBookAsync(id, book);
if(updatedBook == null)
{
return NotFound();
}
return Ok(updatedBook);
}
catch(Exception exception)
{
_logger.LogError(exception, "An error occurred while updating the book.");
return StatusCode(500, "An error occurred while updating the book.");
}
}
[HttpDelete(nameof(DeleteBook))]
public async Task<IActionResult> DeleteBook(int id)
{
try
{
var deleted = await _bookService.DeleteBookAsync(id);
if(!deleted)
{
return NotFound();
}
return NoContent();
}
catch(Exception exception)
{
_logger.LogError(exception, "An error occurred while deleting the book.");
return StatusCode(500, "An error occurred while deleting the book.");
}
}
}
}
Output
GitHub Project URL
https://github.com/SardarMudassarAliKhan/MicroservicesWithCircutDesignPatternInAspNetCoreWebAPI
Conclusion
In conclusion, the BookService class showcases robust error handling and resilience through the implementation of the Circuit Breaker pattern using the Polly library in an ASP.NET Core Web API application.
By incorporating AsyncCircuitBreakerPolicy, the service manages potential faults by monitoring and breaking the circuit when exceptions occur, preventing a cascade of repeated failures. This implementation leverages the ILogger interface to log informative messages, allowing for comprehensive monitoring and tracing of method execution, exceptions, and Circuit Breaker state transitions.
The code demonstrates best practices in exception handling, asynchronous programming, and logging, ensuring a more stable and reliable system. Utilizing interfaces like IBookService and IBookRepository showcases adherence to the Dependency Injection principle, promoting modularity and testability in the codebase.
This design fortifies the service layer against faults, enhances system resilience, and facilitates efficient error management in a microservices-based architecture.