Table of Contents
Introduction to Architectural and Enterprise Patterns
1.1 What Are Architectural Patterns?
1.2 What Are Enterprise Patterns?
1.3 Why These Patterns Matter
1.4 Real-World Scenario: E-Commerce Platform
Presentation Patterns
2.1 Model-View-Controller (MVC)
2.1.1 Overview and Real-World Analogy
2.1.2 Implementation in ASP.NET Core
2.1.3 Exception Handling
2.1.4 Pros, Cons, and Alternatives
2.2 Model-View-Presenter (MVP)
2.2.1 Overview and Real-World Analogy
2.2.2 Implementation in ASP.NET Core
2.2.3 Exception Handling
2.2.4 Pros, Cons, and Alternatives
2.3 Model-View-ViewModel (MVVM)
2.3.1 Overview and Real-World Analogy
2.3.2 Implementation in .NET (WPF)
2.3.3 Exception Handling
2.3.4 Pros, Cons, and Alternatives
Layered / N-Tier Architecture Patterns
3.1 Overview and Real-World Analogy
3.2 Implementation in ASP.NET Core with SQL Server
3.3 Exception Handling
3.4 Pros, Cons, and Alternatives
Microservices Design Patterns
4.1 Overview and Real-World Analogy
4.2 API Gateway Pattern
4.2.1 Implementation in ASP.NET Core
4.2.2 Exception Handling
4.2.3 Pros, Cons, and Alternatives
4.3 Command Query Responsibility Segregation (CQRS)
4.3.1 Implementation with MediatR in ASP.NET Core
4.3.2 Exception Handling
4.3.3 Pros, Cons, and Alternatives
4.4 Event Sourcing
4.4.1 Implementation with EventStoreDB
4.4.2 Exception Handling
4.4.3 Pros, Cons, and Alternatives
Repository and Unit of Work Patterns
5.1 Repository Pattern
5.1.1 Overview and Real-World Analogy
5.1.2 Implementation in ASP.NET Core with EF Core
5.1.3 Exception Handling
5.1.4 Pros, Cons, and Alternatives
5.2 Unit of Work Pattern
5.2.1 Overview and Real-World Analogy
5.2.2 Implementation with EF Core
5.2.3 Exception Handling
5.2.4 Pros, Cons, and Alternatives
Dependency Injection and IoC Containers
6.1 Overview and Real-World Analogy
6.2 Implementation in .NET Core DI
6.3 Implementation in Java Spring
6.4 Exception Handling
6.5 Pros, Cons, and Alternatives
Service Locator Pattern
7.1 Overview and Real-World Analogy
7.2 Implementation in ASP.NET Core
7.3 Exception Handling
7.4 Pros, Cons, and Alternatives
Best Practices for Architectural and Enterprise Patterns
8.1 General Guidelines
8.2 Performance Optimization
8.3 Scalability and Maintainability
8.4 Testing Strategies
Conclusion
9.1 Recap of Key Patterns
9.2 Choosing the Right Pattern
9.3 Next Steps in Your Learning Journey
Resources and Further Reading
1. Introduction to Architectural and Enterprise Patterns
1.1 What Are Architectural Patterns?
Architectural patterns are high-level strategies for structuring software systems to achieve scalability, maintainability, and flexibility. They define how components interact and are organized within an application. Examples include MVC, Layered architecture, and Microservices.
Analogy: Think of architectural patterns as city planning. Just as a city needs roads, utilities, and zoning to function efficiently, software needs patterns to organize code and ensure smooth interactions.
1.2 What Are Enterprise Patterns?
Enterprise patterns focus on solving common problems in large-scale, business-critical applications. They address concerns like data persistence (Repository, Unit of Work), dependency management (DI, IoC), and distributed systems (Microservices patterns).
Analogy: Enterprise patterns are like corporate workflows in a large organization, ensuring departments (components) work together seamlessly while handling complex operations.
1.3 Why These Patterns Matter
These patterns help developers:
Build scalable, maintainable systems.
Handle complexity in large applications.
Ensure testability and extensibility.
Adapt to modern distributed systems (e.g., Microservices).
1.4 Real-World Scenario: E-Commerce Platform
Throughout this module, we’ll use an e-commerce platform (e.g., an online store like Amazon) as our example. The platform needs:
A user interface for browsing products (MVC/MVVM).
A layered architecture for separating concerns.
Microservices for handling orders, payments, and inventory.
Repositories for data access and DI for modularity.
2. Presentation Patterns
Presentation patterns manage how data is displayed and interacted with in the user interface. Let’s explore MVC, MVP, and MVVM.
2.1 Model-View-Controller (MVC)
2.1.1 Overview and Real-World Analogy
MVC separates an application into three components:
Model: Represents data and business logic (e.g., product data).
View: Displays the data to the user (e.g., product page).
Controller: Handles user input and coordinates Model-View interactions.
Analogy: In a restaurant, the kitchen (Model) prepares food, the waiter (Controller) takes orders and delivers food, and the table setting (View) presents the food to the customer.
2.1.2 Implementation in ASP.NET Core
Let’s build a product listing feature for our e-commerce platform using ASP.NET Core MVC.
Model:
// Models/Product.cs
public class Product
{
public int Id { get; set; }
public string Name { get; set; }
public decimal Price { get; set; }
public string Description { get; set; }
}
View (Razor view):
// Views/Products/Index.cshtml
@model IEnumerable<Product>
<h2>Product Listing</h2>
<table class="table">
<thead>
<tr>
<th>Name</th>
<th>Price</th>
<th>Description</th>
</tr>
</thead>
<tbody>
@foreach (var product in Model)
{
<tr>
<td>@product.Name</td>
<td>@product.Price:C</td>
<td>@product.Description</td>
</tr>
}
</tbody>
</table>
Controller:
// Controllers/ProductsController.cs
using Microsoft.AspNetCore.Mvc;
using System.Collections.Generic;
public class ProductsController : Controller
{
private readonly IProductService _productService;
public ProductsController(IProductService productService)
{
_productService = productService ?? throw new ArgumentNullException(nameof(productService));
}
public IActionResult Index()
{
try
{
var products = _productService.GetAllProducts();
return View(products);
}
catch (Exception ex)
{
// Log exception (e.g., using Serilog or ILogger)
return StatusCode(500, "An error occurred while fetching products.");
}
}
}
Service (for abstraction):
// Services/IProductService.cs
public interface IProductService
{
IEnumerable<Product> GetAllProducts();
}
// Services/ProductService.cs
public class ProductService : IProductService
{
public IEnumerable<Product> GetAllProducts()
{
// Simulated data; replace with DB call
return new List<Product>
{
new Product { Id = 1, Name = "Laptop", Price = 999.99m, Description = "High-performance laptop" },
new Product { Id = 2, Name = "Phone", Price = 499.99m, Description = "Latest smartphone" }
};
}
}
Startup Configuration:
// Program.cs or Startup.cs
builder.Services.AddScoped<IProductService, ProductService>();
2.1.3 Exception Handling
Centralized Handling: Use ASP.NET Core’s middleware for global exception handling.
// Program.cs
app.UseExceptionHandler(errorApp =>
{
errorApp.Run(async context =>
{
context.Response.StatusCode = 500;
await context.Response.WriteAsync("An unexpected error occurred.");
});
});
Specific Handling: Catch exceptions in controllers/services and return user-friendly messages.
Logging: Use ILogger to log exceptions for debugging.
2.1.4 Pros, Cons, and Alternatives
Pros:
Clear separation of concerns.
Easy to test (controllers are stateless).
Well-supported in ASP.NET Core.
Cons:
Can become complex with large applications.
Tight coupling between View and Controller in some cases.
Alternatives:
MVVM: For richer client-side interactions.
MVP: For tighter control over View logic.
2.2 Model-View-Presenter (MVP)
2.2.1 Overview and Real-World Analogy
MVP is similar to MVC but with a stronger separation between View and Presenter. The Presenter handles all logic, and the View is passive.
Analogy: In a theater, the script (Model) contains the story, the director (Presenter) interprets it and directs actors, and the stage (View) displays the performance without controlling the show.
2.2.2 Implementation in ASP.NET Core
MVP is less common in web apps but can be implemented using a custom structure. Here’s an example for a product search feature.
View Interface:
// Views/IProductView.cs
public interface IProductView
{
void DisplayProducts(IEnumerable<Product> products);
void DisplayError(string message);
}
Presenter:
// Presenters/ProductPresenter.cs
public class ProductPresenter
{
private readonly IProductView _view;
private readonly IProductService _productService;
public ProductPresenter(IProductView view, IProductService productService)
{
_view = view ?? throw new ArgumentNullException(nameof(view));
_productService = productService ?? throw new ArgumentNullException(nameof(productService));
}
public void LoadProducts()
{
try
{
var products = _productService.GetAllProducts();
_view.DisplayProducts(products);
}
catch (Exception ex)
{
_view.DisplayError("Failed to load products.");
// Log exception
}
}
}
View Implementation (Razor Page):
// Pages/Products.cshtml.cs
public class ProductsModel : PageModel, IProductView
{
private readonly ProductPresenter _presenter;
public ProductsModel(IProductService productService)
{
_presenter = new ProductPresenter(this, productService);
}
public IEnumerable<Product> Products { get; set; }
public string ErrorMessage { get; set; }
public void OnGet()
{
_presenter.LoadProducts();
}
public void DisplayProducts(IEnumerable<Product> products)
{
Products = products;
}
public void DisplayError(string message)
{
ErrorMessage = message;
}
}
Razor View:
// Pages/Products.cshtml
@page
@model ProductsModel
<h2>Products</h2>
@if (!string.IsNullOrEmpty(Model.ErrorMessage))
{
<p class="error">@Model.ErrorMessage</p>
}
<table class="table">
<!-- Similar to MVC View -->
</table>
2.2.3 Exception Handling
Handle exceptions in the Presenter to keep the View passive.
Use a global error handler in ASP.NET Core for uncaught exceptions.
Log errors using a logging framework.
2.2.4 Pros, Cons, and Alternatives
Pros:
Strong separation between View and logic.
Easier to unit test than MVC.
Cons:
More boilerplate code.
Less natural fit for web applications.
Alternatives:
MVC: Simpler for web apps.
MVVM: Better for data-binding scenarios.
2.3 Model-View-ViewModel (MVVM)
2.3.1 Overview and Real-World Analogy
MVVM is designed for data-binding scenarios, common in WPF or Blazor. The ViewModel exposes data and commands that the View binds to.
Analogy: Think of a smart home system where the appliances (Model) store state, the control panel (ViewModel) exposes controls, and the touchscreen (View) displays and interacts with those controls.
2.3.2 Implementation in .NET (WPF)
Here’s an MVVM example for a product details page in WPF.
Model:
// Models/Product.cs (Same as MVC)
ViewModel:
// ViewModels/ProductViewModel.cs
using System.ComponentModel;
using System.Runtime.CompilerServices;
public class ProductViewModel : INotifyPropertyChanged
{
private readonly IProductService _productService;
private Product _product;
private string _errorMessage;
public ProductViewModel(IProductService productService)
{
_productService = productService ?? throw new ArgumentNullException(nameof(productService));
LoadProductCommand = new RelayCommand(LoadProduct);
}
public Product Product
{
get => _product;
set
{
_product = value;
OnPropertyChanged();
}
}
public string ErrorMessage
{
get => _errorMessage;
set
{
_errorMessage = value;
OnPropertyChanged();
}
}
public RelayCommand LoadProductCommand { get; }
private void LoadProduct(object parameter)
{
try
{
int productId = (int)parameter;
Product = _productService.GetProductById(productId);
}
catch (Exception ex)
{
ErrorMessage = "Failed to load product.";
// Log exception
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
RelayCommand (for commanding):
// Commands/RelayCommand.cs
public class RelayCommand : ICommand
{
private readonly Action<object> _execute;
private readonly Func<object, bool> _canExecute;
public RelayCommand(Action<object> execute, Func<object, bool> canExecute = null)
{
_execute = execute ?? throw new ArgumentNullException(nameof(execute));
_canExecute = canExecute;
}
public bool CanExecute(object parameter) => _canExecute?.Invoke(parameter) ?? true;
public void Execute(object parameter) => _execute(parameter);
public event EventHandler CanExecuteChanged
{
add => CommandManager.RequerySuggested += value;
remove => CommandManager.RequerySuggested -= value;
}
}
View (XAML):
<!-- Views/ProductView.xaml -->
<Window x:Class="ECommerceApp.Views.ProductView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<StackPanel>
<TextBox Text="{Binding Product.Id}" />
<TextBlock Text="{Binding Product.Name}" />
<TextBlock Text="{Binding Product.Price, StringFormat=C}" />
<TextBlock Text="{Binding ErrorMessage, Mode=OneWay}" Foreground="Red" />
<Button Content="Load Product" Command="{Binding LoadProductCommand}" CommandParameter="1" />
</StackPanel>
</Window>
2.3.3 Exception Handling
Handle exceptions in the ViewModel to update the UI.
Use data binding to display error messages.
Log exceptions for debugging.
2.3.4 Pros, Cons, and Alternatives
Pros:
Ideal for data-binding frameworks like WPF.
Decouples View from business logic.
Highly testable.
Cons:
Complex for small applications.
Requires understanding of data binding.
Alternatives:
MVC: Simpler for web apps.
MVP: For non-data-binding scenarios.
3. Layered / N-Tier Architecture Patterns
3.1 Overview and Real-World Analogy
Layered/N-tier architecture organizes an application into layers (e.g., Presentation, Business, Data) to separate concerns and improve maintainability.
Analogy: Think of a factory where raw materials (Data Layer) are processed by machines (Business Layer) and packaged for customers (Presentation Layer).
3.2 Implementation in ASP.NET Core with SQL Server
Let’s build a layered e-commerce application with:
Presentation Layer: ASP.NET Core MVC.
Business Layer: Service classes.
Data Layer: Entity Framework Core with SQL Server.
Data Layer (Entity Framework Core):
// Data/Entities/Product.cs
public class Product
{
public int Id { get; set; }
public string Name { get; set; }
public decimal Price { get; set; }
public string Description { get; set; }
}
// Data/AppDbContext.cs
using Microsoft.EntityFrameworkCore;
public class AppDbContext : DbContext
{
public DbSet<Product> Products { get; set; }
public AppDbContext(DbContextOptions<AppDbContext> options) : base(options) { }
}
Business Layer:
// Services/IProductService.cs
public interface IProductService
{
Task<IEnumerable<Product>> GetAllProductsAsync();
}
// Services/ProductService.cs
public class ProductService : IProductService
{
private readonly AppDbContext _context;
public ProductService(AppDbContext context)
{
_context = context ?? throw new ArgumentNullException(nameof(context));
}
public async Task<IEnumerable<Product>> GetAllProductsAsync()
{
try
{
return await _context.Products.ToListAsync();
}
catch (DbException ex)
{
// Log exception
throw new ApplicationException("Database error occurred.", ex);
}
}
}
Presentation Layer (MVC Controller):
// Controllers/ProductsController.cs
public class ProductsController : Controller
{
private readonly IProductService _productService;
public ProductsController(IProductService productService)
{
_productService = productService ?? throw new ArgumentNullException(nameof(productService));
}
public async Task<IActionResult> Index()
{
try
{
var products = await _productService.GetAllProductsAsync();
return View(products);
}
catch (ApplicationException ex)
{
// Log exception
return StatusCode(500, "An error occurred while fetching products.");
}
}
}
Startup Configuration:
// Program.cs
builder.Services.AddDbContext<AppDbContext>(options =>
options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")));
builder.Services.AddScoped<IProductService, ProductService>();
Connection String (appsettings.json):
{
"ConnectionStrings": {
"DefaultConnection": "Server=localhost;Database=ECommerceDb;Trusted_Connection=True;"
}
}
3.3 Exception Handling
Catch database-specific exceptions (e.g., DbException) in the Data Layer.
Wrap in custom exceptions (ApplicationException) for the Business Layer.
Handle in the Presentation Layer to return user-friendly responses.
3.4 Pros, Cons, and Alternatives
Pros:
Clear separation of concerns.
Easy to maintain and scale.
Reusable business logic.
Cons:
Can lead to over-engineering for small apps.
Performance overhead due to layering.
Alternatives:
Monolithic Architecture: Simpler for small apps.
Microservices: For distributed systems.
4. Microservices Design Patterns
Microservices break an application into small, independent services. Let’s explore key patterns: API Gateway, CQRS, and Event Sourcing.
4.1 Overview and Real-World Analogy
Microservices enable modular, scalable systems where each service handles a specific function (e.g., Order Service, Payment Service).
Analogy: Think of a shopping mall with specialized stores (services) for clothing, electronics, etc., communicating via a central entrance (API Gateway).
4.2 API Gateway Pattern
4.2.1 Implementation in ASP.NET Core
An API Gateway acts as a single entry point for client requests, routing them to appropriate microservices.
API Gateway (ASP.NET Core):
// Startup.cs
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
services.AddHttpClient();
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
app.UseRouting();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}
}
// Controllers/ProductGatewayController.cs
[Route("api/[controller]")]
[ApiController]
public class ProductGatewayController : ControllerBase
{
private readonly HttpClient _httpClient;
public ProductGatewayController(HttpClient httpClient)
{
_httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
}
[HttpGet]
public async Task<IActionResult> GetProducts()
{
try
{
var response = await _httpClient.GetAsync("http://product-service/api/products");
response.EnsureSuccessStatusCode();
var content = await response.Content.ReadAsStringAsync();
return Ok(content);
}
catch (HttpRequestException ex)
{
// Log exception
return StatusCode(503, "Product service is unavailable.");
}
}
}
Product Microservice:
// ProductService/Controllers/ProductsController.cs
[Route("api/[controller]")]
[ApiController]
public class ProductsController : ControllerBase
{
private readonly AppDbContext _context;
public ProductsController(AppDbContext context)
{
_context = context;
}
[HttpGet]
public async Task<IActionResult> Get()
{
try
{
var products = await _context.Products.ToListAsync();
return Ok(products);
}
catch (DbException ex)
{
// Log exception
return StatusCode(500, "Database error.");
}
}
}
4.2.2 Exception Handling
Handle HTTP errors in the API Gateway (e.g., service unavailable).
Implement circuit breakers (e.g., using Polly) to handle repeated failures.
Log errors for monitoring.
4.2.3 Pros, Cons, and Alternatives
Pros:
Simplifies client communication.
Centralizes cross-cutting concerns (e.g., authentication).
Cons:
Single point of failure if not designed properly.
Additional latency.
Alternatives:
Direct Client-to-Service Communication: For simple systems.
Service Mesh: For advanced routing and observability.
4.3 Command Query Responsibility Segregation (CQRS)
4.3.1 Implementation with MediatR in ASP.NET Core
CQRS separates read (Query) and write (Command) operations, optimizing performance and scalability.
Command (Create Product):
// Commands/CreateProductCommand.cs
public class CreateProductCommand : IRequest<int>
{
public string Name { get; set; }
public decimal Price { get; set; }
public string Description { get; set; }
}
// Handlers/CreateProductCommandHandler.cs
public class CreateProductCommandHandler : IRequestHandler<CreateProductCommand, int>
{
private readonly AppDbContext _context;
public CreateProductCommandHandler(AppDbContext context)
{
_context = context;
}
public async Task<int> Handle(CreateProductCommand request, CancellationToken cancellationToken)
{
try
{
var product = new Product
{
Name = request.Name,
Price = request.Price,
Description = request.Description
};
_context.Products.Add(product);
await _context.SaveChangesAsync(cancellationToken);
return product.Id;
}
catch (DbException ex)
{
// Log exception
throw new ApplicationException("Failed to create product.", ex);
}
}
}
Query (Get Products):
// Queries/GetProductsQuery.cs
public class GetProductsQuery : IRequest<IEnumerable<Product>> { }
// Handlers/GetProductsQueryHandler.cs
public class GetProductsQueryHandler : IRequestHandler<GetProductsQuery, IEnumerable<Product>>
{
private readonly AppDbContext _context;
public GetProductsQueryHandler(AppDbContext context)
{
_context = context;
}
public async Task<IEnumerable<Product>> Handle(GetProductsQuery request, CancellationToken cancellationToken)
{
try
{
return await _context.Products.AsNoTracking().ToListAsync(cancellationToken);
}
catch (DbException ex)
{
// Log exception
throw new ApplicationException("Failed to fetch products.", ex);
}
}
}
Controller:
// Controllers/ProductsController.cs
[Route("api/[controller]")]
[ApiController]
public class ProductsController : ControllerBase
{
private readonly IMediator _mediator;
public ProductsController(IMediator mediator)
{
_mediator = mediator ?? throw new ArgumentNullException(nameof(mediator));
}
[HttpGet]
public async Task<IActionResult> Get()
{
try
{
var products = await _mediator.Send(new GetProductsQuery());
return Ok(products);
}
catch (ApplicationException ex)
{
// Log exception
return StatusCode(500, ex.Message);
}
}
[HttpPost]
public async Task<IActionResult> Post([FromBody] CreateProductCommand command)
{
try
{
var productId = await _mediator.Send(command);
return CreatedAtAction(nameof(Get), new { id = productId }, null);
}
catch (ApplicationException ex)
{
// Log exception
return StatusCode(500, ex.Message);
}
}
}
Startup Configuration:
// Program.cs
builder.Services.AddMediatR(cfg => cfg.RegisterServicesFromAssembly(typeof(Program).Assembly));
4.3.2 Exception Handling
Handle exceptions in command/query handlers.
Use MediatR’s pipeline behaviors for centralized error handling.
Log errors for monitoring.
4.3.3 Pros, Cons, and Alternatives
Pros:
Optimizes read/write performance.
Simplifies complex domain logic.
Cons:
Increases complexity.
Requires careful synchronization.
Alternatives:
CRUD-Based Architecture: Simpler for small apps.
Event Sourcing: For event-driven systems.
4.4 Event Sourcing
4.4.1 Implementation with EventStoreDB
Event Sourcing stores the state of an application as a sequence of events, allowing reconstruction of state by replaying events.
Event:
// Events/ProductCreatedEvent.cs
public class ProductCreatedEvent
{
public int Id { get; set; }
public string Name { get; set; }
public decimal Price { get; set; }
public string Description { get; set; }
}
Aggregate:
// Aggregates/ProductAggregate.cs
public class ProductAggregate
{
public int Id { get; private set; }
public string Name { get; private set; }
public decimal Price { get; private set; }
public string Description { get; private set; }
public void Apply(ProductCreatedEvent @event)
{
Id = @event.Id;
Name = @event.Name;
Price = @event.Price;
Description = @event.Description;
}
}
Event Store (Using EventStoreDB):
// Infrastructure/EventStoreRepository.cs
public class EventStoreRepository
{
private readonly IEventStoreConnection _connection;
public EventStoreRepository(IEventStoreConnection connection)
{
_connection = connection ?? throw new ArgumentNullException(nameof(connection));
}
public async Task SaveEventAsync<T>(T @event, string streamName)
{
try
{
var eventData = new EventData(
Guid.NewGuid(),
typeof(T).Name,
true,
Encoding.UTF8.GetBytes(JsonSerializer.Serialize(@event)),
null
);
await _connection.AppendToStreamAsync(streamName, ExpectedVersion.Any, eventData);
}
catch (Exception ex)
{
// Log exception
throw new ApplicationException("Failed to save event.", ex);
}
}
public async Task<ProductAggregate> GetAggregateAsync(string streamName)
{
try
{
var aggregate = new ProductAggregate();
var events = await _connection.ReadStreamEventsForwardAsync(streamName, 0, 4096, false);
foreach (var evt in events.Events)
{
var eventData = JsonSerializer.Deserialize<ProductCreatedEvent>(
Encoding.UTF8.GetString(evt.Event.Data));
aggregate.Apply(eventData);
}
return aggregate;
}
catch (Exception ex)
{
// Log exception
throw new ApplicationException("Failed to load aggregate.", ex);
}
}
}
Service:
// Services/ProductService.cs
public class ProductService
{
private readonly EventStoreRepository _eventStore;
public ProductService(EventStoreRepository eventStore)
{
_eventStore = eventStore;
}
public async Task CreateProductAsync(ProductCreatedEvent @event)
{
await _eventStore.SaveEventAsync(@event, $"product-{@event.Id}");
}
public async Task<ProductAggregate> GetProductAsync(int id)
{
return await _eventStore.GetAggregateAsync($"product-{id}");
}
}
4.4.2 Exception Handling
Handle event store connection errors.
Validate events before saving.
Log errors for debugging.
4.4.3 Pros, Cons, and Alternatives
Pros:
Provides a complete audit trail.
Enables state reconstruction.
Cons:
Complex to implement.
Requires significant storage.
Alternatives:
Traditional CRUD: Simpler for basic apps.
CQRS without Event Sourcing: For less complexity.
5. Repository and Unit of Work Patterns
5.1 Repository Pattern
5.1.1 Overview and Real-World Analogy
The Repository Pattern abstracts data access, providing a collection-like interface for querying and persisting data.
Analogy: Think of a librarian (Repository) managing a library’s books (data) for users.
5.1.2 Implementation in ASP.NET Core with EF Core
Repository Interface:
// Repositories/IProductRepository.cs
public interface IProductRepository
{
Task<IEnumerable<Product>> GetAllAsync();
Task<Product> GetByIdAsync(int id);
Task AddAsync(Product product);
}
Repository Implementation:
// Repositories/ProductRepository.cs
public class ProductRepository : IProductRepository
{
private readonly AppDbContext _context;
public ProductRepository(AppDbContext context)
{
_context = context ?? throw new ArgumentNullException(nameof(context));
}
public async Task<IEnumerable<Product>> GetAllAsync()
{
try
{
return await _context.Products.AsNoTracking().ToListAsync();
}
catch (DbException ex)
{
// Log exception
throw new ApplicationException("Failed to fetch products.", ex);
}
}
public async Task<Product> GetByIdAsync(int id)
{
try
{
return await _context.Products.FindAsync(id)
?? throw new KeyNotFoundException($"Product with ID {id} not found.");
}
catch (DbException ex)
{
// Log exception
throw new ApplicationException("Database error.", ex);
}
}
public async Task AddAsync(Product product)
{
try
{
await _context.Products.AddAsync(product);
await _context.SaveChangesAsync();
}
catch (DbException ex)
{
// Log exception
throw new ApplicationException("Failed to add product.", ex);
}
}
}
5.1.3 Exception Handling
Catch database-specific exceptions.
Throw meaningful custom exceptions.
Log errors for debugging.
5.1.4 Pros, Cons, and Alternatives
Pros:
Abstracts data access.
Improves testability.
Cons:
Adds abstraction overhead.
Can duplicate EF Core functionality.
Alternatives:
Direct EF Core Usage: For simpler apps.
Active Record: For tightly coupled data models.
5.2 Unit of Work Pattern
5.2.1 Overview and Real-World Analogy
The Unit of Work Pattern coordinates multiple repository operations, ensuring atomicity.
Analogy: A bank teller (Unit of Work) processes multiple transactions (Repository operations) as a single unit.
5.2.2 Implementation with EF Core
Unit of Work Interface:
// Repositories/IUnitOfWork.cs
public interface IUnitOfWork : IDisposable
{
IProductRepository Products { get; }
Task<int> CompleteAsync();
}
Unit of Work Implementation:
// Repositories/UnitOfWork.cs
public class UnitOfWork : IUnitOfWork
{
private readonly AppDbContext _context;
public IProductRepository Products { get; }
public UnitOfWork(AppDbContext context)
{
_context = context ?? throw new ArgumentNullException(nameof(context));
Products = new ProductRepository(_context);
}
public async Task<int> CompleteAsync()
{
try
{
return await _context.SaveChangesAsync();
}
catch (DbUpdateException ex)
{
// Log exception
throw new ApplicationException("Failed to save changes.", ex);
}
}
public void Dispose()
{
_context.Dispose();
}
}
Usage:
// Services/ProductService.cs
public class ProductService
{
private readonly IUnitOfWork _unitOfWork;
public ProductService(IUnitOfWork unitOfWork)
{
_unitOfWork = unitOfWork ?? throw new ArgumentNullException(nameof(unitOfWork));
}
public async Task AddProductAsync(Product product)
{
try
{
await _unitOfWork.Products.AddAsync(product);
await _unitOfWork.CompleteAsync();
}
catch (ApplicationException ex)
{
// Log exception
throw;
}
}
}
Startup Configuration:
// Program.cs
builder.Services.AddScoped<IUnitOfWork, UnitOfWork>();
builder.Services.AddScoped<IProductRepository, ProductRepository>();
5.2.3 Exception Handling
Handle database exceptions in CompleteAsync.
Ensure proper disposal of resources.
Log errors for debugging.
5.2.4 Pros, Cons, and Alternatives
Pros:
Ensures atomicity.
Simplifies transaction management.
Cons:
Adds complexity.
EF Core’s DbContext already acts as a Unit of Work.
Alternatives:
Direct DbContext Usage: For simpler apps.
TransactionScope: For explicit transaction management.
6. Dependency Injection and IoC Containers
6.1 Overview and Real-World Analogy
Dependency Injection (DI) injects dependencies into a class, while Inversion of Control (IoC) containers manage object creation and lifetimes.
Analogy: A chef (class) needs ingredients (dependencies). Instead of growing them, a supplier (IoC container) delivers them.
6.2 Implementation in .NET Core DI
Service Registration:
// Program.cs
builder.Services.AddScoped<IProductService, ProductService>();
builder.Services.AddDbContext<AppDbContext>(options =>
options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")));
Usage:
// Controllers/ProductsController.cs
public class ProductsController : ControllerBase
{
private readonly IProductService _productService;
public ProductsController(IProductService productService)
{
_productService = productService ?? throw new ArgumentNullException(nameof(productService));
}
[HttpGet]
public async Task<IActionResult> Get()
{
try
{
var products = await _productService.GetAllProductsAsync();
return Ok(products);
}
catch (ApplicationException ex)
{
// Log exception
return StatusCode(500, ex.Message);
}
}
}
6.3 Implementation in Java Spring
Spring Bean Configuration:
// Config/AppConfig.java
@Configuration
public class AppConfig {
@Bean
public ProductService productService() {
return new ProductService();
}
}
Service:
// Service/ProductService.java
public class ProductService {
public List<Product> getAllProducts() {
// Simulated data
return Arrays.asList(
new Product(1, "Laptop", 999.99, "High-performance laptop"),
new Product(2, "Phone", 499.99, "Latest smartphone")
);
}
}
Controller:
// Controller/ProductController.java
@RestController
@RequestMapping("/api/products")
public class ProductController {
private final ProductService productService;
@Autowired
public ProductController(ProductService productService) {
this.productService = Objects.requireNonNull(productService);
}
@GetMapping
public ResponseEntity<List<Product>> getProducts() {
try {
return ResponseEntity.ok(productService.getAllProducts());
} catch (Exception e) {
// Log exception
return ResponseEntity.status(500).body(null);
}
}
}
6.4 Exception Handling
Validate dependencies in constructors.
Handle service-specific exceptions in controllers/services.
Log errors for debugging.
6.5 Pros, Cons, and Alternatives
Pros:
Promotes loose coupling.
Enhances testability.
Cons:
Adds configuration complexity.
Can obscure dependency flow.
Alternatives:
Service Locator: For dynamic dependency resolution.
Manual Dependency Management: For small apps.
7. Service Locator Pattern
7.1 Overview and Real-World Analogy
The Service Locator Pattern provides a centralized registry for accessing services, but it’s considered an anti-pattern in modern development due to hidden dependencies.
Analogy: A hotel concierge (Service Locator) provides guests with services (dependencies) on demand, but guests don’t know where services come from.
7.2 Implementation in ASP.NET Core
Service Locator:
// Infrastructure/ServiceLocator.cs
public class ServiceLocator
{
private readonly IServiceProvider _serviceProvider;
public ServiceLocator(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider));
}
public T GetService<T>()
{
try
{
return _serviceProvider.GetService<T>()
?? throw new InvalidOperationException($"Service of type {typeof(T).Name} not found.");
}
catch (Exception ex)
{
// Log exception
throw new ApplicationException("Failed to resolve service.", ex);
}
}
}
Usage:
// Controllers/ProductsController.cs
public class ProductsController : ControllerBase
{
private readonly ServiceLocator _serviceLocator;
public ProductsController(ServiceLocator serviceLocator)
{
_serviceLocator = serviceLocator;
}
[HttpGet]
public IActionResult Get()
{
try
{
var productService = _serviceLocator.GetService<IProductService>();
var products = productService.GetAllProducts();
return Ok(products);
}
catch (ApplicationException ex)
{
// Log exception
return StatusCode(500, ex.Message);
}
}
}
Startup Configuration:
// Program.cs
builder.Services.AddSingleton<ServiceLocator>();
7.3 Exception Handling
Validate service resolution.
Handle missing service exceptions.
Log errors for debugging.
7.4 Pros, Cons, and Alternatives
Pros:
Centralized dependency management.
Flexible for dynamic resolution.
Cons:
Hides dependencies, making code harder to understand.
Complicates unit testing.
Alternatives:
Dependency Injection: Preferred for transparency.
Factory Pattern: For controlled object creation.
8. Best Practices for Architectural and Enterprise Patterns
8.1 General Guidelines
Keep It Simple: Avoid over-engineering for small projects.
Follow SOLID Principles: Ensure single responsibility, open-closed, etc.
Use Patterns Judiciously: Match patterns to project requirements.
8.2 Performance Optimization
Cache frequently accessed data.
Use asynchronous programming (e.g., async/await).
Optimize database queries with indexing.
8.3 Scalability and Maintainability
Design for horizontal scaling in microservices.
Use clear naming conventions.
Document architecture decisions.
8.4 Testing Strategies
Write unit tests for services and repositories.
Use integration tests for microservices.
Mock dependencies using Moq or similar frameworks.
9. Conclusion
9.1 Recap of Key Patterns
We covered essential architectural and enterprise patterns:
MVC, MVP, MVVM: For presentation logic.
Layered/N-tier: For structured applications.
Microservices (API Gateway, CQRS, Event Sourcing): For distributed systems.
Repository and Unit of Work: For data access.
DI/IoC and Service Locator: For dependency management.
9.2 Choosing the Right Pattern
Use MVC/MVVM for web/desktop apps.
Use Layered/N-tier for monolithic apps.
Use Microservices for distributed, scalable systems.
Use Repository/Unit of Work with DI for clean data access.
9.3 Next Steps in Your Learning Journey
Explore advanced topics like Domain-Driven Design (DDD).
Experiment with tools like Docker for microservices.
Contribute to open-source projects to apply these patterns.
10. Resources and Further Reading
Microsoft Docs: ASP.NET Core
Spring Framework Documentation
Patterns of Enterprise Application Architecture by Martin Fowler
EventStoreDB Documentation
🚀 Expand Your Learning Journey
📘 Master Software Design Patterns: Complete Course Outline (.NET & Java) | 🎯 Free Learning Zone
📘 Master Software Design Patterns: Complete Course Outline (.NET & Java) 🎯 Visit Free Learning Zone
No comments:
Post a Comment
Thanks for your valuable comment...........
Md. Mominul Islam