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:
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
}
Related Entities¶
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¶
- Add new message version with breaking changes
- Maintain old version temporarily
- Update consumers to handle both versions
- Deprecate old version after migration
- 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¶
- Keep messages simple: Avoid complex object graphs
- Avoid circular references: Keep relationships simple
- Use value types: Prefer primitive types and DTOs
- Version-tolerant: Design for backward compatibility
- Avoid framework types: Don't include framework-specific types
Best Practices¶
Do's¶
- Keep messages simple and focused
- One message = one purpose
- Include only necessary data
-
Avoid complex object hierarchies
-
Use clear naming conventions
- Commands: Verb + Entity + "Command"
-
Events: Entity + Past Tense + "Event"
-
Include correlation IDs
- Enable end-to-end tracing
- Support saga correlation
-
Track message flow
-
Add validation attributes
- Prevent invalid messages
- Fail fast at sender
-
Improve debugging
-
Design for versioning
- Use additive changes
- Make new properties optional
-
Document breaking changes
-
Keep messages transport-agnostic
- No framework-specific types
- No infrastructure dependencies
-
Pure data containers
-
Document messages
- XML documentation comments
- Property descriptions
- Usage examples
Don'ts¶
- Don't include business logic in messages
- Messages are data containers
- Logic belongs in handlers/services
-
Keep contracts pure
-
Don't share domain entities as messages
- Use dedicated message contracts
- Avoid coupling to domain model
-
Prefer DTOs
-
Don't use framework-specific types
- Avoid MassTransit/NServiceBus types in messages
- Use standard .NET types
-
Keep contracts reusable
-
Don't make breaking changes casually
- Plan versioning strategy
- Support multiple versions during migration
-
Document migration path
-
Don't include sensitive data
- Avoid passwords, tokens, PII in messages
- Use references instead
-
Encrypt if necessary
-
Don't create huge messages
- Use claim check pattern for large payloads
- Split into multiple messages if needed
- 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; }
}
Scenario 3: Event with Related Data¶
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.