Skip to content

Early-response API feedback #44

@soxtoby

Description

@soxtoby

A common issue with the current handler API is that SlackNet waits for all of the handlers' work to complete before responding to Slack, which doesn't wait very long before showing errors in the UI. The v0.7 release includes an experimental API for responding to Slack before completing all the work a handler performs. This lets you implement a handler like this:

public Task Handle(SlashCommand command, Responder<SlashCommandResponse> respond)
{
    // Provides feedback as quickly as possible
    await respond(new SlashCommandResponse {
        Message = new Message { Text = "Command received" } });

    // Posts more information once it's available
    var info = await FetchSomeInfoFromARemoteService();
    await _slackApi.Chat.PostMessage(new Message { 
        Text = $"Here's what you asked for: {info}" });
}

Eventually I want to replace the existing API, rather than having two parallel handler APIs, and so the goals are:

  • Limit the cost of upgrading
  • Make it clear how to respond to Slack
  • Handlers that respond early should be easy to write, with minimal cognitive overhead

I'd appreciate any feedback or suggestions people may have.

Other options that were considered

Respond method on the input

public async Task Handle(SlashCommand command)
{
    await command.Respond(new SlashCommandResponse { Message = new Message() });
    await DoSomeMoreWork();
}

Simpler than a Responder callback parameter, but I'm not sure it's obvious enough how to respond.

Append extra work to the return value

public async Task<SlashCommandResponse> Handle(SlashCommand command)
{
    return new SlashCommandResponse { Message = new Message() }
        .AndThen(async () => await DoSomeMoreWork());
}

For handlers with a return value, this keeps the same method signature, which means zero upgrade pain. Doesn't work so well for handlers without a return value, like block action handlers. Having to split the extra work into another function isn't great, either.

Specific return type

public async Task<AsyncResponse<SlashCommandResponse>> Handle(SlashCommand command)
{
    return new AsyncResponse<SlashCommandResponse>
        new SlashCommandResponse { Message = new Message() },
        async () => await DoSomeMoreWork());
}

This makes it really clear how to do more work after responding, but it's pretty ugly, and once again you need to split the extra work into another function.

Async enumerables

public async IAsyncEnumerable<SlashCommandResponse> Handle(SlashCommand command)
{
    yield return new SlashCommandResponse { Message = new Message() };
    await DoSomeMoreWork();
}

I really liked the elegance of this, but it's not clear that you're only meant to return one response, and development on older .NET Core/Framework versions with C# 7 is not straightforward.

Concerns with the current design

  • While the general structure of handler code remains the same, upgrading handlers would require removing the return type, adding the new Responder parameter, and calling respond instead of returning, which would be a pain if you have a lot of handlers. Could be mitigated with a code analyzer + fix.
  • What happens when you call respond multiple times?
    • Should it throw or just ignore subsequent calls?
    • Requests such as block actions are only waiting for an OK response, and can be handled by multiple handlers - should the first response win, or should SlackNet wait for every handler to respond before sending back the OK response?
  • The Responder delegate is simple, but not very extensible. An IResponder interface would make the code slightly more verbose (responder.Respond() instead of just respond()), but it would allow adding new response options in an obvious place. Delegates can have extension methods, which would cover most scenarios, but it might not be obvious enough that they're available.

Once again, any feedback is appreciated. Everything in this API is in an Experimental namespace and marked as Obsolete, because I want to be able to make improvements before "releasing" it.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions