ZuloOne
English
Sign in
Menu

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:

  • OriginalEntity automatically captured
  • Direct comparison: OriginalEntity.Status vs customer.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 of List<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?