Skip to content

Messaging Model in ConnectSoft Microservice Template

Purpose & Overview

The Messaging Model defines the message contracts used for inter-service communication in the ConnectSoft Microservice Template. It provides a unified, transport-agnostic abstraction for commands and events that enables loose coupling between services while supporting both MassTransit and NServiceBus messaging frameworks.

Why Messaging Model?

The messaging model offers several key benefits:

  • Transport Agnostic: Messages defined once work with both MassTransit and NServiceBus
  • Clean Separation: Messages are isolated from business logic and infrastructure concerns
  • Type Safety: Strongly-typed contracts prevent runtime errors
  • Versioning Support: Easy to evolve messages while maintaining backward compatibility
  • Validation: Built-in support for DataAnnotations validation
  • Unobtrusive: Minimal coupling with messaging framework assemblies

Architecture Overview

The messaging model sits at the contract layer between services:

Service A
    ├── Domain Logic
    └── Publishes Event / Sends Command
MessagingModel (Contracts)
    ├── Commands (ICommand)
    └── Events (IEvent)
Service B
    ├── Handles Command / Subscribes to Event
    └── Domain Logic

Project Structure

ConnectSoft.MicroserviceTemplate.MessagingModel/
    ├── CreateMicroserviceAggregateRootCommand.cs
    ├── MicroserviceAggregateRootCreatedEvent.cs
    └── ... (other message contracts)

Core Components

1. Message Contracts

Messages are divided into two types:

  • Commands (ICommand): Request an action to be performed (imperative)
  • Events (IEvent): Notification that something occurred (declarative)

2. Interface Definitions

ICommand Interface

// From ConnectSoft.Extensions.MessagingModel
namespace ConnectSoft.Extensions.MessagingModel
{
    /// <summary>
    /// Marker interface to indicate that a class is a command.
    /// </summary>
    public interface ICommand
    {
    }
}

IEvent Interface

// From ConnectSoft.Extensions.MessagingModel
namespace ConnectSoft.Extensions.MessagingModel
{
    /// <summary>
    /// Marker interface to indicate that a class is an event.
    /// </summary>
    public interface IEvent
    {
    }
}

Message Types

Commands

Commands represent an intent to perform an action. They are:

  • Directed: Sent to a specific handler/endpoint
  • Imperative: Request that something happen
  • Named with verbs: CreateEntityCommand, UpdateEntityCommand, DeleteEntityCommand

Command Example

namespace ConnectSoft.MicroserviceTemplate.MessagingModel
{
    using System;
    using System.ComponentModel.DataAnnotations;
    using ConnectSoft.Extensions.DataAnnotations;
    using ConnectSoft.Extensions.MessagingModel;

    /// <summary>
    /// Command that used to create IMicroserviceAggregateRoot using messaging model.
    /// </summary>
    public class CreateMicroserviceAggregateRootCommand : ICommand
    {
        /// <summary>
        /// Gets or sets a object identifier.
        /// </summary>
        [Required]
        [NotDefault]
        public Guid ObjectId { get; set; }
    }
}

Command Characteristics

  • Single Handler: Commands typically have one handler
  • Expects Success/Failure: Commands can succeed or fail
  • Idempotent When Possible: Should be safe to retry
  • Contains Intent: Clear about what action is requested

Events

Events represent a fact that something occurred. They are:

  • Broadcast: Published to all interested subscribers
  • Declarative: State what happened, not what should happen
  • Named in past tense: EntityCreatedEvent, EntityUpdatedEvent, EntityDeletedEvent

Event Example

namespace ConnectSoft.MicroserviceTemplate.MessagingModel
{
    using System;
    using System.ComponentModel.DataAnnotations;
    using ConnectSoft.Extensions.DataAnnotations;
    using ConnectSoft.Extensions.MessagingModel;

    /// <summary>
    /// Event that used to raise when IMicroserviceAggregateRoot successfully created.
    /// </summary>
    public class MicroserviceAggregateRootCreatedEvent : IEvent
    {
        /// <summary>
        /// Gets or sets a object identifier.
        /// </summary>
        [Required]
        [NotDefault]
        public Guid ObjectId { get; set; }
    }
}

Event Characteristics

  • Multiple Subscribers: Events can have multiple handlers
  • Immutable Facts: Represent something that already happened
  • No Return Value: Events are fire-and-forget
  • Contains Context: Include enough information for subscribers

Message Validation

Messages support DataAnnotations validation:

public class CreateMicroserviceAggregateRootCommand : ICommand
{
    [Required]
    [NotDefault]
    public Guid ObjectId { get; set; }

    [Required]
    [StringLength(100)]
    public string Name { get; set; }

    [Range(0, 100)]
    public int Priority { get; set; }
}

Validation Attributes

Common validation attributes:

  • [Required]: Property must have a value
  • [NotDefault]: Property must not be default value (e.g., Guid.Empty)
  • [StringLength]: String length constraints
  • [Range]: Numeric range constraints
  • [EmailAddress]: Email format validation
  • [Url]: URL format validation

Framework Support

MassTransit

MassTransit doesn't have built-in validation, but you can add it via middleware:

config.UseInMemoryOutbox();
// Add custom validation middleware

NServiceBus

NServiceBus supports built-in DataAnnotations validation:

endpointConfiguration.UseDataAnnotationsValidation(
    outgoing: true,  // Validate outgoing messages
    incoming: true); // Validate incoming messages

Validation errors result in: - Validation exception thrown - Message handled by recoverability (retry/fault) - Added to unrecoverable exceptions list

Message Naming Conventions

Commands

Pattern Example Notes
{Action}{Entity}Command CreateOrderCommand Standard action verbs
{Action}{Entity}{Context}Command ApproveOrderPaymentCommand Specific action context
Process{Entity}{Action}Command ProcessOrderPaymentCommand Processing action

Events

Pattern Example Notes
{Entity}{Action}Event OrderCreatedEvent Past tense
{Entity}{Action}{Context}Event OrderPaymentApprovedEvent Specific context
{Entity}{State}ChangedEvent OrderStatusChangedEvent State change

Best Practices

  • ✅ Use clear, descriptive names
  • ✅ Use past tense for events
  • ✅ Use imperative verbs for commands
  • ✅ Include entity/aggregate name
  • ❌ Avoid generic names like Message, Data, Info
  • ❌ Avoid abbreviations unless widely understood

Message Properties

Common Patterns

Identifiers

public class OrderCreatedEvent : IEvent
{
    [Required]
    [NotDefault]
    public Guid OrderId { get; set; }  // Primary identifier

    public Guid? CorrelationId { get; set; }  // Optional correlation
}

Timestamps

public class OrderCreatedEvent : IEvent
{
    public Guid OrderId { get; set; }

    [Required]
    public DateTimeOffset CreatedAt { get; set; }  // Event timestamp

    public DateTimeOffset? ProcessedAt { get; set; }  // Optional processing time
}
public class OrderCreatedEvent : IEvent
{
    public Guid OrderId { get; set; }
    public Guid CustomerId { get; set; }
    public List<Guid> ProductIds { get; set; }
}

Context Information

public class OrderCreatedEvent : IEvent
{
    public Guid OrderId { get; set; }
    public string SourceService { get; set; }  // Which service created it
    public string UserId { get; set; }  // Who triggered it
    public Dictionary<string, string> Metadata { get; set; }  // Additional context
}

Message Versioning

Additive Changes

Add new optional properties without breaking existing consumers:

// Version 1
public class OrderCreatedEvent : IEvent
{
    public Guid OrderId { get; set; }
    public Guid CustomerId { get; set; }
}

// Version 2 (additive)
public class OrderCreatedEvent : IEvent
{
    public Guid OrderId { get; set; }
    public Guid CustomerId { get; set; }
    public string? SourceService { get; set; }  // New optional property
    public DateTimeOffset? ProcessedAt { get; set; }  // New optional property
}

Breaking Changes

For breaking changes, create a new message version:

// Version 1 (deprecated)
public class OrderCreatedEvent : IEvent
{
    public Guid OrderId { get; set; }
    public string CustomerEmail { get; set; }  // Changed to CustomerId
}

// Version 2 (new)
public class OrderCreatedEventV2 : IEvent
{
    public Guid OrderId { get; set; }
    public Guid CustomerId { get; set; }  // Changed property
}

Migration Strategy

  1. Add new message version with breaking changes
  2. Maintain old version temporarily
  3. Update consumers to handle both versions
  4. Deprecate old version after migration
  5. Remove old version when all consumers migrated

Integration with Messaging Frameworks

MassTransit Integration

MassTransit works with POCO classes:

// Message contract (no MassTransit reference needed)
public class MicroserviceAggregateRootCreatedEvent : IEvent
{
    public Guid ObjectId { get; set; }
}

// Publishing
await publishEndpoint.Publish(new MicroserviceAggregateRootCreatedEvent
{
    ObjectId = entity.ObjectId
});

// Consuming
public class EventConsumer : IConsumer<MicroserviceAggregateRootCreatedEvent>
{
    public Task Consume(ConsumeContext<MicroserviceAggregateRootCreatedEvent> context)
    {
        var @event = context.Message;
        // Handle event
        return Task.CompletedTask;
    }
}

NServiceBus Integration

NServiceBus uses unobtrusive mode with conventions:

// Message contract (no NServiceBus reference needed)
public class MicroserviceAggregateRootCreatedEvent : IEvent
{
    public Guid ObjectId { get; set; }
}

// Unobtrusive conventions
conventions.DefiningCommandsAs(type =>
    typeof(ICommand).IsAssignableFrom(type));

conventions.DefiningEventsAs(type =>
    typeof(IEvent).IsAssignableFrom(type));

// Publishing
await messageSession.Publish(new MicroserviceAggregateRootCreatedEvent
{
    ObjectId = entity.ObjectId
});

// Handling
public class EventHandler : IHandleMessages<MicroserviceAggregateRootCreatedEvent>
{
    public Task Handle(MicroserviceAggregateRootCreatedEvent message, IMessageHandlerContext context)
    {
        // Handle event
        return Task.CompletedTask;
    }
}

IEventBus Abstraction

The template provides an IEventBus interface for transport-agnostic messaging:

namespace ConnectSoft.Extensions.MessagingModel
{
    public interface IEventBus
    {
        Task SendCommand<TCommand>(TCommand command, CancellationToken cancellationToken = default)
            where TCommand : ICommand;

        Task PublishEvent<TEvent>(TEvent @event, CancellationToken cancellationToken = default)
            where TEvent : IEvent;
    }
}

Usage Example

public class MicroserviceAggregateRootsProcessor : IMicroserviceAggregateRootsProcessor
{
    private readonly IEventBus eventBus;

    public MicroserviceAggregateRootsProcessor(IEventBus eventBus)
    {
        this.eventBus = eventBus;
    }

    public async Task<IMicroserviceAggregateRoot> CreateMicroserviceAggregateRoot(
        CreateMicroserviceAggregateRootInput input,
        CancellationToken token = default)
    {
        var entity = new MicroserviceAggregateRoot { ObjectId = input.ObjectId };
        // ... save entity ...

        // Publish event via abstraction
        await this.eventBus.PublishEvent(new MicroserviceAggregateRootCreatedEvent
        {
            ObjectId = entity.ObjectId
        }, token);

        return entity;
    }
}

Framework Adapters

MassTransit Adapter

public class MassTransitAdapter : IEventBus
{
    private readonly IPublishEndpoint publishEndpoint;
    private readonly ISendEndpointProvider sendEndpointProvider;

    public async Task PublishEvent<TEvent>(TEvent @event, CancellationToken cancellationToken = default)
        where TEvent : IEvent
    {
        await publishEndpoint.Publish(@event, cancellationToken);
    }

    public async Task SendCommand<TCommand>(TCommand command, CancellationToken cancellationToken = default)
        where TCommand : ICommand
    {
        // MassTransit uses Publish for both commands and events
        await publishEndpoint.Publish(command, cancellationToken);
    }
}

NServiceBus Adapter

public class NServiceBusAdapter : IEventBus
{
    private readonly IMessageSession messageSession;

    public async Task PublishEvent<TEvent>(TEvent @event, CancellationToken cancellationToken = default)
        where TEvent : IEvent
    {
        await messageSession.Publish(@event);
    }

    public async Task SendCommand<TCommand>(TCommand command, CancellationToken cancellationToken = default)
        where TCommand : ICommand
    {
        await messageSession.Send(command);
    }
}

Message Serialization

Messages are serialized by the messaging framework:

MassTransit

  • Default: JSON (System.Text.Json)
  • Configurable: Can use XML, BSON, or custom serializers
  • POCO Support: Works with any serializable class

NServiceBus

  • Default: System.Text.Json (SystemJsonSerializer)
  • Configurable: Can use XML, JSON.NET, or custom serializers
  • Interface Support: Uses unobtrusive mode for interface-based messages

Serialization Best Practices

  1. Keep messages simple: Avoid complex object graphs
  2. Avoid circular references: Keep relationships simple
  3. Use value types: Prefer primitive types and DTOs
  4. Version-tolerant: Design for backward compatibility
  5. Avoid framework types: Don't include framework-specific types

Best Practices

Do's

  1. Keep messages simple and focused
  2. One message = one purpose
  3. Include only necessary data
  4. Avoid complex object hierarchies

  5. Use clear naming conventions

  6. Commands: Verb + Entity + "Command"
  7. Events: Entity + Past Tense + "Event"

  8. Include correlation IDs

  9. Enable end-to-end tracing
  10. Support saga correlation
  11. Track message flow

  12. Add validation attributes

  13. Prevent invalid messages
  14. Fail fast at sender
  15. Improve debugging

  16. Design for versioning

  17. Use additive changes
  18. Make new properties optional
  19. Document breaking changes

  20. Keep messages transport-agnostic

  21. No framework-specific types
  22. No infrastructure dependencies
  23. Pure data containers

  24. Document messages

  25. XML documentation comments
  26. Property descriptions
  27. Usage examples

Don'ts

  1. Don't include business logic in messages
  2. Messages are data containers
  3. Logic belongs in handlers/services
  4. Keep contracts pure

  5. Don't share domain entities as messages

  6. Use dedicated message contracts
  7. Avoid coupling to domain model
  8. Prefer DTOs

  9. Don't use framework-specific types

  10. Avoid MassTransit/NServiceBus types in messages
  11. Use standard .NET types
  12. Keep contracts reusable

  13. Don't make breaking changes casually

  14. Plan versioning strategy
  15. Support multiple versions during migration
  16. Document migration path

  17. Don't include sensitive data

  18. Avoid passwords, tokens, PII in messages
  19. Use references instead
  20. Encrypt if necessary

  21. Don't create huge messages

  22. Use claim check pattern for large payloads
  23. Split into multiple messages if needed
  24. Keep message size reasonable

Common Scenarios

Scenario 1: Simple Command

public class CreateOrderCommand : ICommand
{
    [Required]
    [NotDefault]
    public Guid OrderId { get; set; }

    [Required]
    [NotDefault]
    public Guid CustomerId { get; set; }

    [Required]
    [MinLength(1)]
    public List<OrderItemDto> Items { get; set; }

    public DateTimeOffset? RequestedDeliveryDate { get; set; }
}

Scenario 2: Simple Event

public class OrderCreatedEvent : IEvent
{
    [Required]
    [NotDefault]
    public Guid OrderId { get; set; }

    [Required]
    [NotDefault]
    public Guid CustomerId { get; set; }

    [Required]
    public decimal TotalAmount { get; set; }

    [Required]
    public DateTimeOffset CreatedAt { get; set; }

    public Guid? CorrelationId { get; set; }
}
public class OrderShippedEvent : IEvent
{
    [Required]
    [NotDefault]
    public Guid OrderId { get; set; }

    [Required]
    [NotDefault]
    public Guid ShipmentId { get; set; }

    [Required]
    public string TrackingNumber { get; set; }

    [Required]
    public ShippingAddressDto ShippingAddress { get; set; }

    [Required]
    public DateTimeOffset ShippedAt { get; set; }
}

Scenario 4: Command with Validation

public class UpdateOrderCommand : ICommand
{
    [Required]
    [NotDefault]
    public Guid OrderId { get; set; }

    [StringLength(500)]
    public string? Notes { get; set; }

    [Range(0, 100)]
    public int? Priority { get; set; }

    [EmailAddress]
    public string? NotificationEmail { get; set; }
}

Summary

The Messaging Model in the ConnectSoft Microservice Template provides:

  • Transport-Agnostic Contracts: Works with MassTransit and NServiceBus
  • Type Safety: Strongly-typed commands and events
  • Clear Separation: Isolated from business logic and infrastructure
  • Validation Support: DataAnnotations validation
  • Versioning Strategy: Support for evolving messages
  • Unobtrusive Mode: Minimal coupling with messaging frameworks
  • Clean Architecture: Fits seamlessly with DDD and CQRS patterns

By following these patterns and best practices, the messaging model becomes a powerful foundation for reliable, scalable inter-service communication.