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:
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:
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.
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:
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With