0

I'm working on a message bus class library for Redis (StackExchange.Redis) and NATS (AlterNats). The SubscribeAsync method uses observers from System.Reactive for the callbacks.

The problem is _connection in NatsSubscriber has only DisposeAsync overload and I cannot really use async in Disposable.Create because it would become async void. What should I do?

public sealed class NatsSubscriber<TKey, TMessage> : IBusSubscriber<TKey, TMessage>
{
    private readonly NatsConnectionFactory _connectionFactory;

    public NatsSubscriber(NatsConnectionFactory connectionFactory)
    {
        _connectionFactory = connectionFactory;
    }

    public async Task<IDisposable> SubscribeAsync(TKey key, Action<TMessage> callback)
    {
        var subject = GetSubjectString(key);

        if (subject == null)
        {
            throw new ArgumentNullException(nameof(key));
        }

        var connection = await _connectionFactory.GetConnectionAsync();

        var observer = Observer.Create(callback);

        await connection.SubscribeAsync<TMessage>(subject, data =>
        {
            observer.OnNext(data);
        });

        return Disposable.Create(subject, _ =>
        {
            // await connection.DisposeAsync();
            observer.OnCompleted();
        });
    }

    private string? GetSubjectString(TKey key)
    {
        return key switch
        {
            NatsKey natsKey => natsKey.Key,
            string s => s,
            _ => key?.ToString()
        };
    }
}

public sealed class RedisSubscriber<TKey, TMessage> : IBusSubscriber<TKey, TMessage>
{
    private readonly IConnectionMultiplexer _connectionMultiplexer;
    private readonly IRedisSerializer _serializer;

    public RedisSubscriber(IConnectionMultiplexer connectionMultiplexer, IRedisSerializer serializer)
    {
        _connectionMultiplexer = connectionMultiplexer;
        _serializer = serializer;
    }

    public async Task<IDisposable> SubscribeAsync(TKey key, Action<TMessage> callback)
    {
        var channel = CreateChannel(key);
        var subscriber = _connectionMultiplexer.GetSubscriber();
        var observer = Observer.Create(callback);

        await subscriber.SubscribeAsync(channel, (_, redisValue) =>
        {
            observer.OnNext(_serializer.Deserialize<TMessage>(redisValue.ToString()));
        }).ConfigureAwait(false);

        return Disposable.Create(channel, x =>
        {
            subscriber.Unsubscribe(x);
            observer.OnCompleted();
        });
    }

    private RedisChannel CreateChannel(TKey key)
    {
        return key switch
        {
            string s => new RedisChannel(s, RedisChannel.PatternMode.Auto),
            byte[] v => new RedisChannel(v, RedisChannel.PatternMode.Auto),
            _ => new RedisChannel(_serializer.Serialize(key), RedisChannel.PatternMode.Literal)
        };
    }
}

nop
  • 4,711
  • 6
  • 32
  • 93
  • I have a [`Disposables` library](https://github.com/StephenCleary/Disposables) that supports async disposables. However, that would require `SubscribeAsync` to return `IAsyncDisposable` instead of `IDisposable`. – Stephen Cleary Mar 02 '23 at 15:11
  • You can create a `non-async` version of the Dispose method that calls `DisposeAsync` and waits for the task to complete – Pranav Bilurkar Mar 02 '23 at 15:17
  • @StephenCleary that is okay as long as both RedisSubscriber and NatsSubscriber are using it. It would be nice to see your approach – nop Mar 02 '23 at 15:20

1 Answers1

2

I have a Disposables library that has full support for async disposables. This would require you to change IBusSubscriber<TKey, TMessage>.SubscribeAsync to return Task<IAsyncDisposable> instead of Task<IDisposable>, and update usages from using to await using.

The NatsSubscriber is mostly straightforward. I'm not sure of the purpose of the first parameter passed to Disposable.Create (it's not part of the Rx Disposable or anything else I could find), so I'm just ignoring it:

public sealed class NatsSubscriber<TKey, TMessage> : IBusSubscriber<TKey, TMessage>
{
  public async Task<IAsyncDisposable> SubscribeAsync(TKey key, Action<TMessage> callback)
  {
    ...
    return AsyncDisposable.Create(async () =>
    {
      await connection.DisposeAsync();
      observer.OnCompleted();
    });
  }
}

The RedisSubscriber has synchronous disposal, but my Disposables library has a helper for treating sync as async:

public sealed class RedisSubscriber<TKey, TMessage> : IBusSubscriber<TKey, TMessage>
{
  public async Task<IAsyncDisposable> SubscribeAsync(TKey key, Action<TMessage> callback)
  {
    ...
    return Disposable.Create(() =>
    {
      subscriber.Unsubscribe(channel);
      observer.OnCompleted();
    }).ToAsyncDisposable();
  }
}
Stephen Cleary
  • 437,863
  • 77
  • 675
  • 810