Monolithic Architecture is a traditional approach to building software applications where all components—such as the user interface, business logic, and data access—are packaged together into a single, unified codebase. While monolithic applications can be easier to develop and deploy, they often become difficult to maintain and scale as they grow.
In this blog, we'll explore how to build a Product and Order service using a Monolithic Architecture in .NET Core. We'll explain each part of the application and provide code examples to demonstrate how everything fits together.
What is Monolithic Architecture?
Monolithic Architecture is a software design pattern where the entire application is built as a single, unified unit. In a monolithic application:
- All modules (like authentication, business logic, and data access) are tightly coupled.
- Deployment is done as a single unit, meaning any changes require redeploying the entire application.
- Scalability can be a challenge, as scaling one part of the application often means scaling the whole application.
Despite these drawbacks, monolithic applications are straightforward to develop and deploy, especially for smaller projects or teams.
Setting up the Project
Let's build a Product and Order service using a Monolithic Architecture. In this example, the entire application will be contained within a single .NET Core project. We'll structure the application using folders to separate concerns but keep everything within the same codebase.
1. Project Structure
In a monolithic application, we usually organize the codebase into folders to separate concerns. Here’s how we might structure our Product and Order service.
/ProductOrderService
/Controllers
- ProductsController.cs
- OrdersController.cs
/Models
- Product.cs
- Order.cs
/Services
- ProductService.cs
- OrderService.cs
/Data
- ApplicationDbContext.cs
- ProductRepository.cs
- OrderRepository.cs
- Startup.cs
- Program.cs
2. Models
The Models folder contains the domain entities that represent the data structure of the application.
Product and Order Models
namespace ProductOrderService.Models
{
public class Product
{
public Guid Id { get; set; }
public string Name { get; set; }
public decimal Price { get; set; }
}
public class Order
{
public Guid Id { get; set; }
public Guid ProductId { get; set; }
public int Quantity { get; set; }
public DateTime OrderDate { get; set; }
public decimal Total => Quantity * Product.Price;
public Product Product { get; set; }
}
}
3. Data Access
The Data folder includes the ApplicationDbContext class for managing database interactions and repository classes for data access.
ApplicationDbContext
using Microsoft.EntityFrameworkCore;
using ProductOrderService.Models;
namespace ProductOrderService.Data
{
public class ApplicationDbContext : DbContext
{
public DbSet<Product> Products { get; set; }
public DbSet<Order> Orders { get; set; }
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
: base(options)
{
}
}
}
Repositories
using ProductOrderService.Models;
namespace ProductOrderService.Data
{
public class ProductRepository
{
private readonly ApplicationDbContext _context;
public ProductRepository(ApplicationDbContext context)
{
_context = context;
}
public async Task<Product> GetByIdAsync(Guid id)
{
return await _context.Products.FindAsync(id);
}
public async Task<IEnumerable<Product>> GetAllAsync()
{
return await _context.Products.ToListAsync();
}
public async Task AddAsync(Product product)
{
await _context.Products.AddAsync(product);
await _context.SaveChangesAsync();
}
public async Task UpdateAsync(Product product)
{
_context.Products.Update(product);
await _context.SaveChangesAsync();
}
public async Task DeleteAsync(Guid id)
{
var product = await GetByIdAsync(id);
if (product != null)
{
_context.Products.Remove(product);
await _context.SaveChangesAsync();
}
}
}
public class OrderRepository
{
private readonly ApplicationDbContext _context;
public OrderRepository(ApplicationDbContext context)
{
_context = context;
}
public async Task<Order> GetByIdAsync(Guid id)
{
return await _context.Orders.Include(o => o.Product).FirstOrDefaultAsync(o => o.Id == id);
}
public async Task<IEnumerable<Order>> GetAllAsync()
{
return await _context.Orders.Include(o => o.Product).ToListAsync();
}
public async Task AddAsync(Order order)
{
await _context.Orders.AddAsync(order);
await _context.SaveChangesAsync();
}
public async Task UpdateAsync(Order order)
{
_context.Orders.Update(order);
await _context.SaveChangesAsync();
}
public async Task DeleteAsync(Guid id)
{
var order = await GetByIdAsync(id);
if (order != null)
{
_context.Orders.Remove(order);
await _context.SaveChangesAsync();
}
}
}
}
4. Services
The Services folder contains business logic and application services that orchestrate the actions performed on the models.
Product and Order Services
using ProductOrderService.Data;
using ProductOrderService.Models;
namespace ProductOrderService.Services
{
public class ProductService
{
private readonly ProductRepository _productRepository;
public ProductService(ProductRepository productRepository)
{
_productRepository = productRepository;
}
public async Task<Product> GetProductByIdAsync(Guid id)
{
return await _productRepository.GetByIdAsync(id);
}
public async Task AddProductAsync(Product product)
{
await _productRepository.AddAsync(product);
}
// Additional methods...
}
public class OrderService
{
private readonly OrderRepository _orderRepository;
private readonly ProductRepository _productRepository;
public OrderService(OrderRepository orderRepository, ProductRepository productRepository)
{
_orderRepository = orderRepository;
_productRepository = productRepository;
}
public async Task<Order> CreateOrderAsync(Guid productId, int quantity)
{
var product = await _productRepository.GetByIdAsync(productId);
if (product == null) throw new Exception("Product not found");
var order = new Order
{
Id = Guid.NewGuid(),
ProductId = productId,
Quantity = quantity,
OrderDate = DateTime.UtcNow,
Product = product
};
await _orderRepository.AddAsync(order);
return order;
}
// Additional methods...
}
}
5. Controllers
The Controllers folder contains the API endpoints that expose the application’s functionality to external clients.
Product and Order Controllers
using Microsoft.AspNetCore.Mvc;
using ProductOrderService.Models;
using ProductOrderService.Services;
namespace ProductOrderService.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class ProductsController : ControllerBase
{
private readonly ProductService _productService;
public ProductsController(ProductService productService)
{
_productService = productService;
}
[HttpGet("{id}")]
public async Task<IActionResult> GetProduct(Guid id)
{
var product = await _productService.GetProductByIdAsync(id);
if (product == null) return NotFound();
return Ok(product);
}
[HttpPost]
public async Task<IActionResult> CreateProduct([FromBody] Product product)
{
await _productService.AddProductAsync(product);
return CreatedAtAction(nameof(GetProduct), new { id = product.Id }, product);
}
// Additional actions...
}
[Route("api/[controller]")]
[ApiController]
public class OrdersController : ControllerBase
{
private readonly OrderService _orderService;
public OrdersController(OrderService orderService)
{
_orderService = orderService;
}
[HttpPost]
public async Task<IActionResult> CreateOrder(Guid productId, int quantity)
{
var order = await _orderService.CreateOrderAsync(productId, quantity);
return CreatedAtAction(nameof(GetOrder), new { id = order.Id }, order);
}
[HttpGet("{id}")]
public async Task<IActionResult> GetOrder(Guid id)
{
var order = await _orderService.GetOrderByIdAsync(id);
if (order == null) return NotFound();
return Ok(order);
}
// Additional actions...
}
}
6. Dependency Injection and Startup Configuration
Finally, in the Startup.cs file, we configure dependency injection and set up the middleware pipeline.
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));
services.AddScoped<ProductRepository>();
services.AddScoped<OrderRepository>();
services.AddScoped<ProductService>();
services.AddScoped<OrderService>();
services.AddControllers();
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseRouting();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}
}
Conclusion
In a Monolithic Architecture, the entire application is built and deployed as a single unit. This approach can simplify development and deployment, especially for smaller applications. However, as the application grows, managing and scaling a monolithic application can become increasingly challenging.
The Product and Order service example demonstrates how to structure a monolithic application in .NET Core. While this architecture may work well for simple or smaller applications, consider using more modular architectures like Onion or Microservices for larger, more complex applications.