As the world are moving to REST services (and luckily we have a proper way to describe the services we create with swagger), we still write a lot of boilerplate code and implementing clients for services that others (in the team, our organization or 3rd party vendors). Unit testing suffers as the service implementations are strongly tied to the Http context and request response scope.
With this as a backdrop I tried to create a tool that can wrap an interface without any ties to the http stuff we usually have in webapi, and still retain the flexibillity of it. I guess I got a bit carried away.
So to the solution:
A small package that generates a client over the contract and enables service generation over the same interface.
Code you say? Lets roll..
The interface:
[IRoutePrefix("api")]
public interface ITestApi
{
[Route("test/{id}")]
[HttpGet]
string Apply1([In(InclutionTypes.Path)] string id, [In(InclutionTypes.Path)]string name);
[Route("test2/{id}")]
[HttpGet]
string Apply2([In(InclutionTypes.Path)] string id, [In(InclutionTypes.Path)]string name, [In(InclutionTypes.Header)]string item3);
[Route("test3/{id}")]
[HttpGet]
string Apply3([In(InclutionTypes.Path)] string id, [In(InclutionTypes.Path)]string name, [In(InclutionTypes.Header)]string item3, [In(InclutionTypes.Header)]string item4);
[Route("put1/{id}")]
[HttpPut]
void Put([In(InclutionTypes.Path)] string id, [In(InclutionTypes.Body)] DateTime timestamp);
[Route("test5/{id}")]
[HttpGet]
Task
[Route("put2/{id}")]
[HttpPut]
Task PutAsync([In(InclutionTypes.Path)] string id, [In(InclutionTypes.Body)] DateTime timestamp);
}
As you can see, the interface is decorated with some of the known and loved attributes from WebApi,
like Route, HttpGet and HttpPost, and some new like the InAttribute. This workes like the FromUri and FromBody with the addition of Header.
You can still use the FromBody and FromUri attributes, the tool converts them to In internally.
And note the IRoutePrefixAttribute vs RoutePrefixAttribute in webapi. RoutePrefixAttribute is limitet to classes only, so to make this work i had to create a new attribute that maps to RoutePrefixAttribute.
To make a service you have to apply a DI container and register the implementation of the interface.
After that you only need 2 lines of code to wrap the interface behind a WebApi controller.
The service:
public class WebApiApplication : System.Web.HttpApplication
{
protected void Application_Start()
{
ServiceFactory.CreateServiceImplementationForAllInCotainingAssembly();
ServiceFactory.FinalizeRegistration();
this.LoadBindingConfiguration();
AreaRegistration.RegisterAllAreas();
GlobalConfiguration.Configure(WebApiConfig.Register);
FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
RouteConfig.RegisterRoutes(RouteTable.Routes);
BundleConfig.RegisterBundles(BundleTable.Bundles);
}
}
The generated service:
[RoutePrefix("api")]
public class TestApiController : ServiceWrapperBase
{
// Methods
public TestApiController(ITestApi implementation) : base(implementation)
{
}
[Route("test1/{id}"), HttpGet]
public HttpResponseMessage Apply1([FromUri] string id, [FromUri] string name)
{
try
{
object[] fromWebMethodParameters = new object[] { id, name };
ParameterWrapper[] wrapperArray = base.GatherParameters("Apply1", fromWebMethodParameters);
string message = base.implementation.Apply1((string) wrapperArray[0].value, (string) wrapperArray[1].value);
return base.CreateResponse(HttpStatusCode.OK, message);
}
catch (Exception exception)
{
return base.CreateErrorResponse(exception);
}
}
[HttpGet, Route("test2/{id}")]
public HttpResponseMessage Apply2([FromUri] string id, [FromUri] string name, string item3)
{
try
{
object[] fromWebMethodParameters = new object[] { id, name, item3 };
ParameterWrapper[] wrapperArray = base.GatherParameters("Apply2", fromWebMethodParameters);
string message = base.implementation.Apply2((string) wrapperArray[0].value, (string) wrapperArray[1].value, (string) wrapperArray[2].value);
return base.CreateResponse(HttpStatusCode.OK, message);
}
catch (Exception exception)
{
return base.CreateErrorResponse(exception);
}
}
[Route("test/{id}"), HttpGet]
public HttpResponseMessage Apply3([FromUri] string id, [FromUri] string name, string item3, string item4)
{
try
{
object[] fromWebMethodParameters = new object[] { id, name, item3, item4 };
ParameterWrapper[] wrapperArray = base.GatherParameters("Apply3", fromWebMethodParameters);
string message = base.implementation.Apply3((string) wrapperArray[0].value, (string) wrapperArray[1].value, (string) wrapperArray[2].value, (string) wrapperArray[3].value);
return base.CreateResponse(HttpStatusCode.OK, message);
}
catch (Exception exception)
{
return base.CreateErrorResponse(exception);
}
}
[HttpGet, Route("test/{id}")]
public Task ApplyAsync([FromUri] string id, [FromUri] string name, [FromUri] string item3, [FromUri] string item4)
{
try
{
object[] fromWebMethodParameters = new object[] { id, name, item3, item4 };
ParameterWrapper[] wrapperArray = base.GatherParameters("ApplyAsync", fromWebMethodParameters);
Task messageTask = base.implementation.ApplyAsync((string) wrapperArray[0].value, (string) wrapperArray[1].value, (string) wrapperArray[2].value, (string) wrapperArray[3].value);
return base.CreateResponseAsync(HttpStatusCode.OK, messageTask);
}
catch (Exception exception)
{
return Task.FromResult(base.CreateErrorResponse(exception));
}
}
[HttpPut, Route("test/{id}")]
public HttpResponseMessage Put([FromUri] string id, [FromBody] DateTime timestamp)
{
try
{
object[] fromWebMethodParameters = new object[] { id, timestamp };
ParameterWrapper[] wrapperArray = base.GatherParameters("Put", fromWebMethodParameters);
base.implementation.Put((string) wrapperArray[0].value, (DateTime) wrapperArray[1].value);
return base.CreateResponse
The implementation bases it's http status codes on the exception type the implementation creates (a really limited amount of mappings at the moment)
The client:
[Fact]
public async Task GeneratorTest()
{
var service = ProxyFactory.CreateInstance("http://localhost/Stardust.Interstellar.Test/");
try
{
var res =await service.ApplyAsync("test", "Jonas Syrstad", "Hello", "Sample");
output.WriteLine(res);
}
catch (Exception ex)
{
throw;
}
try
{
await service.PutAsync("test",DateTime.Today);
output.WriteLine("Put was successfull");
}
catch (Exception ex)
{
throw;
}
}
public async Task GeneratorTest()
{
var service = ProxyFactory.CreateInstance
try
{
var res =await service.ApplyAsync("test", "Jonas Syrstad", "Hello", "Sample");
output.WriteLine(res);
}
catch (Exception ex)
{
throw;
}
try
{
await service.PutAsync("test",DateTime.Today);
output.WriteLine("Put was successfull");
}
catch (Exception ex)
{
throw;
}
}
To create and instantiate the rest client you only need one line of code (in the simplest scenarios) and you are good to go accessing the service. Wrapping the client in a container it will work nicely with any DI container
and you can easily mock the remote service when unit testing the client application.
This tool is available on nuget and feedback is more than welcome.
I do have some ideas on which features to add next, but feedback can change the priorities.
Nuget:
Install-Package Stardust.Interstellar.Rest
Cheers
/J