Jump to: navigation, search

Difference between revisions of "OpenStack-SDK-DotNet/DesignDetails"

(Created page with "__TOC__ == Overview == At a high level the design of the API is separated into four main layers (Client, POCO/Model, REST, HTTP). File:APILayeredDesign.png Each layer...")
 
Line 1: Line 1:
 
__TOC__
 
__TOC__
 
  
 
== Overview ==
 
== Overview ==
Line 10: Line 9:
  
 
== HTTP layer ==
 
== HTTP layer ==
The HTTP layer is the lowest level in the API. It provides a simple abstraction for working with the HTTP protocol, and making and receiving requests and responses. While it may be considered redundant, since the .NET framwork already offers several ways to do this, HttpClient for example. There are two reason why this layer is important:
+
The HTTP layer is the lowest level in the API. It provides a simple abstraction for working with the HTTP protocol, and making and receiving requests and responses. While it may be considered redundant, since the .NET framework already offers several ways to do this, HttpClient for example. There are two reason why this layer is important:
:#Unit tests can inject HTTP responses (including error states, and timeouts) without needing a live server.
+
:*Unit tests can inject HTTP responses (including error states, and timeouts) without needing a live server.
 
::By using interfaces for the abstraction, and dependency injection, unit tests can be written that inject specific HTTP responses into code that takes a dependency on this layer. This eliminates the need for an individual developer to have a live server to test against. Since remote calls are not being made, latency is greatly reduced, tests execute quickly, and the developer can continue doing what he/she does best, and not waste time on watching tests run. This does not eliminate the need for integration tests that are intended to be run against a live server. Those tests should, and will still be written, but do not need to be run as a part of a gated check-in, and should be run by the CI system on a periodic basis.  
 
::By using interfaces for the abstraction, and dependency injection, unit tests can be written that inject specific HTTP responses into code that takes a dependency on this layer. This eliminates the need for an individual developer to have a live server to test against. Since remote calls are not being made, latency is greatly reduced, tests execute quickly, and the developer can continue doing what he/she does best, and not waste time on watching tests run. This does not eliminate the need for integration tests that are intended to be run against a live server. Those tests should, and will still be written, but do not need to be run as a part of a gated check-in, and should be run by the CI system on a periodic basis.  
:#Tight coupling and dependencies are removed.
+
:*Tight coupling and dependencies are removed.
::The .NET framework is always evolving. There are several ways (WebRequest, WebClient, HttpClient, etc) with the .NET framework alone to make a Http requests, not to mention third-party libraries. The Http layer is _not_ intended to be a replacement for these APIs. It is intended to represent a contract for the API to abstract itself from the implementation. With this layer in place, a developer that takes this layer as a dependency does not need to know or understand how the request is being carried out, if the current framework version supports the functionality required, etc. By using the Http layer, if/when the underlying implementation needs to change, none of the code that has taken a dependency will need to change (unless there is a breaking change in the Http specification, which is highly unlikely). This gives the API tremendous flexibility and allows the API to easily adapt to cross platform and framework scenarios.  
+
::The .NET framework is always evolving. There are several ways (WebRequest, WebClient, HttpClient, etc) with the .NET framework alone to make a HTTP requests, not to mention third-party libraries. The HTTP layer is _not_ intended to be a replacement for these APIs. It is intended to represent a contract for the API to abstract itself from the implementation. With this layer in place, a developer that takes this layer as a dependency does not need to know or understand how the request is being carried out, if the current framework version supports the functionality required, etc. By using the HTTP layer, if/when the underlying implementation needs to change, none of the code that has taken a dependency will need to change (unless there is a breaking change in the HTTP specification, which is highly unlikely). This gives the API tremendous flexibility and allows the API to easily adapt to cross platform and framework scenarios.  
  
 
== REST layer ==
 
== REST layer ==
The REST layers primary responsibility is to interact with the REST endpoints of a remote service. Each service has its own REST client in this layer, that represents an interface consistent with the remote interfaces. The intention of this layer is to provide a client side representation of the remote services API surface area, and to abstract away the details of how the REST calls are formatted and made. The REST layer is responsible for taking in the required information needed by the remote service, and translating it into a request that the remote service can understand.
+
The REST layers primary responsibility is to interact with the REST endpoints of a remote service. Each remote service should be represented by it's own client in this layer. The intention of this layer is to provide a client side representation of a remote services API surface area, and to also abstract away the details of how the REST calls are formatted and made. A REST client takes in information from the code above it, adds and formats it as needed, and sends the request off to the server using the HTTP layer. By consolidating the code that interacts with a remote REST endpoint, updates, changes, or new version of the remote API can easily be accommodated. This layer, like the HTTP, layer can be used to test any code that takes a dependency on it in isolation, greatly reducing the complexity and length of the test code.
 +
 
 +
== POCO/Model layer ==
 +
The POCO/Model layer is responsible for transforming user facing models, into and out of a format that that REST layer understands. This isolates the code that is being used to parse, serialize, or deserialize the response from the REST layer into a strongly typed C# object that makes logical sense to the end user. Changes, updates, new versions, or extensions to remote service can easily be accommodated and contained in this single layer. Like the REST and HTTP layers, this layer can be used to test code that depends on it in isolation, removing the need to depend on the this layer directly or any of the layers below it.
 +
 
 +
== Client layer ==
 +
The top most layer in the API is the Client layer. This layer is exposed to the end user directly and is responsible for establishing an end-user-centric surface area. This surface area can, and most of the time should, be different then the remote services. The client layer _should_ be simple and lightweight, and is _not_ a place to add high level orchestration or processing. The client layer's main goal is to provide a friendly, intuitive, and native felling interface for the end user. The client takes information from the end user, passes it to the POCO layer and gives the responses back to the end user in the form of POCO objects.
 +
 
 +
== Putting it all together ==
 +
In this example there is a _very_ simple service that we want to add to the API. The service has one endpoint, that simply echos text back to the caller, and adds a time-stamp to it. For example, making a PUT request to "http://sample.service.org/v1/echo?m=Text" would return the following JSON "{ 'msg':'Text', 'Timestamp':'2014-05-01 7:34:42' }". A contributor to the API would first create a new REST client and a factory that can be used to create it for the API.
 +
  public interface IEchoRestClient
 +
  {
 +
    Task<IHttpResponseAbstraction> Echo(string message);
 +
  }
 +
  public interface IEchoRestClientFactory
 +
  {
 +
    IEchoRestClient Create(IServiceLocator serviceLocator);
 +
  }
 +
This interface matches the services endpoint surface area, and will depend on and surface the response from the HTTP layer. An implementation of the REST client and it's factory might look like:
 +
  internal class EchoRestClient : IEchoRestClient
 +
  {
 +
    internal const string serviceEndpoint = "http://sample.service.org/v1/echo";
 +
    internal IServiceLocator ServiceLocator;
 +
   
 +
    public EchoRestClient(IServiceLocator serviceLocator)
 +
    {
 +
      this.ServiceLocator = serviceLocator;
 +
    }
 +
 
 +
    public async Task<IHttpResponseAbstraction> Echo(string message)
 +
    {
 +
      var client = this.ServiceLocator.Locate<IHttpAbstractionClientFactory>().Create(CancellationToken.None);
 +
      client.Method = HttpMethod.Put;
 +
      client.Uri = new Uri(serviceEndpoint +"?m=" +message);
 +
      return await client..SendAsync();
 +
    }
 +
  }
 +
  internal class EchoRestClientFactory : IEchoRestClientFactory
 +
  {
 +
    public IEchoRestClient Create(IServiceLocator serviceLocator)
 +
    {
 +
      return new EchoRestClient(serviceLocator);
 +
    }
 +
  }
 +
First the class defines the service endpoint (in this example it's hard coded). Secondly the object has a field for a service locator. This allows the REST client to locate the HTTP abstraction, and to locate any other dependencies it might need. Next, the client defines a constructor that accepts a service locator. Then, the client implements the interfaces Echo method. In this method the object locates and creates an instance of the HttpAbstractionClient, then populates it with the desired HTTP method, and the  target Uri. Lastly the method calls the clients SendAsync method, and awaits the response. Since a raw JSON response from the service is not suitable to be consumed by the layers high up, a simple POCO/Model class can be defined to represent the remote services response.
 +
  public class EchoResponse
 +
  {
 +
    public string Message { get; private set; }
 +
    public DateTime Timestamp { get; private set; }
 +
 
 +
    public EchoResponse(string message, DateTime timestamp)
 +
    {
 +
      this.Message = message;
 +
      this.Timestamp = timestamp;
 +
    }
 +
  }
 +
The POCO client can then be created that surfaces the new POCO object the the client layer, and interact with the REST layer.
 +
  public interface IEchoPocoClient
 +
  {
 +
    Task<EchoResponse> Echo(string message);
 +
  }
 +
  internal class EchoPocoClient : IEchoPocoClient
 +
  {
 +
    internal IServiceLocator ServiceLocator;
 +
   
 +
    public EchoPocoClient(IServiceLocator serviceLocator)
 +
    {
 +
      this.ServiceLocator = serviceLocator;
 +
    }
 +
    public async Task<EchoResponse> Echo(string message)
 +
    {
 +
      var restClient = this.ServiceLocator.Locate<IEchoRestClientFactory>().Create(this.ServiceLocator);
 +
      var resp = await restClient.Echo(message);
 +
      var payload = resp.ReadContentAsStringAsync();
 +
      var obj = JObject.Parse(payload);
 +
      return new EchoResponse(obj["msg"],obj["timestamp"]);
 +
    }
 +
  }

Revision as of 21:50, 30 April 2014

Overview

At a high level the design of the API is separated into four main layers (Client, POCO/Model, REST, HTTP).

APILayeredDesign.png

Each layer is described in more detail below. By separating the API into these four levels, the API is resilient to underlying dependency changes, can be easily unit tested, and easily extended. While the end-user/consumer of the API will not see these design details, it's important for any one who would like to contribute to the project, or extend the API to be familiar with the design. Below you will find details of each layer, and how they interact.

HTTP layer

The HTTP layer is the lowest level in the API. It provides a simple abstraction for working with the HTTP protocol, and making and receiving requests and responses. While it may be considered redundant, since the .NET framework already offers several ways to do this, HttpClient for example. There are two reason why this layer is important:

  • Unit tests can inject HTTP responses (including error states, and timeouts) without needing a live server.
By using interfaces for the abstraction, and dependency injection, unit tests can be written that inject specific HTTP responses into code that takes a dependency on this layer. This eliminates the need for an individual developer to have a live server to test against. Since remote calls are not being made, latency is greatly reduced, tests execute quickly, and the developer can continue doing what he/she does best, and not waste time on watching tests run. This does not eliminate the need for integration tests that are intended to be run against a live server. Those tests should, and will still be written, but do not need to be run as a part of a gated check-in, and should be run by the CI system on a periodic basis.
  • Tight coupling and dependencies are removed.
The .NET framework is always evolving. There are several ways (WebRequest, WebClient, HttpClient, etc) with the .NET framework alone to make a HTTP requests, not to mention third-party libraries. The HTTP layer is _not_ intended to be a replacement for these APIs. It is intended to represent a contract for the API to abstract itself from the implementation. With this layer in place, a developer that takes this layer as a dependency does not need to know or understand how the request is being carried out, if the current framework version supports the functionality required, etc. By using the HTTP layer, if/when the underlying implementation needs to change, none of the code that has taken a dependency will need to change (unless there is a breaking change in the HTTP specification, which is highly unlikely). This gives the API tremendous flexibility and allows the API to easily adapt to cross platform and framework scenarios.

REST layer

The REST layers primary responsibility is to interact with the REST endpoints of a remote service. Each remote service should be represented by it's own client in this layer. The intention of this layer is to provide a client side representation of a remote services API surface area, and to also abstract away the details of how the REST calls are formatted and made. A REST client takes in information from the code above it, adds and formats it as needed, and sends the request off to the server using the HTTP layer. By consolidating the code that interacts with a remote REST endpoint, updates, changes, or new version of the remote API can easily be accommodated. This layer, like the HTTP, layer can be used to test any code that takes a dependency on it in isolation, greatly reducing the complexity and length of the test code.

POCO/Model layer

The POCO/Model layer is responsible for transforming user facing models, into and out of a format that that REST layer understands. This isolates the code that is being used to parse, serialize, or deserialize the response from the REST layer into a strongly typed C# object that makes logical sense to the end user. Changes, updates, new versions, or extensions to remote service can easily be accommodated and contained in this single layer. Like the REST and HTTP layers, this layer can be used to test code that depends on it in isolation, removing the need to depend on the this layer directly or any of the layers below it.

Client layer

The top most layer in the API is the Client layer. This layer is exposed to the end user directly and is responsible for establishing an end-user-centric surface area. This surface area can, and most of the time should, be different then the remote services. The client layer _should_ be simple and lightweight, and is _not_ a place to add high level orchestration or processing. The client layer's main goal is to provide a friendly, intuitive, and native felling interface for the end user. The client takes information from the end user, passes it to the POCO layer and gives the responses back to the end user in the form of POCO objects.

Putting it all together

In this example there is a _very_ simple service that we want to add to the API. The service has one endpoint, that simply echos text back to the caller, and adds a time-stamp to it. For example, making a PUT request to "http://sample.service.org/v1/echo?m=Text" would return the following JSON "{ 'msg':'Text', 'Timestamp':'2014-05-01 7:34:42' }". A contributor to the API would first create a new REST client and a factory that can be used to create it for the API.

 public interface IEchoRestClient
 {
   Task<IHttpResponseAbstraction> Echo(string message);
 }
 public interface IEchoRestClientFactory
 {
   IEchoRestClient Create(IServiceLocator serviceLocator);
 }

This interface matches the services endpoint surface area, and will depend on and surface the response from the HTTP layer. An implementation of the REST client and it's factory might look like:

 internal class EchoRestClient : IEchoRestClient
 {
   internal const string serviceEndpoint = "http://sample.service.org/v1/echo";
   internal IServiceLocator ServiceLocator;
   
   public EchoRestClient(IServiceLocator serviceLocator)
   {
     this.ServiceLocator = serviceLocator;
   }
   public async Task<IHttpResponseAbstraction> Echo(string message)
   {
     var client = this.ServiceLocator.Locate<IHttpAbstractionClientFactory>().Create(CancellationToken.None);
     client.Method = HttpMethod.Put;
     client.Uri = new Uri(serviceEndpoint +"?m=" +message);
     return await client..SendAsync();
   }
 }
 internal class EchoRestClientFactory : IEchoRestClientFactory
 {
   public IEchoRestClient Create(IServiceLocator serviceLocator)
   {
     return new EchoRestClient(serviceLocator);
   }
 }

First the class defines the service endpoint (in this example it's hard coded). Secondly the object has a field for a service locator. This allows the REST client to locate the HTTP abstraction, and to locate any other dependencies it might need. Next, the client defines a constructor that accepts a service locator. Then, the client implements the interfaces Echo method. In this method the object locates and creates an instance of the HttpAbstractionClient, then populates it with the desired HTTP method, and the target Uri. Lastly the method calls the clients SendAsync method, and awaits the response. Since a raw JSON response from the service is not suitable to be consumed by the layers high up, a simple POCO/Model class can be defined to represent the remote services response.

 public class EchoResponse
 {
   public string Message { get; private set; }
   public DateTime Timestamp { get; private set; }
   public EchoResponse(string message, DateTime timestamp)
   {
     this.Message = message;
     this.Timestamp = timestamp;
   }
 }

The POCO client can then be created that surfaces the new POCO object the the client layer, and interact with the REST layer.

 public interface IEchoPocoClient
 {
   Task<EchoResponse> Echo(string message);
 }
 internal class EchoPocoClient : IEchoPocoClient
 {
   internal IServiceLocator ServiceLocator;
   
   public EchoPocoClient(IServiceLocator serviceLocator)
   {
     this.ServiceLocator = serviceLocator;
   }
   public async Task<EchoResponse> Echo(string message)
   {
     var restClient = this.ServiceLocator.Locate<IEchoRestClientFactory>().Create(this.ServiceLocator);
     var resp = await restClient.Echo(message);
     var payload = resp.ReadContentAsStringAsync();
     var obj = JObject.Parse(payload);
     return new EchoResponse(obj["msg"],obj["timestamp"]);
   }
 }