Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Configuring Swashbuckle With DelegatingHandler as message dispatcher

I've got a Web API that is a really thin piece of infrastructure that contains nothing more than two DelegatingHandler implementations that dispatch incoming messages to message handler implementations that are defined in the business layer. This means that there are no Controllers and no controller actions; the API is defined based solely on the messages. This means that no code changes in this infrastructure layer are required when new features are implemented.

For instance, we have messages like:

  • CreateOrderCommand
  • ShipOrderCommand
  • GetOrderByIdQuery
  • GetUnshippedOrdersForCurrentCustomerQuery

The delegating handlers determine the exact message based on the url, and the request contents is deserialized to an instance of that message type, after which that message is forwarded to the appropriate message handler. For instance, these messages are (currently) mapped to the following urls:

  • api/commands/CreateOrder
  • api/commands/ShipOrder
  • api/queries/GetOrderById
  • api/queries/GetUnshippedOrdersForCurrentCustomer

As you can imagine, this way of working with Web API simplifies development and boosts development performance; there's less code to write and less code to test.

But since there are no controllers, I'm having trouble bootstrapping this in Swashbuckle; after reading through the documentation, I didn't find a way to register these kinds of urls in Swashbuckle. Is there a way to configure Swashbuckle in such way that it can still output the API documentation?

For completeness, a reference architecture application that demonstrates this can be found here.

like image 935
Steven Avatar asked Jun 01 '16 13:06

Steven


1 Answers

It seems Swashbuckle does not support this out of the box, but you can extend it to achieve the desired result, while still reusing most of swagger infrastructure. It might take some time and efforts though, not much in general, but too much for me to provide complete solution in this answer. However I'll try to at least get you started. Note all code below will not be very clean and production ready.

What you need first is create and register custom IApiExplorer. That's an interface used by Swashbuckle to fetch descriptions of your api, and it is responsible for exploring all controllers and actions to gather required information. We will basically extend existing ApiExplorer with the code to explore our message classes and build api description from them. Interface itself is simple:

public interface IApiExplorer
{    
    Collection<ApiDescription> ApiDescriptions { get; }
}

Api description class contains various information about api operation and it is what is used by Swashbuckle to build swagger ui page. It has one problematic property: ActionDescriptor. It represents asp.net mvc action, and we don't have actions, no do we have controllers. You can either use fake implementation of that, or mimic behavior of asp.net HttpActionDescriptor and provide real values. For simplicity we will go with the first route:

class DummyActionDescriptor : HttpActionDescriptor {
    public DummyActionDescriptor(Type messageType, Type returnType) {
        this.ControllerDescriptor = new DummyControllerDescriptor() {
            ControllerName = "Message Handlers"
        };
        this.ActionName = messageType.Name;
        this.ReturnType = returnType;
    }

    public override Collection<HttpParameterDescriptor> GetParameters() {
        // note you might provide properties of your message class and HttpParameterDescriptor here
        return new Collection<HttpParameterDescriptor>();
    }

   public override string ActionName { get; }
   public override Type ReturnType { get; }

    public override Task<object> ExecuteAsync(HttpControllerContext controllerContext, IDictionary<string, object> arguments, CancellationToken cancellationToken) {
        // will never be called by swagger
        throw new NotSupportedException();
    }
}

class DummyControllerDescriptor : HttpControllerDescriptor {
    public override Collection<T> GetCustomAttributes<T>() {
         // note you might provide some asp.net attributes here
        return new Collection<T>();
    }
}

Here we provide only some overrides that swagger will call and will fail if we didn't provide values for them.

Now let's define some attributes to decorate message classes with:

class MessageAttribute : Attribute {
    public string Url { get; }
    public string Description { get; }
    public MessageAttribute(string url, string description = null) {
        Url = url;
        Description = description;
    }
}

class RespondsWithAttribute : Attribute {
    public Type Type { get; }
    public RespondsWithAttribute(Type type) {
        Type = type;
    }
}

And some messages:

abstract class BaseMessage {

}

[Message("/api/commands/CreateOrder", "This command creates new order")]
[RespondsWith(typeof(CreateOrderResponse))]
class CreateOrderCommand : BaseMessage {

}

class CreateOrderResponse {   
    public long OrderID { get; set; }
    public string Description { get; set; }
}

Now custom ApiExplorer:

class MessageHandlerApiExplorer : IApiExplorer {
    private readonly ApiExplorer _proxy;

    public MessageHandlerApiExplorer() {
        _proxy = new ApiExplorer(GlobalConfiguration.Configuration);
        _descriptions = new Lazy<Collection<ApiDescription>>(GetDescriptions, true);
    }

    private readonly Lazy<Collection<ApiDescription>> _descriptions;

    private Collection<ApiDescription> GetDescriptions() {
        var desc = _proxy.ApiDescriptions;
        foreach (var handlerDesc in ReadDescriptionsFromHandlers()) {
            desc.Add(handlerDesc);
        }
        return desc;
    }

    public Collection<ApiDescription> ApiDescriptions => _descriptions.Value;

    private IEnumerable<ApiDescription> ReadDescriptionsFromHandlers() {
        foreach (var msg in Assembly.GetExecutingAssembly().GetTypes().Where(c => typeof(BaseMessage).IsAssignableFrom(c))) {
            var urlAttr = msg.GetCustomAttribute<MessageAttribute>();
            var respondsWith = msg.GetCustomAttribute<RespondsWithAttribute>();
            if (urlAttr != null && respondsWith != null) {
                var desc = new ApiDescription() {
                    HttpMethod = HttpMethod.Get, // grab it from some attribute
                    RelativePath = urlAttr.Url,
                    Documentation = urlAttr.Description,
                    ActionDescriptor = new DummyActionDescriptor(msg, respondsWith.Type)
                };                    

                var response = new ResponseDescription() {
                    DeclaredType = respondsWith.Type,
                    ResponseType = respondsWith.Type,
                    Documentation = "This is some response documentation you grabbed from some other attribute"
                };                    
                desc.GetType().GetProperty(nameof(desc.ResponseDescription)).GetSetMethod(true).Invoke(desc, new object[] {response});
                yield return desc;
            }
        }
    }
}

And finally register IApiExplorer (after you have registered your Swagger stuff) with:

GlobalConfiguration.Configuration.Services.Replace(typeof(IApiExplorer), new MessageHandlerApiExplorer());

After doing all this we can see our custom message command in swagger interface:

enter image description here

like image 62
Evk Avatar answered Oct 08 '22 10:10

Evk