SlimMessageBus icon indicating copy to clipboard operation
SlimMessageBus copied to clipboard

Allow Access to Full Message During Deserialization

Open gnios opened this issue 7 months ago • 9 comments

Hello everyone! First of all, congratulations on the excellent work with SlimMessageBus.

I am using SlimMessageBus with Azure Service Bus and I have encountered a need in scenarios where message consumption depends on information present in the headers, such as content-type, as well as the Label of the message (a field widely used in Azure Service Bus).

Currently, I noticed that the serializer receives only the Body of the message to perform deserialization. However, for some more advanced scenarios, I need to access both the Label and some header fields in order to decide which serializer/deserializer to use globally and dynamically (for example: content-type application/json → JsonSerializer, content-type avro/binary → AvroSerializer, etc.).

I would like to know if there is any official recommendation for this type of problem, where it is necessary to access information beyond the Body at deserialization time.

If there is no such recommendation, I would like to know if it would be interesting for me to open a PR to allow passing the full message to the serializer, instead of just the Body, enabling this kind of logic during deserialization.

Thank you in advance for your attention and I am available to further detail the scenario if necessary.

gnios avatar May 27 '25 02:05 gnios

Thank you for the kind words and for taking the time to suggest this feature!

Here are a couple of ideas that might help with your use case today:

  • If your messages are coming through different transports (e.g., Azure Service Bus and Kafka), you can configure each to use a different serialization plugin. This allows you to tailor serialization per transport.

  • If you're using a single transport but need to switch serialization strategies based on the MessageType header (which SlimMessageBus uses to infer the message type), the Hybrid Serializer can help. It allows routing messages to the appropriate serializer, such as Avro or JSON, based on the message type.

That said, if you require more advanced or custom logic—for example, inspecting both headers and payload before deciding on the serializer - we would need to extend the IMessageSerializer interface to support this. This is definitely a reasonable enhancement, and if that's aligned with your use case, we’d be happy to explore it further.

Let us know your thoughts!

zarusz avatar May 27 '25 16:05 zarusz

Hello @zarusz,

My need is precisely the one not covered by standard serializers.

For now, to work around this issue, I have implemented a custom "ServiceBusMessageBus," overriding the CreateConsumers method, where I can access the complete message and pass it to my serializer.

This allowed me to implement a dynamic serializer depending on the "content-type." It turned out something like this:

  public object Deserialize(Type t, object message, byte[] payload)
    {
        try
        {
            // Se o message for ServiceBusReceivedMessage, podemos extrair o content-type
            string contentType = null;
            if (message is ServiceBusReceivedMessage sbMessage && sbMessage.ContentType != null)
            {
                contentType = sbMessage.ContentType;
            }
            
            // Usar o serializador com o content-type se disponível
            if (contentType != null)
            {
                var result = _serializerService.Deserialize(payload, t, contentType);
                this._logger.LogDebug("Type {0} deserialized using MessageSerializerService with content-type {1}", t, contentType);
                return result;
            }
            
            // Fallback para deserialização simples
            return Deserialize(t, payload);
        }
        catch (Exception ex)
        {
            this._logger.LogError(ex, "Failed to deserialize using MessageSerializerService. Falling back to direct deserialization");
            return Deserialize(t, payload);
        }
    }

For now, this implementation is in place so that application development isn't blocked, but my intention is to create a strategy for each message type (Azure Service Bus, RabbitMQ, etc.) to maintain consistency in deserialization (which, in this case, could be JSON or MSGPACK + GZIP) across all providers.

Additionally, there is a need to retrieve the "HEADER" -> "Type" from legacy messages to correctly parse them into objects (similar to what TypeResolver does).

Since this implementation makes sense, I plan to take some time to submit a PR with these changes. Does that sound good to you, or do you see a better approach than modifying the IMessageSerializer interface?

gnios avatar May 28 '25 21:05 gnios

Thanks, @gnios — that definitely helps clarify the scenario.

My suggestion would be to extend the core IMessageSerializer interface to provide access to message headers. From there, we can make the necessary adjustments in the host implementations. Most transport plugins will likely require minimal changes.

This update would allow serializer plugins to receive the message headers, byte payload, and expected message type — enabling them to determine how to handle the message (e.g., Avro vs. JSON, or applying compression). The benefit is a consistent and unified behavior across all transports and plugins.

If you'd like to move forward with this approach, I'm happy to accept a PR. Alternatively, if you're unsure where to start, I can prototype the changes in a branch and hand it over, or even publish a preview release for you to test.

Let me know how you'd like to proceed.

zarusz avatar May 29 '25 07:05 zarusz

@gnios just a heads up - I've started to prototype the change as per the suggestion mentioned earlier. I have a separate feature branch. Looks like it will be rather trivial to add.

zarusz avatar Jun 04 '25 21:06 zarusz

Hi @zarusz , I had scheduled this change to start on Monday, but I can bring it forward and support it if you share the branch with me.

One idea I had was to try passing the IConsumeContext to the serializer, since I noticed there are some properties beyond the headers in the messages. For example, in Service Bus there's the Label, which is a message property—I believe other providers may have something similar. I was planning to experiment with this approach. What do you think?

P.S.: I thought I had replied to your last message—sorry about that!

gnios avatar Jun 05 '25 14:06 gnios

@gnios no worries. My branch is not ready for prime time and still need to fix some test etc, but will be there shortly.

Interesting, with the ASB Message Label. What would you need it for? Kafka would have partition-topic-offset, key. RabbitMq would have a sequence number. Wondering about use cases. I am a bit hesitant to pass too much to the serializer as it exposes more internals.

So far this is what I have (besides the other changes to make it work):

public interface IMessageSerializer<TPayload>
{
    TPayload Serialize(Type messageType, IDictionary<string, object> headers, object message);
    object Deserialize(Type messageType, IReadOnlyDictionary<string, object> headers, TPayload payload);
}

To get ahold of the transport message, the Deserialize would also have to get the transport message object (e.g. ServcieBusRecievedMessage). The IConsumerContext is in a higher layer inside SlimMessageBus.Host.

Let me know your thoughts.

zarusz avatar Jun 05 '25 16:06 zarusz

Hello, in the service bus we have some properties that are part of the message itself (I'm sending you a picture so you can check), and I believe there is important information there for a serializer, such as "ContentType" and "Label".

Going into more detail, with the "ContentType" I can select the correct serializer for that message.

As for the "Label", depending on the strategy of whoever is using the app, it can carry the same weight as the "MessageType" header.

I believe we could add this information to the dictionary; I’m just not sure if the name "Headers" would still make sense after that. However, this way each provider could add similar information, allowing us to have a generic serializer, without needing to assign one per message or globally, and be able to deserialize everything from "Json" to "Msgpack", depending on the message.

gnios avatar Jun 06 '25 16:06 gnios

I forgot to mention my use case, so let me explain:

We have several microservices here and we use an event-driven architecture, meaning these microservices communicate with each other via messages on the Service Bus.

For that, we already have a Notifications contract in place, so that all these services can communicate with each other.

The point is that we have two serialization options for these contracts (JSON and GZIP + MsgPack), and this serialization is controlled by the message's "ContentType" and "Label". In other words, we look up in a dictionary which class that "Label" belongs to (very similar to the typeResolver strategy used here), and then we check the "ContentType", find the deserializer responsible for that MIME type, and perform the deserialization.

In summary, we need to be able to maintain some kind of generic serialization/deserialization mechanism, so we can react based on the information provided in the messages.

gnios avatar Jun 06 '25 16:06 gnios

@gnios that helps a lot. The headers in ASB would represent application properties of the message. If we need to get ahold of other properties of the message (content type etc), then it makes sense to also add the the custom transport message.

Image

I can amend my branch to also include it here soon.

zarusz avatar Jun 06 '25 17:06 zarusz

Hi @gnios, I’ve opened a PR for this feature in #408 - feel free to take a look when you get a chance.

The key change is in the IMessageSerializer interface, which now allows:

  • Accepting the native transport message (to be provided per transport),
  • Handling message headers through a unified abstraction - enabling consistent support for application-specific headers across transports (e.g., for Azure Service Bus this maps to application properties).

Let me know if this direction works for your use case. You’ll be able to extend the serializer interface to implement custom logic as needed.

A couple of additional notes (also mentioned in the interface comments):

  • During serialization, headers can be modified or augmented.
  • The transportMessage may be null during serialization in some cases, as some transports require the payload to be serialized first before the native message object can be created.

Also, I can issue a preview release if you want to check it out before we merge it.

Looking forward to your feedback!

zarusz avatar Jun 14 '25 23:06 zarusz

Please take a look at the linked PR. I'm happy to publish a pre-release so it can be tested and previewed early. I'd like to include this in the upcoming 3.3.0 release, but I need feedback first to confirm it addresses the intended use case. Thanks!

zarusz avatar Jun 26 '25 09:06 zarusz

There is a pre-release 3.3.0 version available if anyone wants to try this out. @gnios already tried and earlier pre-release, and it was meeting his expectations.

Closing as done.

zarusz avatar Jun 26 '25 21:06 zarusz