↵
Introduction
I have recently needed to prepare a Web API which had several small operations, and new ones would be added continuously. Instead of adding many controllers with a few actions in them, I decided to use an extensible architecture. So, new operations could be added only by adding new operation types.
Architecture
The architecture and the project are very simple. The project is created using Web API 2 project template of Visual Studio 2017. There is only one API controller named OperationsController
with a single action named Process
. There is a simple IOperation
interface that an operation must implement and a base class named OperationBase
for boilerplate code.
Autofac and Autofac WebAPI2 integration packages are used for automatically discovering operation types and injecting them into the controller.
Code
IOperation
interface is as follows:
public interface IOperation
{
string Name { get; }
string Description { get; }
Type ParameterClassType { get; }
object Execute(object prm);
}
ParameterClassType
is the type of the parameter object that Execute
method needs. It is used when parsing the JSON data in request
body. If Execute
method returns a non-null
value, it is returned as the result to the caller, otherwise just Ok
response is returned.
OperationController
and its Process
action are as follows:
public class OperationsController : ApiController
{
public OperationsController(IEnumerable<IOperation> supportedOperations)
{
_supportedOperations = new List<IOperation>(supportedOperations);
}
[HttpPost]
public async Task<IHttpActionResult> Process(string operationName)
{
IOperation operation = _supportedOperations.FirstOrDefault(x => x.Name == operationName);
if (operation == null)
return BadRequest($"'{operationName}' is not supported.");
string jsonBody = await Request.Content.ReadAsStringAsync();
object operationParams = null;
try
{
if (operation.ParameterClassType != null)
operationParams = Newtonsoft.Json.JsonConvert.DeserializeObject
(jsonBody, operation.ParameterClassType);
object result = operation.Execute(operationParams);
if (result != null)
return Json(result);
}
catch(Exception ex)
{
return InternalServerError(ex);
}
return Ok();
}
}
Routing configuration that only needs operationName
value is as follows:
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{operationName}",
defaults: new { controller = "operations", action = "process", operationName = "help" }
);
This routing configuration routes all the requests to OperationsControlller
's Process
action. If there is no operation name provided, help is executed.
Autofac dependency injection is configured and set as the dependency resolver for the Web API pipeline:
ContainerBuilder builder = new ContainerBuilder();
builder.RegisterApiControllers(Assembly.GetExecutingAssembly());
builder.RegisterAssemblyTypes(Assembly.GetExecutingAssembly())
.Where(t=> t.IsAssignableTo<IOperation>())
.As<IOperation>();
var container = builder.Build();
config.DependencyResolver = new AutofacWebApiDependencyResolver(container);
Testing with Postman
Testing parameterless operation "help".
Response returned from "help" operation.
Testing "sum" operation which expects an object with properties "Number1
" and "Number2
".
Response returned from "sum" operation.
Next Steps
To demonstrate only the idea, the code left is very simple. It is good to separate command processing operations from controller class.
Help command only displays the name and description of the operations but it is good to provide whether a particular operation needs parameters and their types with names. Because operations are not controller actions, Swagger will not help for parameter information.
There is also no automatic validation. Validation can be done before the Execute
method of the operation, or every operation may perform its own validation logic.
Authentication and authorization operations should also be added for securing the Web API.
History
- 3rd July, 2019: Initial version