Conversation
cba521d to
eea63cf
Compare
|
If the resulting API feels weird (and it does) it's cause at the top of the publish api we don't have a generic |
| @@ -1,13 +1,12 @@ | |||
| using System; | |||
| using JustSaying.Models; | |||
|
|
|||
| namespace JustSaying.Messaging.MessageSerialization | |||
| { | |||
| public interface IMessageSerializer | |||
There was a problem hiding this comment.
This whole interface should be generic and object killed from the api surface.
| @@ -1,5 +1,3 @@ | |||
| using JustSaying.Models; | |||
|
|
|||
| namespace JustSaying.Messaging.MessageSerialization | |||
| { | |||
| public interface IMessageSerializationRegister | |||
There was a problem hiding this comment.
This whole interface should be generic and object killed from the api surface.
eea63cf to
c8c8546
Compare
|
|
||
| public async Task<bool> Handle(T message) | ||
| { | ||
| string lockKey = $"{message.UniqueKey()}-{_lockSuffixKeyForHandler}"; |
There was a problem hiding this comment.
So here UniqueKey is a virtual method that defaults to Id.ToString(), I've changed it so we can optionally pass a function when we register the handler, maybe not the right place to surface the api.
|
|
||
| _logger.LogTrace( | ||
| "Handled message with Id '{MessageId}' of type {MessageType} in {TimeToHandle}.", | ||
| message.Id, |
There was a problem hiding this comment.
So here it is, the one place where we actually do something with a property on Message (ok, there was UniqueKey() too I guess). It's on a trace log, we arguably would want to use the SQS message id for this level of debugging, but tying it back to the domain event is going to be the challenge.
|
There's a failing test because the |
| @@ -49,14 +48,19 @@ private IHandlerAsync<T> MaybeWrapWithExactlyOnce<T>(IHandlerAsync<T> handler) w | |||
| throw new Exception("IMessageLock is null. You need to specify an implementation for IMessageLock."); | |||
There was a problem hiding this comment.
We should change this to InvalidOperationException(?)
| throw new Exception("IMessageLock is null. You need to specify an implementation for IMessageLock."); | ||
| } | ||
|
|
||
| if (uniqueKeySelector == null) |
There was a problem hiding this comment.
Should throw this in the caller before we start to do any work, rather than in the delegate further along the call path.
| } | ||
|
|
||
| public void AddMessageHandler<T>(Func<IHandlerAsync<T>> futureHandler) where T : Message | ||
| public void AddMessageHandler<T>(Func<IHandlerAsync<T>> futureHandler, Func<T, string> uniqueKeySelector = default) where T : class |
There was a problem hiding this comment.
If this is optional, how does it avoid causing the ArgumentNullException later? Or is this causing the test failure you mentioned?
There was a problem hiding this comment.
Wow. I didn't know there was a default for Func<T, string>!
There was a problem hiding this comment.
There isn't, well there is, it's just null.
There was a problem hiding this comment.
Oh, haha. I don't know why I assumed otherwise.
There was a problem hiding this comment.
default is just syntactic sugar added in C# 8 (at least I think it was 8, maybe a minor revision of 7) for default(Type). It's null for any reference type and the "zero" value for value types (0, 0l, etc.). The same behaviour you'd get from LINQ's FirstOrDefault()`.
| /// </summary> | ||
| /// <returns>Boolean indicating whether the exception has been handled</returns> | ||
| public Func<Exception, Message, bool> HandleException { get; set; } | ||
| public Func<Exception, object, bool> HandleException { get; set; } |
There was a problem hiding this comment.
Maybe we'd need to bite the bullet and introduce the generics you mentioned? Otherwise, using the error handler is going to be quite bleurgh from a caller having to cast/pattern-match the type all the time.
There was a problem hiding this comment.
Yeah, this is the bit that persuaded me we need to do better. As it stands there are islands of generic type information, we can fix that by having more generic apis at the top level and flowing it down. A big change with this approach would be IMessagePublisher<T> and reworking the builder APIs (yet again).
Or we do the message envelope thing, which is looking more appealing. It would contain Id & Timestamp (perhaps) and a TMessage property, but other than that, is really just a way of allowing people to cleanly "pick out" their message types by defining a concrete envelope, then they get the nice typed guide rails they'd come to expect.
The fiddly bit for us with this approach is then splitting out the properties on (de)serializing, but shouldn't be too tricky.
There was a problem hiding this comment.
Quite the conundrum! I'm not such a fan of the idea of IMessagePublisher<T>, although if we did choose to go down that route, we could probably easily have some sort of IAnyMessagePublisher.
I think it's worth thinking about what use there was in having this typed as Message in the first place. What sort of error handling code would we like to be able to write? If you want to write some generic error handling code then you can't write that in terms of any specific domain message type anyway. In that case you'd rather want to write a try-catch in your handler for that specific message.
There was a problem hiding this comment.
That's my thoughts too @shaynevanasperen, I think a message envelope concept doesn't solve this problem, it acts as a potential way to separate the non-domain stuff from the transporty stuff.
When the base class was Message, you were either:
- using
Id,Tenantetc... onMessage - ignoring the message and doing some generic error reporting (I think this is the primary use case)
- upcasting to the known inheriting types
You could still do those last two with a message of type object, it just looks bad.
There was a problem hiding this comment.
Oh, I just realised that this is for handling SNS publishing errors! I thought it was for errors in implementations of IHandlerAsync<T>. I've had a look around the code now and I see that the equivalent of this on the other side (SQS) is in SqsReadConfiguration. Over there, we work exclusively with the Amazon.SQS.Model.Message type (with no strong typing of our domain message).
So why not have symmetry with that approach and just do the equivalent here? That would mean using Amazon.SimpleNotificationService.Model.PublishRequest making the signature public Func<Exception, PublishRequest, bool> HandleException { get; set; }. The PublishRequest object has the message as a serialized string, so the error logging can easily use that. This abstraction seems like it should remain at the raw transport level, and if consumers want more control they can always wrap the IMessagePublisher this library returns with some decorator that adds custom error logging and possibly even things like a retry mechanism etc.
While looking around at the code which uses this configuration delegate, I noticed something else that we might want to do there. The error handling there only handles exceptions but won't call our error handler if the response.HttpStatusCode is not 200. I haven't checked this though (maybe the Client.PublishAsync call does throw for non-success status codes?).
| { | ||
| throw new ArgumentNullException(nameof(handler)); | ||
| } | ||
| var wrappedHandler = new Func<Exception, object, bool>((ex, message) => handler(ex, (T)message)); |
| int PublishFailureReAttempts { get; set; } | ||
| TimeSpan PublishFailureBackoff { get; set; } | ||
| Action<MessageResponse, Message> MessageResponseLogger { get; set; } | ||
| Action<MessageResponse, object> MessageResponseLogger { get; set; } |
| } | ||
|
|
||
| public void AddMessageHandler<T>(Func<IHandlerAsync<T>> futureHandler) where T : Message | ||
| public void AddMessageHandler<T>(Func<IHandlerAsync<T>> futureHandler, Func<T, string> uniqueKeySelector = default) where T : class |
There was a problem hiding this comment.
Wow. I didn't know there was a default for Func<T, string>!
| } | ||
|
|
||
| public Func<Message, Task<bool>> WrapMessageHandler<T>(Func<IHandlerAsync<T>> futureHandler) where T : Message | ||
| public Func<object, Task<bool>> WrapMessageHandler<T>(Func<IHandlerAsync<T>> futureHandler, Func<T, string> uniqueKeySelector = default) where T : class |
There was a problem hiding this comment.
Is it not possible to just use the SNS/SQS message ID for the unique key? Why does it have to be a value in the domain message?
There was a problem hiding this comment.
The original thinking behind this was that only the developer knows what determines the messages identity, and what is a duplicate. Perhaps the event publisher has OrderId and that is the real identity of the message, or a composite of OrderId and Status. Maybe it's a hash of all of the properties on the message?
There was a problem hiding this comment.
I see. And with the benefit of hindsight, has that thinking paid off? Perhaps it's better to remove this "exactly once" functionality from this library and force consumers to make their solutions inherently idempotent in the domain rather than only at the message bus layer?
| /// </summary> | ||
| /// <returns>Boolean indicating whether the exception has been handled</returns> | ||
| public Func<Exception, Message, bool> HandleException { get; set; } | ||
| public Func<Exception, object, bool> HandleException { get; set; } |
There was a problem hiding this comment.
Quite the conundrum! I'm not such a fan of the idea of IMessagePublisher<T>, although if we did choose to go down that route, we could probably easily have some sort of IAnyMessagePublisher.
I think it's worth thinking about what use there was in having this typed as Message in the first place. What sort of error handling code would we like to be able to write? If you want to write some generic error handling code then you can't write that in terms of any specific domain message type anyway. In that case you'd rather want to write a try-catch in your handler for that specific message.
|
After some discussion, while this is possible it's not something we are prioritising at the moment. Having a little pain involved in sharing types as contracts will nudge people in the direction we want, which is to declare their own type (share contracts not types). |
A draft to start discussion around removing the JS dependency on domain messages.
I think the api would be more intuitive if we go down the route of message envelopes. You get to keep your domain message clean, and still feel like the apis are steering you with types, where as with an unbounded
TMessageif feels like you could easily put any parameter in there and not find out till runtime you messed up.