Alexander Yarmolovich
Alexander Yarmolovich

Reputation: 45

MassTransit - best practices to initialize complex messages

Let's say I have an ASP.NET Core Web API application, and one of my action methods receives IEnumerable<AddressModel> addresses, where AddressModel looks like:

public class AddressModel
{
    public string Street { get; set; }
    public string ZipCode { get; set; }
    public string City { get; set; }
    public string Country { get; set; }
}

I'd like to use it to construct a more complex message object and to send it via MassTransit - Addresses will be a nested property and I'll set more fields:

public interface ICreateContact
{
    ContactTypeEnum Type { get; }
    List<IAddress> Addresses { get; }
}

public interface IAddress
{
    string Street { get; }
    string ZipCode { get; }
    string City { get; }
    string Country { get; }
}

So, how to create such message in the most convenient readable way? I see a few options, but all of them have drawbacks:

  1. The most straightforward option:
await _messageBus.Send<ICreateContact>(new {
    Type = ContactTypeEnum.Single,
    Addresses = addresses.Select(a => new
    {
        Street = a.Street,
        ZipCode = a.ZipCode,
        City = a.City,
        Country = a.Country
    })
});

Will work, but I don't want to write a lot of code to assign each property and I can't use Automapper, because there're no setters in ICreateContact/IAddress.

  1. Intermediate class:
public class CreateContact
{
    public ContactTypeEnum Type { get; set; }
    public List<Address> Addresses { get; set; }

    public class Address
    {
        public string Street { get; set; }
        public string ZipCode { get; set; }
        public string City { get; set; }
        public string Country { get; set; }
    }
}
var command = new CreateContact
{
    Type = ContactTypeEnum.Single,
    Addresses = addresses.Select(a => _mapper.Map<CreateContact.Address>(a)).ToList()
};

await _messageBus.Send<ICreateContact>(command);

Looks better, but what if I want it implement ICreateContact/IAddress, so compiler will tell if I construct the message incorrectly? I can't do that, because if I write CreateContact : ICreateContact, my Addresses field must be of type List<IAddress> (even if I make Address implement IAddress).

So to summarize my questions:

  1. Is it possible to avoid intermediate class and use option 1 with automatic mapping of the properties (with or without Automapper)?
  2. Is it a good idea to create a strongly-typed classes for message contracts in every service?
  3. If so - how to deal with nested properties of interface types?
  4. If not - what to do if a message contract has 30 fields, one of them is renamed and you need to know which one without documentation?

Upvotes: 4

Views: 2879

Answers (1)

Chris Patterson
Chris Patterson

Reputation: 33388

First, you don't need an immediate class, you can just use another nested anonymous type to initialize the address list. MassTransit's message initializer does most mapping between types by convention, including type conversions (string to int, date, etc.).

Since all the property names match, you could easily use:

await _messageBus.Send<ICreateContact>(new {
    Type = ContactTypeEnum.Single,
    Addresses = addresses
});

And it would initial the address list with elements from the input addresses.

Second, your question about strongly typed classes for producers? It depends. I definitely wouldn't share those concrete types outside of the message producer. If you want that level of property type/name validation, you can do it. It's just more classes to manage in your project. I've seen it done in some teams that needed that level of direction for engineers, but more often I see teams happy to just let it go and guard contract changes at the repository level.

Third, you can make the concrete type have the correct element type (IAddress) and add the Address concrete types to it. That eliminates the array type issue, and still allows you to use an Address concrete type that implements IAddress.

Fourth, seriously, don't rename properties! If it's a mistake, fix it globally using a tool and grep to find any usages across repositories. But once it's in production, renaming it can break things. In that case, add a new property (correctly spelled, whatever) and updated producers to send both eventually, and once systems are upgraded across the board, deprecate the original property.

There are more details on what MassTransit support with message initializers in the documentation.

Upvotes: 5

Related Questions