# Wrapper Attribute - Reference-Based Property Delegation ## Overview The `[Wrapper]` attribute generates wrapper classes that **delegate** to a source object instance, creating a reference-based facade pattern. Unlike `[Facet]` which creates independent value copies, wrappers maintain a reference to the source object, so changes to wrapper properties affect the underlying source. ## When to Use Wrapper vs Facet | Use Case | Use Wrapper | Use Facet | |----------|-------------|-----------| | DTOs for API/serialization | ❌ | ✅ | | EF Core query projections | ❌ | ✅ | | Facade pattern (hide properties) | ✅ | ❌ | | ViewModel with live binding | ✅ | ❌ | | Decorator pattern | ✅ | ❌ | | Read-only views | ✅ | ❌ | | Memory efficiency (avoid duplication) | ✅ | ❌ | | Disconnected data transfer | ❌ | ✅ | ## Basic Usage ```csharp public class User { public int Id { get; set; } public string FirstName { get; set; } public string LastName { get; set; } public string Email { get; set; } public string Password { get; set; } public decimal Salary { get; set; } } // Wrapper that hides sensitive properties [Wrapper(typeof(User), "Password", "Salary")] public partial class PublicUserWrapper { } // Usage var user = new User { Id = 1, FirstName = "John", Password = "secret123", Salary = 75000 }; var wrapper = new PublicUserWrapper(user); // Read from wrapper (delegates to source) Console.WriteLine(wrapper.FirstName); // "John" // Modify through wrapper (affects source!) wrapper.FirstName = "Jane"; Console.WriteLine(user.FirstName); // "Jane" // Sensitive properties not accessible // wrapper.Password; // ❌ Compile error // wrapper.Salary; // ❌ Compile error ``` ## Attribute Parameters ### Constructor Parameters ```csharp [Wrapper(Type sourceType, params string[] exclude)] ``` - **sourceType**: The type to wrap and delegate to - **exclude**: Property/field names to exclude from the wrapper ### Named Parameters | Parameter | Type | Default | Description | |-----------|------|---------|-------------| | `Include` | `string[]?` | `null` | Include only these properties (mutually exclusive with Exclude) | | `IncludeFields` | `bool` | `false` | Include public fields from source type | | `ReadOnly` | `bool` | `false` | Generate get-only properties (immutable facade) | | `NestedWrappers` | `Type[]?` | `null` | Wrapper types for nested complex properties | | `CopyAttributes` | `bool` | `false` | Copy validation attributes from source to wrapper | | `UseFullName` | `bool` | `false` | Use full type name for generated file | ## Include/Exclude Patterns ### Exclude Pattern (Default) ```csharp // Exclude specific properties [Wrapper(typeof(User), "Password", "Salary", "SocialSecurity")] public partial class PublicUserWrapper { } ``` ### Include Pattern ```csharp // Only include specific properties [Wrapper(typeof(User), Include = [nameof(User.Id), nameof(User.FirstName), nameof(User.LastName), nameof(User.Email)])] public partial class UserContactWrapper { } ``` ## Read-Only Wrappers Generate immutable facades that prevent accidental modifications: ```csharp [Wrapper(typeof(Product), ReadOnly = true)] public partial class ReadOnlyProductView { } var product = new Product { Name = "Laptop", Price = 1299.99m }; var view = new ReadOnlyProductView(product); // Can read Console.WriteLine(view.Name); // "Laptop" Console.WriteLine(view.Price); // 1299.99 // Cannot write (compile error CS0200) // view.Name = "Desktop"; // Property is read-only // view.Price = 999.99m; // Property is read-only // Still reflects source changes product.Name = "Desktop"; Console.WriteLine(view.Name); // "Desktop" ``` ### Use Cases for ReadOnly Wrappers - **Security**: Prevent modifications to sensitive domain objects - **API Design**: Provide read-only views to consumers - **Defensive Programming**: Ensure certain contexts can't mutate state - **Event Handlers**: Pass immutable views to prevent side effects ## Copying Attributes Copy validation and other attributes from source to wrapper: ```csharp public class Product { [Required] [StringLength(100)] public string Name { get; set; } [Range(0, 10000)] public decimal Price { get; set; } } [Wrapper(typeof(Product), CopyAttributes = true)] public partial class ProductWrapper { } ``` Generated code: ```csharp public partial class ProductWrapper { [Required] [StringLength(100)] public string Name { get => _source.Name; set => _source.Name = value; } [Range(0, 10000)] public decimal Price { get => _source.Price; set => _source.Price = value; } } ``` ## Including Fields By default, only properties are wrapped. Enable field wrapping: ```csharp public class Entity { public int Id; // Field public string Name { get; set; } // Property } [Wrapper(typeof(Entity), IncludeFields = true)] public partial class EntityWrapper { } ``` ## Nested Wrappers Wrap complex nested objects with their own wrapper types. This enables deep property hiding and creates layered facade patterns. ### Basic Nested Wrapper Usage ```csharp public class Address { public string Street { get; set; } public string City { get; set; } public string ZipCode { get; set; } public string Country { get; set; } } public class Person { public int Id { get; set; } public string Name { get; set; } public Address Address { get; set; } public string SocialSecurityNumber { get; set; } } // Define wrapper for nested type [Wrapper(typeof(Address), "Country")] public partial class PublicAddressWrapper { } // Reference nested wrapper in parent wrapper [Wrapper(typeof(Person), "SocialSecurityNumber", NestedWrappers = new[] { typeof(PublicAddressWrapper) })] public partial class PublicPersonWrapper { } // Usage var person = new Person { Id = 1, Name = "John Doe", Address = new Address { Street = "123 Main St", City = "Springfield", ZipCode = "12345", Country = "USA" }, SocialSecurityNumber = "123-45-6789" }; var wrapper = new PublicPersonWrapper(person); // Access nested wrapper Console.WriteLine(wrapper.Address.City); // "Springfield" Console.WriteLine(wrapper.Address.ZipCode); // "12345" // Country is excluded by PublicAddressWrapper // wrapper.Address.Country // ❌ Compile error // Changes propagate through nested wrappers wrapper.Address.City = "Boston"; Console.WriteLine(person.Address.City); // "Boston" ``` ### How Nested Wrappers Work When a property's type matches a nested wrapper's source type, the generator: 1. **Wraps on get**: Returns a new wrapper instance wrapping the nested object 2. **Unwraps on set**: Calls `Unwrap()` to extract the source object before assignment Generated code for nested properties: ```csharp // Simple property (no nested wrapper) public int Id { get => _source.Id; set => _source.Id = value; } // Nested wrapper property public PublicAddressWrapper Address { get => new PublicAddressWrapper(_source.Address); set => _source.Address = value.Unwrap(); } // Nullable nested wrapper public PublicAddressWrapper? OptionalAddress { get => _source.OptionalAddress != null ? new PublicAddressWrapper(_source.OptionalAddress) : null; set => _source.OptionalAddress = value?.Unwrap(); } ``` ### Nested Wrapper Best Practices **Do:** - Use nested wrappers for multi-level property hiding - Keep nested wrapper hierarchies shallow (2-3 levels max) - Define nested wrappers before parent wrappers - Use nullable wrappers for optional nested objects **Don't:** - Don't create circular wrapper references - Don't mix Facet and Wrapper for the same source type in nested scenarios - Don't wrap collections with nested wrappers (not yet supported) ### Multi-Level Nesting You can nest wrappers multiple levels deep: ```csharp public class Department { public string Name { get; set; } public string Budget { get; set; } // Internal } public class Employee { public string Name { get; set; } public decimal Salary { get; set; } // Sensitive public Department Department { get; set; } } public class Company { public string Name { get; set; } public Employee CEO { get; set; } } [Wrapper(typeof(Department), "Budget")] public partial class PublicDepartmentWrapper { } [Wrapper(typeof(Employee), "Salary", NestedWrappers = new[] { typeof(PublicDepartmentWrapper) })] public partial class PublicEmployeeWrapper { } [Wrapper(typeof(Company), NestedWrappers = new[] { typeof(PublicEmployeeWrapper) })] public partial class PublicCompanyWrapper { } // Usage var company = new Company { /* ... */ }; var wrapper = new PublicCompanyWrapper(company); // Three-level nesting works Console.WriteLine(wrapper.CEO.Department.Name); // wrapper.CEO.Salary // ❌ Excluded // wrapper.CEO.Department.Budget // ❌ Excluded ``` ## Generated Code Structure ### Mutable Wrapper (Default) ```csharp [Wrapper(typeof(User), "Password")] public partial class UserWrapper { } ``` Generates: ```csharp public partial class UserWrapper { private readonly User _source; /// /// Initializes a new instance of the UserWrapper wrapper. /// /// The source object to wrap. /// Thrown when source is null. public UserWrapper(User source) { _source = source ?? throw new System.ArgumentNullException(nameof(source)); } public int Id { get => _source.Id; set => _source.Id = value; } public string FirstName { get => _source.FirstName; set => _source.FirstName = value; } public string LastName { get => _source.LastName; set => _source.LastName = value; } /// /// Returns the wrapped source object. /// public User Unwrap() => _source; } ``` ### Read-Only Wrapper ```csharp [Wrapper(typeof(User), "Password", ReadOnly = true)] public partial class ReadOnlyUserWrapper { } ``` Generates: ```csharp public partial class ReadOnlyUserWrapper { private readonly User _source; public ReadOnlyUserWrapper(User source) { _source = source ?? throw new System.ArgumentNullException(nameof(source)); } public int Id { get => _source.Id; // No setter - read-only! } public string FirstName { get => _source.FirstName; // No setter - read-only! } public User Unwrap() => _source; } ``` ## Unwrap Method Every wrapper includes an `Unwrap()` method to access the underlying source: ```csharp var user = new User { Id = 1, FirstName = "John" }; var wrapper = new PublicUserWrapper(user); // Access the original source object User original = wrapper.Unwrap(); Console.WriteLine(ReferenceEquals(user, original)); // True ``` ## Null Safety All wrappers include null checks in the constructor: ```csharp var wrapper = new UserWrapper(null); // Throws ArgumentNullException with parameter name "source" ``` ## Common Patterns ### API Facade Pattern Hide internal/sensitive properties from API consumers: ```csharp public class Order { public int Id { get; set; } public string OrderNumber { get; set; } public decimal Total { get; set; } public decimal InternalCost { get; set; } // Internal only public decimal ProfitMargin { get; set; } // Internal only } [Wrapper(typeof(Order), "InternalCost", "ProfitMargin")] public partial class PublicOrderView { } // API endpoint public PublicOrderView GetOrder(int id) { Order order = _repository.GetOrder(id); return new PublicOrderView(order); } ``` ### ViewModel Pattern Expose domain model subset to UI layer: ```csharp public class Customer { public int Id { get; set; } public string Name { get; set; } public string Email { get; set; } public byte[] PasswordHash { get; set; } public string SecurityToken { get; set; } } [Wrapper(typeof(Customer), "PasswordHash", "SecurityToken")] public partial class CustomerViewModel { } // View model usage var customer = _customerService.GetCustomer(id); var viewModel = new CustomerViewModel(customer); DataContext = viewModel; // UI binds to wrapper, changes propagate ``` ### Testing Instrumentation Wrap domain objects with test tracking: ```csharp [Wrapper(typeof(User))] public partial class InstrumentedUserWrapper { } // Test helper public partial class InstrumentedUserWrapper { public List AccessedProperties { get; } = new(); // Override generated properties to add tracking public new string FirstName { get { AccessedProperties.Add(nameof(FirstName)); return _source.FirstName; } } } ``` ### Layered Security with Nested Wrappers Create different security views for different roles: ```csharp public class BillingInfo { public string CardNumber { get; set; } public string CVV { get; set; } public string ExpiryDate { get; set; } } public class User { public string Name { get; set; } public string Email { get; set; } public BillingInfo BillingInfo { get; set; } public string InternalNotes { get; set; } } // Customer view: Hide CVV and internal notes [Wrapper(typeof(BillingInfo), "CVV")] public partial class CustomerBillingWrapper { } [Wrapper(typeof(User), "InternalNotes", NestedWrappers = new[] { typeof(CustomerBillingWrapper) })] public partial class CustomerUserWrapper { } // Admin view: Show all except CVV (PCI compliance) [Wrapper(typeof(BillingInfo), "CVV")] public partial class AdminBillingWrapper { } [Wrapper(typeof(User), NestedWrappers = new[] { typeof(AdminBillingWrapper) })] public partial class AdminUserWrapper { } // Customer endpoint public CustomerUserWrapper GetProfile() { var user = _userService.GetCurrentUser(); return new CustomerUserWrapper(user); // CVV, InternalNotes hidden } // Admin endpoint public AdminUserWrapper GetUserDetails(int userId) { var user = _userService.GetUser(userId); return new AdminUserWrapper(user); // Only CVV hidden } ``` ## Best Practices ### Do - Use wrappers for **runtime facades** and **ViewModels** - Use wrappers when you need **synchronized changes** between wrapper and source - Use `ReadOnly = true` for **immutable views** and **security** - Use wrappers to **hide sensitive properties** from external consumers - Use **nested wrappers** for multi-level property hiding and layered security - Keep nested wrapper hierarchies **shallow** (2-3 levels maximum) - Combine with Facet when you need both patterns for different purposes ### Don't - Don't use wrappers for **DTOs** or **data transfer** (use Facet instead) - Don't use wrappers for **EF Core query projections** (use Facet instead) - Don't use wrappers for **serialization** (use Facet instead) - Don't wrap **Facets** - both should target the same source type - Don't create **circular references** in nested wrapper hierarchies ## Performance Considerations - **Memory**: Wrappers add minimal overhead (one reference field) - **CPU**: Property access is a simple field dereference (very fast) - **GC**: Wrapper keeps source alive as long as wrapper exists - **No reflection**: All property access is direct, compile-time bound - **Nested Wrappers**: Each access creates a new wrapper instance (short-lived, GC-friendly) - Cache nested wrapper references if accessed frequently in loops - Nested wrapper creation is fast (single allocation + field assignment) ## Comparison: Wrapper vs Facet ```csharp public class User { public string Name { get; set; } } // Facet: Creates independent copy [Facet(typeof(User))] public partial class UserDto { } var user = new User { Name = "John" }; var dto = user.ToFacet(); dto.Name = "Jane"; Console.WriteLine(user.Name); // "John" - independent // Wrapper: Delegates to source [Wrapper(typeof(User))] public partial class UserWrapper { } var wrapper = new UserWrapper(user); wrapper.Name = "Jane"; Console.WriteLine(user.Name); // "Jane" - synchronized! ``` | Aspect | Facet | Wrapper | |--------|-------|---------| | Data Storage | Independent copy | Reference to source | | Memory | Duplicates data | No duplication | | Changes | Independent | Synchronized to source | | Use Case | DTOs, EF projections | Facades, ViewModels | | EF Core | Query projections | Not applicable | | Serialization | Safe | Serializes wrapper, not source | ## Limitations The following features are planned for future releases: - **Collection Wrappers**: Wrapping collections of nested wrapper types - **Custom Mapping**: Add computed properties via configuration - **Init-only Properties**: Support for C# 9+ init accessors - **Full Records Support**: Enhanced record type support ## See Also - [Facet Attribute](03_AttributeReference.md) - For value-copying behavior - [Advanced Scenarios](06_AdvancedScenarios.md) - Complex patterns - [Quick Start](02_QuickStart.md) - Getting started with Facet