Typed Command Handlers (MIQS Pattern)
Updated
Overview#
Typed Command Handlers provide strongly-typed entity access, similar to MIQS DictionaryEventHandlerBase<T> pattern. This approach offers:
- IntelliSense support — IDE autocomplete for entity properties
- Compile-time type checking — catch errors before runtime
- No reflection overhead — direct property access
- Cleaner code — no
GetEntityProperty<T>()calls - Better refactoring — rename properties safely
When to Use Typed vs Dynamic Commands#
Use Typed Commands when:#
- You have predefined entity classes (DTOs, models)
- Entity structure is stable and well-known
- You want IntelliSense and compile-time safety
- You're working with generated entities from metadata
Use Dynamic Commands when:#
- Entity structure is unknown at compile time
- You're working with generic/reusable commands
- Entity schema can vary (dynamic dictionaries)
- You need maximum flexibility
Typed Base Classes#
1. TypedCommandHandlerBase<TEntity>#
Base class for any typed command.
public abstract class TypedCommandHandlerBase<TEntity> : CommandHandlerBase
where TEntity : class, new()
{
// Strongly-typed ExecuteAsync
public abstract Task<CommandResult> ExecuteTypedAsync(
CommandContext context,
TEntity entity);
// Optional: Strongly-typed ValidateAsync
public virtual Task<CommandResult?> ValidateTypedAsync(
CommandContext context,
TEntity entity);
// Optional: Strongly-typed OnBeforeExecuteAsync
public virtual Task OnBeforeExecuteTypedAsync(
CommandContext context,
TEntity entity);
// Optional: Strongly-typed OnAfterExecuteAsync
public virtual Task OnAfterExecuteTypedAsync(
CommandContext context,
TEntity entity,
CommandResult result);
}
2. TypedDictionaryCommandBase<TEntity>#
For dictionary commands with change tracking.
public abstract class TypedDictionaryCommandBase<TEntity> : TypedCommandHandlerBase<TEntity>
where TEntity : class, new()
{
// Automatic snapshot
protected TEntity? OriginalEntity { get; private set; }
// Helper methods
protected string? GetDictionaryName(CommandContext context);
protected Guid? GetDictionaryMetaId(CommandContext context);
}
3. TypedDocumentCommandBase<TEntity>#
For document commands with change tracking.
public abstract class TypedDocumentCommandBase<TEntity> : TypedCommandHandlerBase<TEntity>
where TEntity : class, new()
{
// Automatic snapshot
protected TEntity? OriginalDocument { get; private set; }
// Helper methods
protected string? GetDocumentTypeName(CommandContext context);
}
4. TypedListCommandBase<TEntity>#
For list commands with strongly-typed entities.
public abstract class TypedListCommandBase<TEntity> : CommandHandlerBase
where TEntity : class, new()
{
// Strongly-typed list processing
public abstract Task<CommandResult> ExecuteTypedAsync(
CommandContext context,
List<TEntity> entities);
// Progress tracking with typed entities
protected Task<CommandResult> ProcessSelectedRecordsAsync(
CommandContext context,
List<TEntity> entities,
Func<TEntity, int, int, Task<bool>> processFunc);
}
Examples#
Example 1: Typed Dictionary Command#
Define Entity
public class Customer
{
public Guid Id { get; set; }
public Guid MetaId { get; set; }
public string Code { get; set; } = string.Empty;
public string Name { get; set; } = string.Empty;
public string? Email { get; set; }
public string? Phone { get; set; }
public string? Status { get; set; }
}
Dynamic Command (Old Way)
public class SendEmailCommand : DictionaryCommandBase
{
public override async Task<CommandResult> ExecuteAsync(CommandContext context)
{
// Manual property extraction
var customerName = GetEntityProperty<string>(context.Entity, "Name");
var customerEmail = GetEntityProperty<string>(context.Entity, "Email");
if (string.IsNullOrWhiteSpace(customerEmail))
{
return CommandResult.ValidationFailed("Email is required");
}
// ... rest of the code
}
}
Typed Command (New Way)
public class SendEmailCommand : TypedDictionaryCommandBase<Customer>
{
public override async Task<CommandResult> ExecuteTypedAsync(
CommandContext context,
Customer customer) // Strongly-typed!
{
// Direct property access - IntelliSense works!
if (string.IsNullOrWhiteSpace(customer.Email))
{
return CommandResult.ValidationFailed("Email is required");
}
var emailService = GetService<IEmailService>(context);
await emailService.SendAsync(customer.Email, "Subject", "Body");
return CommandResult.Successful(
$"Email sent to {customer.Name} ({customer.Email})");
}
}
Benefits:
- IntelliSense for
customer.Email,customer.Name - Compile-time checking (typos caught by compiler)
- No reflection overhead
- Cleaner, more readable code
Example 2: Change Tracking with Typed Command#
public class ActivateCustomerCommand : TypedDictionaryCommandBase<Customer>
{
public override async Task<CommandResult> ExecuteTypedAsync(
CommandContext context,
Customer customer)
{
// OriginalEntity automatically captured in OnBeforeExecuteAsync
var originalStatus = OriginalEntity?.Status;
LogInfo(context, "Activating customer {Name} (Status: {Old} -> Active)",
customer.Name, originalStatus);
if (customer.Status == "Active")
{
return CommandResult.ValidationFailed("Already active");
}
// Update status
customer.Status = "Active";
customer.Modified = DateTime.UtcNow;
// Save to database
var dataService = GetService<DataService>(context);
await dataService.UpdateAsync("Customers", customer.MetaId, new
{
Status = customer.Status,
Modified = customer.Modified
});
// Update context entity (sync back to dynamic entity)
UpdateContextEntity(context, customer);
return CommandResult.Successful($"Customer '{customer.Name}' activated")
{
ShouldReload = true
};
}
}
Benefits:
OriginalEntityautomatically captured- Direct comparison:
OriginalEntity.Statusvscustomer.Status - Type-safe property updates
Example 3: Typed List Command#
public class BulkUpdateStatusCommand : TypedListCommandBase<Customer>
{
public override async Task<CommandResult> ExecuteTypedAsync(
CommandContext context,
List<Customer> customers) // Strongly-typed list!
{
var newStatus = GetRequiredParameter<string>(context, "newStatus");
LogInfo(context, "Updating {Count} customers to status '{Status}'",
customers.Count, newStatus);
var dataService = GetService<DataService>(context);
// Process with progress tracking - type-safe!
return await ProcessSelectedRecordsAsync(context, customers,
async (customer, index, total) =>
{
try
{
LogDebug(context, "Updating {Name} ({Index}/{Total})",
customer.Name, index, total);
await dataService.UpdateAsync("Customers", customer.MetaId, new
{
Status = newStatus,
Modified = DateTime.UtcNow
});
return true;
}
catch (Exception ex)
{
LogError(context, ex, "Failed to update {Name}", customer.Name);
return false;
}
});
}
}
Benefits:
List<Customer>instead ofList<dynamic>- Type-safe lambda:
(customer, index, total) => ... - IntelliSense for
customer.Name,customer.MetaId
Example 4: Typed Document Command#
public class Invoice
{
public Guid MetaId { get; set; }
public string Number { get; set; } = string.Empty;
public DateTime Date { get; set; }
public Guid? CustomerId { get; set; }
public decimal TotalAmount { get; set; }
public string Status { get; set; } = "Draft";
public DateTime? PostedDate { get; set; }
}
public class PostInvoiceCommand : TypedDocumentCommandBase<Invoice>
{
public override async Task<CommandResult> ExecuteTypedAsync(
CommandContext context,
Invoice invoice)
{
LogInfo(context, "Posting invoice {Number}", invoice.Number);
// Validation - type-safe!
if (invoice.Status == "Posted")
{
return CommandResult.ValidationFailed("Already posted");
}
if (!invoice.CustomerId.HasValue)
{
return CommandResult.ValidationFailed("Customer is required");
}
if (invoice.TotalAmount <= 0)
{
return CommandResult.ValidationFailed("Amount must be > 0");
}
// Update status
invoice.Status = "Posted";
invoice.PostedDate = DateTime.UtcNow;
// Save
var dataService = GetService<DataService>(context);
await dataService.UpdateAsync("Invoices", invoice.MetaId, new
{
Status = invoice.Status,
PostedDate = invoice.PostedDate
});
// Update context
UpdateContextEntity(context, invoice);
return CommandResult.Successful($"Invoice {invoice.Number} posted")
{
ShouldReload = true
};
}
public override async Task<CommandResult?> ValidateTypedAsync(
CommandContext context,
Invoice invoice)
{
// Type-safe validation
if (invoice.Status == "Posted")
return CommandResult.ValidationFailed("Already posted");
if (invoice.Status == "Cancelled")
return CommandResult.ValidationFailed("Cannot post cancelled invoice");
return await base.ValidateTypedAsync(context, invoice);
}
}
Entity Conversion#
Automatic Conversion#
Typed commands automatically convert dynamic entities to typed entities using reflection:
// Dynamic entity from context
context.Entity = new Dictionary<string, object>
{
["MetaId"] = Guid.NewGuid(),
["Name"] = "John Doe",
["Email"] = "[email protected]"
};
// Automatically converted to Customer
var customer = new Customer
{
MetaId = /* from dict */,
Name = "John Doe",
Email = "[email protected]"
};
Manual Conversion#
Override ConvertToTypedEntity for custom conversion logic:
protected override Customer? ConvertToTypedEntity(dynamic? dynamicEntity)
{
var dict = dynamicEntity as IDictionary<string, object>;
if (dict == null) return null;
return new Customer
{
MetaId = (Guid)dict["MetaId"],
Name = dict["Name"]?.ToString() ?? "",
Email = dict["Email"]?.ToString(),
// Custom conversion logic here
};
}
Update Context Entity#
After modifying typed entity, sync back to context:
customer.Status = "Active";
UpdateContextEntity(context, customer); // Sync back to context.Entity
Comparison#
| Feature | Dynamic Commands | Typed Commands |
|---|---|---|
| IntelliSense | No | Yes |
| Compile-time checking | No | Yes |
| Runtime overhead | ?? Reflection | Direct access |
| Code readability | ?? Moderate | Excellent |
| Flexibility | High | ?? Moderate |
| Refactoring | Manual | Automatic |
| Unknown schema | Supported | Not supported |
Best Practices#
1. Use DTOs for Typed Commands#
// Good: Simple DTO
public class CustomerDto
{
public Guid MetaId { get; set; }
public string Name { get; set; } = string.Empty;
public string? Email { get; set; }
}
// Bad: Complex entity with navigation properties
public class CustomerEntity
{
public Guid Id { get; set; }
public string Name { get; set; }
public List<Order> Orders { get; set; } // Don't use navigation properties
public Address Address { get; set; } // Don't use complex types
}
2. Define Entities in Shared Project#
ZuloOne.Shared/
??? Entities/
??? Customer.cs
??? Invoice.cs
??? Product.cs
3. Use Partial Classes for Extensibility#
// ZuloOne.Shared/Entities/Customer.cs (generated)
public partial class Customer
{
public Guid MetaId { get; set; }
public string Name { get; set; } = string.Empty;
}
// ZuloOne.Core/Entities/Customer.Extensions.cs (custom)
public partial class Customer
{
// Custom methods
public string GetDisplayName() => $"{Code} - {Name}";
}
4. Validate in ValidateTypedAsync#
public override async Task<CommandResult?> ValidateTypedAsync(
CommandContext context,
Customer customer)
{
// Type-safe validation
if (string.IsNullOrWhiteSpace(customer.Email))
return CommandResult.ValidationFailed("Email is required");
if (!customer.Email.Contains("@"))
return CommandResult.ValidationFailed("Invalid email format");
return await base.ValidateTypedAsync(context, customer);
}
Migration from Dynamic to Typed#
Step 1: Create Entity Class#
public class Customer
{
public Guid MetaId { get; set; }
public string Name { get; set; } = string.Empty;
public string? Email { get; set; }
}
Step 2: Change Base Class#
// Before
public class MyCommand : DictionaryCommandBase
// After
public class MyCommand : TypedDictionaryCommandBase<Customer>
Step 3: Update Method Signatures#
// Before
public override async Task<CommandResult> ExecuteAsync(CommandContext context)
// After
public override async Task<CommandResult> ExecuteTypedAsync(
CommandContext context,
Customer customer)
Step 4: Replace Property Access#
// Before
var name = GetEntityProperty<string>(context.Entity, "Name");
// After
var name = customer.Name; // Direct access!
Summary#
Typed Command Handlers bring:
- Type safety — catch errors at compile time
- Better DX — IntelliSense and refactoring support
- Cleaner code — direct property access
- MIQS compatibility — familiar
DictionaryEventHandlerBase<T>pattern - Performance — no reflection overhead
Use them when:
- Entity structure is known at compile time
- You want maximum type safety
- You're building production-grade commands
Use dynamic commands when:
- Entity structure is unknown
- You need maximum flexibility
- You're building generic/reusable commands
Was this page helpful?