diff --git a/csla-examples/BusinessRules.md b/csla-examples/BusinessRules.md index 0fcb5df..6328c7c 100644 --- a/csla-examples/BusinessRules.md +++ b/csla-examples/BusinessRules.md @@ -182,7 +182,9 @@ For detailed information on specific rule types and patterns, see: - [BusinessRulesValidation.md](BusinessRulesValidation.md) - Simple and complex validation rules - [BusinessRulesCalculation.md](BusinessRulesCalculation.md) - Property calculation rules -- [BusinessRulesPriority.md](BusinessRulesPriority.md) - Rule priorities and execution order +- [BusinessRulesPriority.md](BusinessRulesPriority.md) - Rule priorities, execution order, and short-circuiting +- [BusinessRulesContext.md](BusinessRulesContext.md) - Rule context, execution flags (IsCheckRulesContext), and context properties +- [BusinessRulesObjectLevel.md](BusinessRulesObjectLevel.md) - Object-level validation and authorization rules - [BusinessRulesAsync.md](BusinessRulesAsync.md) - Asynchronous rules - [BusinessRulesAuthorization.md](BusinessRulesAuthorization.md) - Authorization rules diff --git a/csla-examples/BusinessRulesContext.md b/csla-examples/BusinessRulesContext.md new file mode 100644 index 0000000..5d157f8 --- /dev/null +++ b/csla-examples/BusinessRulesContext.md @@ -0,0 +1,443 @@ +# Business Rules: Rule Context and Execution Flags + +The `IRuleContext` parameter passed to business rules provides important information about the execution environment and state. Understanding these context flags helps you create rules that behave appropriately in different scenarios. + +## IRuleContext Overview + +Every business rule receives an `IRuleContext` parameter in its `Execute` or `ExecuteAsync` method: + +```csharp +protected override void Execute(IRuleContext context) +{ + // Access context properties here +} +``` + +## Key Context Properties + +### IsCheckRulesContext + +Indicates whether the rule is executing because `CheckRules()` or `CheckRulesAsync()` was explicitly called. + +```csharp +public class MyRule : BusinessRule +{ + public MyRule(IPropertyInfo primaryProperty) + : base(primaryProperty) + { + InputProperties.Add(primaryProperty); + } + + protected override void Execute(IRuleContext context) + { + if (context.IsCheckRulesContext) + { + // Rule is running from explicit CheckRules call + Console.WriteLine($"Rule running from CheckRules"); + } + else + { + // Rule is running because a property changed + Console.WriteLine($"Rule running from {PrimaryProperty.Name} property change"); + } + + // Normal rule logic continues... + var value = (string)context.InputPropertyValues[PrimaryProperty]; + if (string.IsNullOrWhiteSpace(value)) + context.AddErrorResult("Value is required"); + } +} +``` + +**When IsCheckRulesContext is true:** +- `CheckRules()` or `CheckRulesAsync()` was explicitly called +- Usually happens during object initialization or save operations +- All rules run regardless of whether properties changed + +**When IsCheckRulesContext is false:** +- Rule is running because a property value changed +- Triggered by `SetProperty()` or cascading from another rule +- Only rules for affected properties run + +### Common Use Cases for IsCheckRulesContext + +**Skip expensive operations during property changes:** + +```csharp +public class ExpensiveValidation : BusinessRuleAsync +{ + public ExpensiveValidation(IPropertyInfo primaryProperty) + : base(primaryProperty) + { + InputProperties.Add(primaryProperty); + } + + protected override async Task ExecuteAsync(IRuleContext context) + { + if (!context.IsCheckRulesContext) + { + // Skip expensive database lookup during property changes + // Will still run during CheckRules (like before save) + return; + } + + // Expensive validation - only during CheckRules + var value = (string)context.InputPropertyValues[PrimaryProperty]; + var dal = context.ApplicationContext.GetRequiredService(); + bool isValid = await dal.ValidateAsync(value); + + if (!isValid) + context.AddErrorResult("Value is not valid"); + } +} +``` + +**Different behavior based on context:** + +```csharp +public class SmartValidation : BusinessRule +{ + public SmartValidation(IPropertyInfo primaryProperty) + : base(primaryProperty) + { + InputProperties.Add(primaryProperty); + } + + protected override void Execute(IRuleContext context) + { + var value = (int)context.InputPropertyValues[PrimaryProperty]; + + if (context.IsCheckRulesContext) + { + // Strict validation during CheckRules (before save) + if (value < 1 || value > 100) + context.AddErrorResult("Value must be between 1 and 100"); + } + else + { + // Lenient validation during property changes (better UX) + if (value < 1 || value > 100) + context.AddWarningResult("Value should be between 1 and 100"); + } + } +} +``` + +## InputPropertyValues + +Access to the current values of all properties declared in `InputProperties`: + +```csharp +public class CalculateTotal : BusinessRule +{ + private IPropertyInfo _quantityProperty; + private IPropertyInfo _priceProperty; + + public CalculateTotal( + IPropertyInfo totalProperty, + IPropertyInfo quantityProperty, + IPropertyInfo priceProperty) + : base(totalProperty) + { + _quantityProperty = quantityProperty; + _priceProperty = priceProperty; + + InputProperties.Add(quantityProperty); + InputProperties.Add(priceProperty); + AffectedProperties.Add(totalProperty); + } + + protected override void Execute(IRuleContext context) + { + // Access input values from context + int quantity = (int)context.InputPropertyValues[_quantityProperty]; + decimal price = (decimal)context.InputPropertyValues[_priceProperty]; + + decimal total = quantity * price; + context.AddOutValue(PrimaryProperty, total); + } +} +``` + +## Target + +Reference to the business object being validated: + +```csharp +public class ConditionalRule : BusinessRule +{ + public ConditionalRule(IPropertyInfo primaryProperty) + : base(primaryProperty) + { + } + + protected override void Execute(IRuleContext context) + { + // Cast to your business object type + var obj = (MyBusinessObject)context.Target; + + // Check object state + if (obj.IsNew) + { + // Different validation for new objects + } + else if (obj.IsDirty) + { + // Different validation for modified objects + } + + // Access object properties directly (bypasses authorization) + var status = ReadProperty(context.Target, StatusProperty); + } +} +``` + +**Warning:** Be careful when accessing `context.Target` in async rules - the target object may not be thread-safe. Use `InputPropertyValues` instead when possible. + +## ApplicationContext + +Access to application-level services and state: + +```csharp +public class UserAwareRule : BusinessRule +{ + public UserAwareRule(IPropertyInfo primaryProperty) + : base(primaryProperty) + { + InputProperties.Add(primaryProperty); + } + + protected override void Execute(IRuleContext context) + { + // Access current user + var user = context.ApplicationContext.User; + var principal = context.ApplicationContext.Principal; + + // Get services from DI + var logger = context.ApplicationContext.GetRequiredService>(); + var dal = context.ApplicationContext.GetRequiredService(); + + // Check user permissions + if (!user.IsInRole("Admin")) + { + context.AddErrorResult("Only admins can modify this field"); + } + + logger.LogInformation($"Rule executed by user: {user.Identity.Name}"); + } +} +``` + +## Exception Property + +Check if an exception occurred during rule execution (used in `Complete` phase): + +```csharp +public class RuleWithErrorHandling : BusinessRule +{ + public RuleWithErrorHandling(IPropertyInfo primaryProperty) + : base(primaryProperty) + { + } + + protected override void Execute(IRuleContext context) + { + try + { + // Rule logic that might throw + var value = (string)context.InputPropertyValues[PrimaryProperty]; + var result = SomeMethodThatMightThrow(value); + + if (!result) + context.AddErrorResult("Validation failed"); + } + catch (Exception ex) + { + // Log and add appropriate error message + var logger = context.ApplicationContext.GetRequiredService>(); + logger.LogError(ex, "Rule execution failed"); + + context.AddErrorResult("An error occurred during validation"); + } + } +} +``` + +## Adding Results to Context + +### AddErrorResult + +Adds an error and stops subsequent rule execution (short-circuits): + +```csharp +context.AddErrorResult("This value is required"); +context.AddErrorResult("Value must be between {0} and {1}", 1, 100); +``` + +### AddWarningResult + +Adds a warning but allows subsequent rules to run: + +```csharp +context.AddWarningResult("This value is unusual"); +``` + +### AddInformationResult + +Adds an informational message: + +```csharp +context.AddInformationResult("Value was automatically corrected"); +``` + +### AddSuccessResult + +Indicates success and optionally stops rule execution: + +```csharp +// Stop all subsequent rules for this property +context.AddSuccessResult(true); + +// Mark success but allow rules to continue +context.AddSuccessResult(false); +``` + +### AddOutValue + +Sets a property value (for calculation rules): + +```csharp +context.AddOutValue(TotalProperty, calculatedTotal); +``` + +## Practical Examples + +### Example 1: Debug Logging Based on Context + +```csharp +public class DiagnosticRule : BusinessRule +{ + public DiagnosticRule(IPropertyInfo primaryProperty) + : base(primaryProperty) + { + InputProperties.Add(primaryProperty); + } + + protected override void Execute(IRuleContext context) + { + var logger = context.ApplicationContext.GetRequiredService>(); + var value = context.InputPropertyValues[PrimaryProperty]; + + if (context.IsCheckRulesContext) + { + logger.LogInformation($"CheckRules validation: {PrimaryProperty.Name} = {value}"); + } + else + { + logger.LogDebug($"Property change: {PrimaryProperty.Name} = {value}"); + } + + // Normal validation logic + if (value == null) + context.AddErrorResult("Value cannot be null"); + } +} +``` + +### Example 2: State-Based Validation + +```csharp +public class StateBasedValidation : BusinessRule +{ + public StateBasedValidation(IPropertyInfo primaryProperty) + : base(primaryProperty) + { + InputProperties.Add(primaryProperty); + } + + protected override void Execute(IRuleContext context) + { + var target = (ITrackStatus)context.Target; + var value = (string)context.InputPropertyValues[PrimaryProperty]; + + // Different rules based on object state + if (target.IsNew) + { + // New objects must have a value + if (string.IsNullOrWhiteSpace(value)) + context.AddErrorResult("This field is required for new records"); + } + else if (target.IsDeleted) + { + // Don't validate deleted objects + context.AddSuccessResult(true); + } + else + { + // Existing objects can have optional values + if (string.IsNullOrWhiteSpace(value)) + context.AddInformationResult("Consider providing a value"); + } + } +} +``` + +### Example 3: User-Specific Validation + +```csharp +public class ManagerApprovalRequired : BusinessRule +{ + private IPropertyInfo _amountProperty; + + public ManagerApprovalRequired(IPropertyInfo statusProperty, IPropertyInfo amountProperty) + : base(statusProperty) + { + _amountProperty = amountProperty; + InputProperties.Add(statusProperty); + InputProperties.Add(amountProperty); + } + + protected override void Execute(IRuleContext context) + { + var status = (string)context.InputPropertyValues[PrimaryProperty]; + var amount = (decimal)context.InputPropertyValues[_amountProperty]; + var user = context.ApplicationContext.User; + + // Only validate when trying to approve + if (status != "Approved") + return; + + // Check if approval requires manager + if (amount > 10000 && !user.IsInRole("Manager")) + { + context.AddErrorResult("Manager approval required for amounts over $10,000"); + } + } +} +``` + +## Best Practices + +1. **Use IsCheckRulesContext wisely** - Skip expensive operations during property changes when appropriate +2. **Prefer InputPropertyValues over Target** - More reliable, especially in async scenarios +3. **Handle exceptions gracefully** - Catch and log exceptions, provide user-friendly error messages +4. **Access services via ApplicationContext** - Use DI to get dependencies +5. **Log diagnostic information** - Use different log levels based on IsCheckRulesContext +6. **Consider object state** - Use ITrackStatus properties (IsNew, IsDirty, IsDeleted) for conditional logic +7. **Check user context** - Access User or Principal when validation depends on identity +8. **Test both contexts** - Ensure rules work correctly when called from CheckRules and property changes + +## Notes + +- `IsCheckRulesContext` is the most commonly used context flag +- Context properties are read-only - you cannot modify them +- The `Target` property should be avoided in async rules for thread-safety +- `ApplicationContext` provides access to all registered services via DI +- Context flags help rules adapt to different execution scenarios + +## See Also + +- [BusinessRules.md](BusinessRules.md) - Overview of the business rules system +- [BusinessRulesPriority.md](BusinessRulesPriority.md) - Rule priorities and short-circuiting +- [BusinessRulesAsync.md](BusinessRulesAsync.md) - Asynchronous business rules +- [BusinessRulesValidation.md](BusinessRulesValidation.md) - Validation rules diff --git a/csla-examples/BusinessRulesObjectLevel.md b/csla-examples/BusinessRulesObjectLevel.md new file mode 100644 index 0000000..da1aa32 --- /dev/null +++ b/csla-examples/BusinessRulesObjectLevel.md @@ -0,0 +1,550 @@ +# Business Rules: Object-Level Rules + +Object-level rules validate or authorize the business object as a whole, rather than individual properties. These rules are essential for cross-property validation and object-level authorization. + +## Overview + +Most business rules are property-level rules that validate or calculate individual property values. However, many business scenarios require validating relationships between multiple properties or authorizing actions on entire objects. + +**Object-level rules:** +- Have no primary property (`PrimaryProperty` is `null`) +- Validate relationships between multiple properties +- Check overall object state +- Control object-level authorization (Create, Get, Edit, Delete) +- Run when `CheckRules()` or `CheckRulesAsync()` is called +- Can run when any dependent property changes + +## Object-Level Validation Rules + +### Basic Object-Level Validation + +Create a rule without specifying a primary property: + +```csharp +public class ValidDateRange : BusinessRule +{ + private IPropertyInfo _startDateProperty; + private IPropertyInfo _endDateProperty; + + public ValidDateRange(IPropertyInfo startDateProperty, IPropertyInfo endDateProperty) + : base(null) // No primary property - this is an object-level rule + { + _startDateProperty = startDateProperty; + _endDateProperty = endDateProperty; + + InputProperties.Add(startDateProperty); + InputProperties.Add(endDateProperty); + } + + protected override void Execute(IRuleContext context) + { + DateTime startDate = (DateTime)context.InputPropertyValues[_startDateProperty]; + DateTime endDate = (DateTime)context.InputPropertyValues[_endDateProperty]; + + if (startDate > endDate) + { + // Object-level error - not associated with a specific property + context.AddErrorResult("Start date must be before end date"); + } + } +} +``` + +**Usage:** + +```csharp +protected override void AddBusinessRules() +{ + base.AddBusinessRules(); + + // Property-level rules + BusinessRules.AddRule(new Required(StartDateProperty)); + BusinessRules.AddRule(new Required(EndDateProperty)); + + // Object-level rule + BusinessRules.AddRule(new ValidDateRange(StartDateProperty, EndDateProperty)); + + // Trigger object-level rule when either property changes + BusinessRules.AddRule(new Dependency(StartDateProperty, null)); // null = object level + BusinessRules.AddRule(new Dependency(EndDateProperty, null)); +} +``` + +**Key Points:** +- Pass `null` as the primary property to create an object-level rule +- Object-level errors appear in `BusinessRules.BrokenRules` but not associated with a specific property +- Use `Dependency(property, null)` to trigger object-level rules when properties change + +### Multi-Property Validation + +Validate complex relationships between multiple properties: + +```csharp +public class ValidOrderState : BusinessRule +{ + private IPropertyInfo _statusProperty; + private IPropertyInfo _approvedDateProperty; + private IPropertyInfo _approvedByProperty; + + public ValidOrderState( + IPropertyInfo statusProperty, + IPropertyInfo approvedDateProperty, + IPropertyInfo approvedByProperty) + : base(null) // Object-level rule + { + _statusProperty = statusProperty; + _approvedDateProperty = approvedDateProperty; + _approvedByProperty = approvedByProperty; + + InputProperties.Add(statusProperty); + InputProperties.Add(approvedDateProperty); + InputProperties.Add(approvedByProperty); + } + + protected override void Execute(IRuleContext context) + { + string status = (string)context.InputPropertyValues[_statusProperty]; + DateTime? approvedDate = (DateTime?)context.InputPropertyValues[_approvedDateProperty]; + string approvedBy = (string)context.InputPropertyValues[_approvedByProperty]; + + if (status == "Approved") + { + // If approved, must have approval date and approver + if (!approvedDate.HasValue) + context.AddErrorResult("Approved orders must have an approval date"); + + if (string.IsNullOrWhiteSpace(approvedBy)) + context.AddErrorResult("Approved orders must specify who approved them"); + } + else + { + // If not approved, should NOT have approval data + if (approvedDate.HasValue || !string.IsNullOrWhiteSpace(approvedBy)) + context.AddWarningResult("Approval information will be cleared when status is not 'Approved'"); + } + } +} +``` + +### Business Logic Validation + +Validate business logic that spans multiple properties: + +```csharp +public class ValidDiscount : BusinessRule +{ + private IPropertyInfo _subtotalProperty; + private IPropertyInfo _discountPercentProperty; + private IPropertyInfo _discountAmountProperty; + private IPropertyInfo _totalProperty; + + public ValidDiscount( + IPropertyInfo subtotalProperty, + IPropertyInfo discountPercentProperty, + IPropertyInfo discountAmountProperty, + IPropertyInfo totalProperty) + : base(null) // Object-level + { + _subtotalProperty = subtotalProperty; + _discountPercentProperty = discountPercentProperty; + _discountAmountProperty = discountAmountProperty; + _totalProperty = totalProperty; + + InputProperties.Add(subtotalProperty); + InputProperties.Add(discountPercentProperty); + InputProperties.Add(discountAmountProperty); + InputProperties.Add(totalProperty); + } + + protected override void Execute(IRuleContext context) + { + decimal subtotal = (decimal)context.InputPropertyValues[_subtotalProperty]; + decimal discountPercent = (decimal)context.InputPropertyValues[_discountPercentProperty]; + decimal discountAmount = (decimal)context.InputPropertyValues[_discountAmountProperty]; + decimal total = (decimal)context.InputPropertyValues[_totalProperty]; + + // Verify discount calculations are consistent + decimal expectedDiscount = subtotal * (discountPercent / 100); + decimal expectedTotal = subtotal - discountAmount; + + if (Math.Abs(discountAmount - expectedDiscount) > 0.01m) + { + context.AddErrorResult($"Discount amount ${discountAmount} doesn't match discount percent {discountPercent}%"); + } + + if (Math.Abs(total - expectedTotal) > 0.01m) + { + context.AddErrorResult($"Total ${total} is incorrect. Expected ${expectedTotal}"); + } + } +} +``` + +## Object-Level Authorization Rules + +Object-level authorization controls who can perform operations on entire business objects. + +### Authorization for Data Portal Operations + +Control who can create, fetch, update, or delete objects: + +```csharp +public class RequiresManagerRole : AuthorizationRule +{ + public RequiresManagerRole(AuthorizationActions action) + : base(action) // No element - applies to entire object + { + } + + protected override void Execute(IAuthorizationContext context) + { + var user = context.ApplicationContext.User; + + if (!user.IsInRole("Manager") && !user.IsInRole("Admin")) + { + context.HasPermission = false; + } + else + { + context.HasPermission = true; + } + } +} +``` + +**Usage:** + +```csharp +protected override void AddBusinessRules() +{ + base.AddBusinessRules(); + + // Object-level authorization + BusinessRules.AddRule(new RequiresManagerRole(AuthorizationActions.CreateObject)); + BusinessRules.AddRule(new RequiresManagerRole(AuthorizationActions.DeleteObject)); + + // Anyone can fetch (no rule = allowed) + // Property-level rules control who can edit specific fields +} +``` + +### Static Object Authorization + +Use the `[ObjectAuthorizationRules]` attribute for type-level authorization: + +```csharp +[ObjectAuthorizationRules] +public static void AddObjectAuthorizationRules(IAddObjectAuthorizationRulesContext context) +{ + // Only Admin users can create Order objects + context.Rules.AddRule(typeof(Order), + new IsInRole(AuthorizationActions.CreateObject, "Admin")); + + // Only Admin and Manager users can delete Order objects + context.Rules.AddRule(typeof(Order), + new IsInRole(AuthorizationActions.DeleteObject, "Admin", "Manager")); + + // Anyone can fetch orders (no rule = allowed) + + // Edit authorization is handled per-instance in AddBusinessRules +} +``` + +### State-Based Object Authorization + +Authorization based on object state and user identity: + +```csharp +public class CanEditOrder : AuthorizationRule +{ + private IPropertyInfo _ownerIdProperty; + private IPropertyInfo _statusProperty; + + public CanEditOrder(IPropertyInfo ownerIdProperty, IPropertyInfo statusProperty) + : base(AuthorizationActions.EditObject) + { + _ownerIdProperty = ownerIdProperty; + _statusProperty = statusProperty; + + CacheResult = false; // Re-evaluate each time + } + + protected override void Execute(IAuthorizationContext context) + { + // Get object state + int ownerId = (int)ReadProperty(context.Target, _ownerIdProperty); + string status = (string)ReadProperty(context.Target, _statusProperty); + + // Get current user + var principal = context.ApplicationContext.Principal; + var userIdClaim = principal.Claims.FirstOrDefault(c => c.Type == "UserId"); + + if (userIdClaim == null) + { + context.HasPermission = false; + return; + } + + int currentUserId = int.Parse(userIdClaim.Value); + + // Business rules: + // 1. Admins can edit any order + // 2. Owners can edit their own orders if status is "Draft" or "Pending" + // 3. No one can edit "Completed" or "Cancelled" orders + + if (status == "Completed" || status == "Cancelled") + { + // Only admins can edit completed/cancelled orders + context.HasPermission = principal.IsInRole("Admin"); + } + else if (ownerId == currentUserId) + { + // Owner can edit their own draft/pending orders + context.HasPermission = true; + } + else + { + // Others need admin role + context.HasPermission = principal.IsInRole("Admin"); + } + } +} +``` + +**Usage:** + +```csharp +protected override void AddBusinessRules() +{ + base.AddBusinessRules(); + + // Object-level authorization based on state + BusinessRules.AddRule(new CanEditOrder(OwnerIdProperty, StatusProperty)); +} +``` + +## Triggering Object-Level Rules + +### Manual Trigger + +Object-level rules run when `CheckRules()` is called: + +```csharp +[Create] +private void Create() +{ + LoadProperty(StatusProperty, "Draft"); + LoadProperty(CreatedDateProperty, DateTime.UtcNow); + + // Runs all rules, including object-level + await BusinessRules.CheckRulesAsync(); +} +``` + +### Automatic Trigger with Dependencies + +Set up dependencies to trigger object-level rules when properties change: + +```csharp +protected override void AddBusinessRules() +{ + base.AddBusinessRules(); + + // Object-level rule + BusinessRules.AddRule(new ValidDateRange(StartDateProperty, EndDateProperty)); + + // Trigger object-level rules when these properties change + BusinessRules.AddRule(new Dependency(StartDateProperty, null)); // null = object level + BusinessRules.AddRule(new Dependency(EndDateProperty, null)); +} +``` + +When `StartDate` or `EndDate` changes, `ValidDateRange` will automatically run. + +## Displaying Object-Level Errors + +Object-level broken rules aren't associated with a specific property: + +```csharp +// Check for object-level errors +if (!myObject.IsValid) +{ + var objectErrors = myObject.BrokenRulesCollection + .Where(r => r.Property == null || string.IsNullOrEmpty(r.Property)) + .Select(r => r.Description); + + foreach (var error in objectErrors) + { + Console.WriteLine($"Object Error: {error}"); + } +} +``` + +**In UI (example for Blazor):** + +```razor +@if (!Model.IsValid) +{ +
+
Validation Errors:
+
    + @foreach (var rule in Model.BrokenRulesCollection.Where(r => string.IsNullOrEmpty(r.Property))) + { +
  • @rule.Description
  • + } +
+
+} +``` + +## Common Patterns + +### Pattern 1: Required Combination + +At least one of multiple fields must have a value: + +```csharp +public class RequireOneOf : BusinessRule +{ + private IPropertyInfo[] _properties; + + public RequireOneOf(params IPropertyInfo[] properties) + : base(null) // Object-level + { + _properties = properties; + foreach (var prop in properties) + { + InputProperties.Add(prop); + } + } + + protected override void Execute(IRuleContext context) + { + bool hasValue = _properties.Any(prop => + { + var value = context.InputPropertyValues[prop]; + return value != null && !string.IsNullOrWhiteSpace(value.ToString()); + }); + + if (!hasValue) + { + var names = string.Join(", ", _properties.Select(p => p.FriendlyName)); + context.AddErrorResult($"At least one of the following is required: {names}"); + } + } +} +``` + +### Pattern 2: Mutually Exclusive Fields + +Only one of multiple fields can have a value: + +```csharp +public class MutuallyExclusive : BusinessRule +{ + private IPropertyInfo[] _properties; + + public MutuallyExclusive(params IPropertyInfo[] properties) + : base(null) + { + _properties = properties; + foreach (var prop in properties) + { + InputProperties.Add(prop); + } + } + + protected override void Execute(IRuleContext context) + { + int countWithValues = _properties.Count(prop => + { + var value = context.InputPropertyValues[prop]; + return value != null && !string.IsNullOrWhiteSpace(value.ToString()); + }); + + if (countWithValues > 1) + { + var names = string.Join(", ", _properties.Select(p => p.FriendlyName)); + context.AddErrorResult($"Only one of the following can have a value: {names}"); + } + } +} +``` + +### Pattern 3: Workflow State Validation + +Validate object state transitions: + +```csharp +public class ValidStatusTransition : BusinessRule +{ + private IPropertyInfo _statusProperty; + + public ValidStatusTransition(IPropertyInfo statusProperty) + : base(null) + { + _statusProperty = statusProperty; + InputProperties.Add(statusProperty); + } + + protected override void Execute(IRuleContext context) + { + var target = (Order)context.Target; + string newStatus = (string)context.InputPropertyValues[_statusProperty]; + string oldStatus = ReadProperty(target, _statusProperty) as string; + + if (oldStatus == newStatus) + return; + + // Define valid transitions + var validTransitions = new Dictionary + { + { "Draft", new[] { "Pending", "Cancelled" } }, + { "Pending", new[] { "Approved", "Rejected", "Cancelled" } }, + { "Approved", new[] { "Completed", "Cancelled" } }, + { "Rejected", new[] { "Draft" } }, + { "Completed", Array.Empty() }, // Final state + { "Cancelled", Array.Empty() } // Final state + }; + + if (!validTransitions.ContainsKey(oldStatus)) + { + context.AddErrorResult($"Invalid current status: {oldStatus}"); + return; + } + + if (!validTransitions[oldStatus].Contains(newStatus)) + { + context.AddErrorResult($"Cannot transition from {oldStatus} to {newStatus}"); + } + } +} +``` + +## Best Practices + +1. **Use object-level rules for cross-property validation** - Don't try to validate multiple properties from a single-property rule +2. **Set up dependencies** - Use `Dependency(property, null)` to trigger object-level rules automatically +3. **Display object-level errors prominently** - They're not tied to a specific field in the UI +4. **Use for complex authorization** - Object-level authorization can check multiple properties and object state +5. **Keep rules focused** - Even object-level rules should have a single responsibility +6. **Consider performance** - Object-level rules with many input properties can be expensive +7. **Test edge cases** - Verify rules work when properties are null or have default values +8. **Document business logic** - Object-level rules often encode complex business rules + +## Notes + +- Object-level rules have `PrimaryProperty == null` +- Object-level errors appear in `BrokenRulesCollection` with an empty or null `Property` value +- Use `Dependency(property, null)` to trigger object-level rules when specific properties change +- Object-level authorization rules control Create, Get, Edit, and Delete operations +- Static authorization (via `[ObjectAuthorizationRules]`) is checked before object instantiation +- Instance authorization (via `AddBusinessRules`) is checked after object is loaded + +## See Also + +- [BusinessRules.md](BusinessRules.md) - Overview of the business rules system +- [BusinessRulesValidation.md](BusinessRulesValidation.md) - Property-level validation rules +- [BusinessRulesAuthorization.md](BusinessRulesAuthorization.md) - Authorization rules +- [BusinessRulesContext.md](BusinessRulesContext.md) - Rule context and execution flags +- [BusinessRulesPriority.md](BusinessRulesPriority.md) - Rule priorities and execution order diff --git a/csla-examples/BusinessRulesPriority.md b/csla-examples/BusinessRulesPriority.md index c5dd37f..cc1fdbe 100644 --- a/csla-examples/BusinessRulesPriority.md +++ b/csla-examples/BusinessRulesPriority.md @@ -113,6 +113,97 @@ This ensures: 1. Don't check email format if email is missing 2. Don't check uniqueness in database if email format is invalid +## Manual Short-Circuiting with AddSuccessResult + +You can manually stop rule execution from within a rule using `context.AddSuccessResult(true)`: + +```csharp +public class StopIfIsNotNew : BusinessRule +{ + public StopIfIsNotNew(IPropertyInfo primaryProperty) + : base(primaryProperty) + { + } + + protected override void Execute(IRuleContext context) + { + var target = (ITrackStatus)context.Target; + + if (!target.IsNew) + { + // Stop processing rules for this property + context.AddSuccessResult(true); + } + // If IsNew = true, do nothing (rules continue) + } +} +``` + +**Usage:** + +```csharp +protected override void AddBusinessRules() +{ + base.AddBusinessRules(); + + // Stop validation if object is not new (priority -1 to run before other rules) + BusinessRules.AddRule(new StopIfIsNotNew(NameProperty) { Priority = -1 }); + + // These rules only run if object IsNew = true + BusinessRules.AddRule(new Required(NameProperty)); + BusinessRules.AddRule(new MaxLength(NameProperty, 50)); +} +``` + +### Common Short-Circuiting Patterns + +**Stop if user cannot write property:** + +```csharp +public class StopIfNotCanWrite : BusinessRule +{ + public StopIfNotCanWrite(IPropertyInfo primaryProperty) + : base(primaryProperty) + { + } + + protected override void Execute(IRuleContext context) + { + if (!CanWriteProperty(PrimaryProperty)) + { + context.AddSuccessResult(true); + } + } +} +``` + +**Usage:** + +```csharp +protected override void AddBusinessRules() +{ + base.AddBusinessRules(); + + // Authorization rule + BusinessRules.AddRule(new IsInRole(AuthorizationActions.WriteProperty, SalaryProperty, "Admin")); + + // Only validate Salary if user has write permission (priority -1) + BusinessRules.AddRule(new StopIfNotCanWrite(SalaryProperty) { Priority = -1 }); + + // Validation rules only run if user can write + BusinessRules.AddRule(new MinValue(SalaryProperty, 0)); + BusinessRules.AddRule(new MaxValue(SalaryProperty, 1000000)); +} +``` + +**Why use `AddSuccessResult(true)`:** +- Prevents unnecessary validation when conditions aren't met +- More efficient than letting validation rules fail +- Cleaner than using Error results to stop execution +- Useful for conditional validation based on object state + +**Note:** `AddSuccessResult(true)` stops ALL subsequent rules for that property, not just the current priority level. This is different from error-based short-circuiting which stops rules at the same or higher priority. + ## Priority Example: Calculation Before Validation A calculation rule must run before a validation rule that checks the calculated value: