Self-contained Systems (SCS) in .NET
David Shergilashvili
???? Engineering Manager | ??? .NET Solution Architect | ?? Software Developer | ?? Herding Cats and Microservices
Introduction
Self-contained Systems (SCS) is an architectural paradigm that has evolved as a response to the challenges of modern software complexity. SCS combines the best characteristics of monolithic and microservices architectures while minimizing their drawbacks.
Philosophical Foundations of SCS
The concept of SCS is based on several fundamental principles:
Autonomy: Each SCS is an independent, self-sufficient unit capable of functioning with minimal dependency on other systems.
Business-Oriented: SCSs reflect real business domains rather than technical abstractions, ensuring a close relationship between business requirements and technical implementation.
Loosely Coupled: Interaction between SCSs is minimal and well-defined, reducing system interdependencies.
Technological Diversity: Each SCS can use a different technology stack, providing opportunities for innovation and optimization.
Independent UI: SCSs often include UI components, enabling end-to-end functionality.
SCS vs Microservices: Nuanced Differences
Although superficially similar, SCS and microservices differ significantly:
Scale and Granularity:
SCS: Larger, encompassing an entire business domain.
Microservices: Smaller, focused on a specific functionality.
UI Integration:
SCS: Often includes its UI.
Microservices: Usually do not include UI, focusing on backend functionality.
Degree of Independence:
SCS: Highly autonomous, with minimal external dependencies.
Microservices: Often tightly integrated with other services.
Data Management:
- SCS: Typically owns a complete database for the domain.
- Microservices: Tends toward data fragmentation.
Deployment Strategy:
- SCS: Can be deployed as a whole system.
- Microservices: Often require more complex orchestration.
Architectural Principles of SCS in the Context of .NET
The .NET ecosystem offers rich tools and frameworks for implementing SCS. Let’s examine the main principles and their realization in .NET.
Ensuring Autonomy
Autonomy is the cornerstone of SCS. In .NET, this can be achieved by:
Isolated Runtime:
- Using .NET self-contained deployment, ensuring the application runs with all necessary dependencies.
- Using Docker for containerization, providing full isolation.
Configuration Isolation:
- Utilizing .NET’s configuration system (`IConfiguration`) individually for each SCS.
- Using Azure App Configuration or AWS AppConfig for centralized but isolated configuration.
Resource Isolation:
- Employing Azure Resource Groups or AWS Resource Groups to isolate resources for each SCS.
Example: Dockerfile for an SCS
领英推荐
FROM mcr.microsoft.com/dotnet/aspnet:6.0 AS base
WORKDIR /app
EXPOSE 80
EXPOSE 443
FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build
WORKDIR /src
COPY ["OrderingSCS.csproj", "./"]
RUN dotnet restore "OrderingSCS.csproj"
COPY . .
RUN dotnet build "OrderingSCS.csproj" -c Release -o /app/build
FROM build AS publish
RUN dotnet publish "OrderingSCS.csproj" -c Release -o /app/publish /p:UseAppHost=false
FROM base AS final
WORKDIR /app
COPY --from=publish /app/publish .
ENTRYPOINT ["dotnet", "OrderingSCS.dll"]
API-First Approach and Communication
SCSs interact via well-defined APIs. .NET provides multiple options for implementing this:
RESTful APIs:
- Use ASP.NET Core Web API to create RESTful endpoints.
- Implement API versioning with the Microsoft.AspNetCore.Mvc.Versioning package.
gRPC:
- Leverage .NET's gRPC support for high-performance RPC communication.
- Define contracts using Protobuf.
GraphQL:
- Use the Hot Chocolate framework to create flexible APIs.
- Implement GraphQL schemas and resolvers.
Asynchronous Communication:
- Use Azure Service Bus or RabbitMQ for messaging.
- Integrate MassTransit for message abstraction.
Example: gRPC Service in SCS
public class OrderService : OrderServiceBase
{
private readonly IOrderRepository _repository;
private readonly ILogger<OrderService> _logger;
private readonly IMapper _mapper;
public OrderService(IOrderRepository repository, ILogger<OrderService> logger, IMapper mapper)
{
_repository = repository;
_logger = logger;
_mapper = mapper;
}
public override async Task<CreateOrderResponse> CreateOrder(CreateOrderRequest request, ServerCallContext context)
{
_logger.LogInformation("Creating order for customer {CustomerId}", request.CustomerId);
var order = _mapper.Map<Order>(request);
var createdOrder = await _repository.CreateAsync(order);
var response = _mapper.Map<CreateOrderResponse>(createdOrder);
// Publish an event
await PublishOrderCreatedEventAsync(createdOrder);
return response;
}
private async Task PublishOrderCreatedEventAsync(Order order)
{
// Implementation to publish the event
}
}
Encapsulation of Business Logic
A major advantage of SCS is the clear encapsulation of business logic. In .NET, this can be achieved through:
Domain-Driven Design (DDD):
- Implementing Aggregate Roots, Value Objects, and Domain Services.
- Encapsulating business rules in domain models.
CQRS (Command Query Responsibility Segregation):
- Using MediatR to separate commands and queries.
- Separating read and write models.
Specification Pattern:
- Encapsulating business rules in Specification objects.
- Using the Ardalis.Specification library.
Example: Aggregate Root Using DDD Principles
public class Order : AggregateRoot
{
private readonly List<OrderItem> _orderItems = new List<OrderItem>();
public IReadOnlyCollection<OrderItem> OrderItems => _orderItems.AsReadOnly();
public CustomerId CustomerId { get; private set; }
public Money TotalAmount { get; private set; }
public OrderStatus Status { get; private set; }
private Order() { } // For EF Core
public static Order Create(CustomerId customerId)
{
var order = new Order
{
Id = OrderId.New(),
CustomerId = customerId,
Status = OrderStatus.Created,
TotalAmount = Money.Zero(CurrencyCode.USD)
};
order.AddDomainEvent(new OrderCreatedDomainEvent(order));
return order;
}
public void AddOrderItem(ProductId productId, int quantity, Money unitPrice)
{
if (Status != OrderStatus.Created)
{
throw new OrderDomainException("Cannot add items to non-draft orders");
}
var existingItem = _orderItems.FirstOrDefault(i => i.ProductId == productId);
if (existingItem != null)
{
existingItem.IncreaseQuantity(quantity);
}
else
{
var newItem = new OrderItem(productId, quantity, unitPrice);
_orderItems.Add(newItem);
}
RecalculateTotalAmount();
AddDomainEvent(new OrderItemAddedDomainEvent(this, productId, quantity));
}
private void RecalculateTotalAmount()
{
TotalAmount = _orderItems.Aggregate(
Money.Zero(CurrencyCode.USD),
(total, item) => total + item.TotalPrice);
}
public void ConfirmOrder()
{
if (Status != OrderStatus.Created)
{
throw new OrderDomainException("Only draft orders can be confirmed");
}
if (!_orderItems.Any())
{
throw new OrderDomainException("Cannot confirm an empty order");
}
Status = OrderStatus.Confirmed;
AddDomainEvent(new OrderConfirmedDomainEvent(this));
}
}
This example demonstrates how business logic can be encapsulated within an Aggregate Root using DDD principles. This approach ensures centralized management of business rules and maintains domain integrity.
Continued Sections
Asynchronous Communication and Event-Driven Architecture in .NET
Conclusion
Self-contained Systems (SCS) architecture is a powerful approach for building complex systems, especially in the .NET ecosystem. It offers a balance between modularity, scalability, and flexibility.
In this article, we have explored the fundamental principles of SCS, their practical implementation in .
NET, and analyzed critical aspects such as integration, security, monitoring, scaling, and performance optimization. Successful adoption of SCS architecture requires the team's learning of new approaches and technologies, but in the long run, it provides significant advantages in terms of system maintainability, development speed, and scalability.
When adopting SCS, it is crucial to pay attention to proper identification of business domains, optimization of communication between systems, and ensuring the autonomy of each SCS.