C# DDD(无ORM框架)场景下,绕过领域实体保护成员加载对象的替代方案咨询
Great question—dealing with protected constructors and field mapping when using raw SQL in DDD is a common pain point, especially when you want to avoid repetitive builder classes. Let’s go through a few cleaner alternatives that fit with .NET Framework 4.7 and DDD principles:
1. Use Internal Constructors + InternalsVisibleTo Attribute
This is my go-to approach because it maintains strong encapsulation in your domain layer while giving your infrastructure layer (where the repository lives) controlled access to create fully initialized entities.
How to implement it:
First, add an internal constructor to your Customer class that accepts all necessary fields (including the Id, which is usually set by the database). Reuse your existing public constructor to keep validation logic centralized:
public class Customer : IAggregateRoot { // ... existing properties and public constructor ... // Internal constructor for repository use internal Customer(int id, string name, Address address) : this(name, address) { Id = id; } // ... protected default constructor ... }
Next, allow your infrastructure assembly to access internal members of your domain assembly by adding this line to your domain project's AssemblyInfo.cs:
[assembly: InternalsVisibleTo("YourInfrastructureAssemblyName")]
Replace YourInfrastructureAssemblyName with the actual name of your infrastructure project (without the .dll extension).
Now in your repository, you can directly create Customer instances using the internal constructor, no builder required:
public class CustomerRepository : ICustomerRepository { public Customer GetById(int id) { using (var conn = new SqlConnection("YourConnectionString")) { conn.Open(); var cmd = new SqlCommand(@" SELECT Id, Name, Street, Number FROM Customers WHERE Id = @Id", conn); cmd.Parameters.AddWithValue("@Id", id); using (var reader = cmd.ExecuteReader()) { if (!reader.Read()) return null; var address = new Address( reader.GetString(reader.GetOrdinal("Street")), reader.GetInt32(reader.GetOrdinal("Number")) ); // Use the internal constructor to create a fully valid Customer return new Customer( reader.GetInt32(reader.GetOrdinal("Id")), reader.GetString(reader.GetOrdinal("Name")), address ); } } } }
Pros:
- Type-safe: No reflection or dynamic code, so you get compile-time checks.
- Maintains encapsulation: Domain logic stays in the domain layer, and you’re not exposing public setters or breaking DDD principles.
- No repetitive code: No need to create a builder class per entity.
2. Reflection (With Caching)
If you can’t or don’t want to modify your domain classes (e.g., they’re in a shared library you can’t edit), reflection is a viable option. To mitigate performance concerns, cache the PropertyInfo objects so you don’t look them up every time.
Example implementation:
public class CustomerRepository : ICustomerRepository { // Cache PropertyInfo objects to avoid repeated reflection overhead private static readonly PropertyInfo _customerIdProp = typeof(Customer) .GetProperty(nameof(Customer.Id), BindingFlags.Public | BindingFlags.Instance); private static readonly PropertyInfo _customerNameProp = typeof(Customer) .GetProperty(nameof(Customer.Name), BindingFlags.Public | BindingFlags.Instance); private static readonly PropertyInfo _customerAddressProp = typeof(Customer) .GetProperty(nameof(Customer.Address), BindingFlags.Public | BindingFlags.Instance); public Customer GetById(int id) { using (var conn = new SqlConnection("YourConnectionString")) { conn.Open(); var cmd = new SqlCommand(@" SELECT Id, Name, Street, Number FROM Customers WHERE Id = @Id", conn); cmd.Parameters.AddWithValue("@Id", id); using (var reader = cmd.ExecuteReader()) { if (!reader.Read()) return null; var address = new Address( reader.GetString(reader.GetOrdinal("Street")), reader.GetInt32(reader.GetOrdinal("Number")) ); // Create Customer using the public constructor var customer = new Customer( reader.GetString(reader.GetOrdinal("Name")), address ); // Use reflection to set the protected Id property _customerIdProp.SetValue(customer, reader.GetInt32(reader.GetOrdinal("Id"))); return customer; } } } }
Pros:
- No changes to domain classes: Works with your existing entity code.
- Flexible: Can handle any entity without modifying its structure.
Cons:
- Performance overhead: Even with caching, reflection is slower than direct constructor calls.
- No compile-time checks: Typos in property names will only show up at runtime.
3. Use a Mapping Library (With Reflection Support)
Libraries like ValueInjecter or AutoMapper can handle mapping from your SQL data (e.g., a SqlDataReader or a DTO) to your domain entities, even with protected properties. You’ll need to configure the library to access non-public members, but it reduces repetitive mapping code.
For example, with AutoMapper, you can configure it to use reflection to set protected properties:
// In your infrastructure layer's AutoMapper profile public class CustomerProfile : Profile { public CustomerProfile() { CreateMap<IDataReader, Customer>() .ForMember(dest => dest.Id, opt => opt.MapFrom(src => src["Id"])) .ForMember(dest => dest.Name, opt => opt.MapFrom(src => src["Name"])) .ForMember(dest => dest.Address, opt => opt.MapFrom(src => new Address( (string)src["Street"], (int)src["Number"] ))); } }
Then in your repository:
public Customer GetById(int id) { // ... execute SQL and get reader ... return _mapper.Map<Customer>(reader); }
Pros:
- Reduces boilerplate: Centralizes mapping logic.
- Flexible: Works with multiple entity types.
Cons:
- Adds a dependency: You’ll need to include a third-party library.
- Less control: Mapping logic is abstracted away, which can make debugging harder.
Which Should You Choose?
- Internal constructor +
InternalsVisibleTo: Best for most DDD scenarios—it’s clean, type-safe, and maintains domain encapsulation. - Reflection: Use this only if you can’t modify your domain classes.
- Mapping libraries: Good if you have many entities to map and want to reduce boilerplate, but weigh the dependency cost.
内容的提问来源于stack exchange,提问作者tincho87

