Advanced Modeling in .NET
This section delves into sophisticated modeling techniques within the .NET ecosystem, enabling developers to build robust, scalable, and maintainable applications. We will explore advanced concepts such as Domain-Driven Design (DDD), Entity Framework Core advanced features, and asynchronous modeling patterns.
Domain-Driven Design (DDD) Principles
Domain-Driven Design is an approach to software development that emphasizes the understanding and modeling of the core business domain. It focuses on collaboration between technical and business experts to iteratively refine a conceptual model that represents the business domain. Key DDD strategic patterns include:
- Bounded Contexts: Defining clear boundaries within which a particular domain model is applicable. This helps manage complexity in large systems.
- Ubiquitous Language: Establishing a common language shared by developers and domain experts, ensuring that terms used in code directly map to business concepts.
- Context Mapping: Describing the relationships and integrations between different Bounded Contexts.
Tactical DDD patterns provide building blocks for creating expressive domain models:
- Entities: Objects that have a distinct identity that runs through time and different states.
- Value Objects: Objects that describe characteristics or attributes but do not have a conceptual identity.
- Aggregates: Clusters of associated objects treated as a single unit for data changes. An Aggregate Root is the entity that bridges the Aggregate.
- Repositories: Mediators that provide an abstraction for data access, allowing you to query and persist domain objects.
- Domain Services: Operations that don't naturally belong to any specific entity or value object.
- Domain Events: Represent significant occurrences within the domain that other parts of the system might be interested in.
Example: Aggregate Design
Consider an e-commerce system. An Order
Aggregate Root might contain multiple OrderItem
entities. Business rules, such as preventing an order from being modified after it's been shipped, are enforced at the Aggregate Root level.
public class Order
{
public Guid Id { get; private set; }
private List<OrderItem> _items = new List<OrderItem>();
public OrderStatus Status { get; private set; }
public IReadOnlyCollection<OrderItem> Items => _items.AsReadOnly();
public void AddItem(Product product, int quantity)
{
if (Status == OrderStatus.Shipped)
{
throw new InvalidOperationException("Cannot add items to a shipped order.");
}
// ... business logic to add item, check stock, etc.
_items.Add(new OrderItem(product, quantity));
}
public void MarkAsShipped()
{
if (Status != OrderStatus.Processing)
{
throw new InvalidOperationException("Order cannot be shipped in its current state.");
}
Status = OrderStatus.Shipped;
// Raise DomainEvent: OrderShipped
}
}
public class OrderItem
{
public Guid Id { get; private set; }
public Product Product { get; private set; }
public int Quantity { get; private set; }
// Constructor and other logic
}
public enum OrderStatus { Pending, Processing, Shipped, Cancelled }
Entity Framework Core Advanced Features
Entity Framework Core (EF Core) offers powerful features that complement advanced modeling techniques:
- Value Converters: Custom logic to convert between .NET types and database column types, useful for handling enums, complex types, or custom representations.
- TPH (Table-Per-Hierarchy), TPT (Table-Per-Type), and TPC (Table-Per-Concrete-Type) Inheritance Mapping: EF Core supports various strategies for mapping .NET inheritance hierarchies to relational databases.
- Owned Entities: Modeling entities that don't have their own identity and are always owned by another entity (e.g., an
Address
owned by aCustomer
). This is crucial for complex value objects. - Query Types: For querying data that doesn't map to an entity, such as from database views or stored procedures that don't return key properties.
- Complex Types (Legacy concept, Owned Entities are preferred): Grouping properties into a single unit that doesn't have a primary key.
Example: Using Owned Entities and Value Converters
Here's how you might configure an Address
as an owned entity and use a Value Converter for a custom postal code format.
// Model definition
public class Customer
{
public int Id { get; set; }
public string Name { get; set; }
public Address ShippingAddress { get; set; } // Owned Entity
}
public class Address
{
public string Street { get; set; }
public string City { get; set; }
public PostalCode ZipCode { get; set; } // Custom Value Object with Value Converter
}
public class PostalCode
{
public string Value { get; private set; }
public PostalCode(string value) { Value = value; }
// ... validation
}
// EF Core Configuration
public class AppDbContext : DbContext
{
public DbSet<Customer> Customers { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
// Configure Address as Owned Entity
modelBuilder.Entity<Customer>().OwnsOne(c => c.ShippingAddress, sa =>
{
sa.Property(a => a.Street).HasColumnName("ShippingStreet");
sa.Property(a => a.City).HasColumnName("ShippingCity");
// Configure PostalCode Value Object with Value Converter
sa.OwnsOne(a => a.ZipCode, zip =>
{
zip.Property(pc => pc.Value).HasColumnName("ShippingZipCode");
});
});
// Example Value Converter for PostalCode (simplistic)
var postalCodeConverter = new ValueConverter<PostalCode, string>(
v => v.Value,
v => new PostalCode(v)
);
modelBuilder.Entity<Customer>().Property("ShippingAddress.ZipCode.Value")
.HasConversion(postalCodeConverter);
}
// ... DbContext setup
}
Asynchronous Modeling Patterns
Asynchronous programming is critical for building responsive and scalable .NET applications, especially in I/O-bound scenarios like web requests or database operations. Key concepts include:
async
andawait
keywords: The core of C#'s asynchronous programming model.- Task-based Asynchronous Pattern (TAP): Using the
Task
andTask<TResult>
types to represent asynchronous operations. - Non-blocking I/O: Performing operations like database queries or network calls without blocking the calling thread. This is crucial for server scalability.
- Asynchronous Repositories and Services: Implementing data access and business logic layers with asynchronous methods.
Example: Asynchronous Repository Method
public interface IProductRepository
{
Task<Product> GetByIdAsync(int id);
Task AddAsync(Product product);
Task<IEnumerable<Product>> GetAllAsync();
}
public class ProductRepository : IProductRepository
{
private readonly AppDbContext _context;
public ProductRepository(AppDbContext context)
{
_context = context;
}
public async Task<Product> GetByIdAsync(int id)
{
return await _context.Products.FindAsync(id);
}
public async Task AddAsync(Product product)
{
_context.Products.Add(product);
await _context.SaveChangesAsync();
}
public async Task<IEnumerable<Product>> GetAllAsync()
{
return await _context.Products.ToListAsync();
}
}