A walkthrough of the various features of this project.
The
Stocks.Api
project aims to capture the design of each API. The idea here is that the controllers implement the interface, which can be designed in a separate session. This allows the implementation to be completed at a later stage and follows the dependency inversion principle.A typical interface is shown below.
public interface IHistoricalStockNews
{
/// <summary>
/// Returns historical news of the specified market instrument.
/// </summary>
Task<List<StockNewsArticle>> Get(HistoricalStockNewsQuery query);
}
Looking at the signature of the proposed API Get method, there are two contracts specified, one for the request query and the other for the response.
// The request query class
public class HistoricalStockNewsQuery
{
public HistoricalStockNewsQuery()
{
Range = HistoricalStockNewsRange.PastWeek;
Count = 10;
}
/// <summary>
/// A market instrument's ticker symbol.
/// </summary>
[BindRequired]
public string TickerSymbol { get; set; }
/// <summary>
/// The period of time for historical news to be returned.
/// The default is "PastWeek".
/// </summary>
public HistoricalStockNewsRange Range { get; set; }
/// <summary>
/// The maximum number of results returned.
/// Defaults to 10.
/// </summary>
public int Count { get; set; }
}
// The response class
public class StockNewsArticle
{
/// <summary>
/// The headline of the article.
/// </summary>
public string HeadLine { get; set; }
/// <summary>
/// The news source which released the article.
/// </summary>
public string Source { get; set; }
/// <summary>
/// The access url of the article.
/// </summary>
public string Url { get; set; }
/// <summary>
/// The timestamp (UTC) of the article's publication.
/// </summary>
public DateTime TimeStamp { get; set; }
/// <summary>
/// A list of ticker symbols which are considered to be related to the article.
/// </summary>
public List<string> RelatedTickerSymbols { get; set; }
}
The query class outlines the various parameters which can be used to retrieve historical news for a given stock. Validation attributes can also be specified here, outlining whether a parameter is required, for example. Defaults can also be specified through a parameterless constructor. This is the kind of information which can arise from discussions with the API consumers or business analysts.
Similarly, the response class outlines what data will be returned from the API. This data will be consumed by the client and used to build the UI. The data returned and its type can be discussed with the API consumers or business analysts and the design contracts finalized based on these discussions.
Ultimately, when implementing these API designs, the contracts for both the request and response are outlined. This allows the developer to decide what underlying APIs to aggregate in order to retrieve response data using the request contract.
Furthermore, the API is designed first and not the implementation. This follows the dependency inversion principle, meaning the API does not rely on the details of the implementation, rather its design is an abstraction decided upon with business analysts.
As mentioned above, the API's interface represents an abstraction of the desired functionality. Now, the implementation and it's details can be built within a controller that implements the interface. The controllers are found in
Stocks.Controllers
.
[ApiController]
[Route(BaseUri.GatewayPrefix + "/news/historical")]
public class HistoricalStockNewsController : ControllerBase, IHistoricalStockNews
{
private readonly IIEXClient _client;
/// <summary>
/// </summary>
/// <param name="client"></param>
public HistoricalStockNewsController(IIEXClient client)
{
_client = client;
}
/// <inheritdoc/>
public async Task<List<StockNewsArticle>> Get([FromQuery] HistoricalStockNewsQuery query)
{
var news = await _client.Api.News.HistoricalNewsAsync(query.TickerSymbol, (TimeSeriesRange?)query.Range.ToTimeSeriesRange(), query.Count);
return news.ToStockNewsArticles();
}
}
The details of the implementation such as the API's route and how it retrieves the data are built here. The controller has a single dependency
IIEXClient
which allows it to aggregate (if necessary, here only a single underlying call is made) underlying API calls to the IEXCloud, which is a third-party service used to retrieve live market information.Furthermore, any additional logic or data mapping can be performed here before returning the response.
Note that if the method of retrieving the market data was to change, say because using the IEXCloud service became too expensive, a new set of controllers could be built, implementing the same set of interfaces from
Stocks.Api
. Other components of the project would not have to change, for example the API documentation, since they rely on the API interfaces and not the controllers and their details.
The market data used by the application comes from
IEXCloud
. This is achieved by installing theIEXSharp
package. The code related to utilizing theIEXSharp
package is contained in theStocks.IEXCloud
project.The idea behind this project is straightforward, setup a client interface, implement it and register the interface implementation pair as a service for dependency injection, to be injected into the controllers.
public interface IIEXClient
{
IEXCloudClient Api { get; }
}
internal class IEXClient : IIEXClient
{
private readonly IEXCloudClient _client;
public IEXClient()
{
string sk = Environment.GetEnvironmentVariable("IEX_SK");
string pk = Environment.GetEnvironmentVariable("IEX_PK");
_client = new IEXCloudClient(pk, sk, signRequest: false, useSandBox: false);
}
public IEXCloudClient Api => _client;
}
The interface is publicly accessible to allow the controllers to reference it for dependency injection. Conversely, the details of the implementation are kept internal; the consumers of this project don't need to know how the client is setup.
In order to register the client as a service, a public extension method on
IServiceCollection
is available, to be used during the application bootstrapping phase.
public static class BootstrapExtensions
{
/// <summary>
/// Register the IEXClient component.
/// </summary>
/// <param name="services"></param>
/// <returns></returns>
public static IServiceCollection RegisterIEXClient(this IServiceCollection services)
{
services.AddSingleton<IIEXClient, IEXClient>();
return services;
}
}
When creating a class library like
Stocks.IEXCloud
, it is important to limit what APIs are made public to consumers. Otherwise, upon installing the library as a dependency, consumers will have access to APIs which they do not require, bloating the interface of the library. Furthermore, unintentional access to excess APIs may cause consumers to misuse the library, which could cause a general misunderstanding of how to consume the library and potentially cause breaking changes in future versions.
The following section outlines how the NSwag middleware is consumed and configured in order to provide detailed API documentation.
A separate class library project is used to install and configure NSwag, named
Stocks.NSwag
. The idea behind this is that other API projects will also require NSwag for API documentation, so setting up a common library to configure it will save time.The library aims to provide public extension methods for configuration of NSwag during application bootstrapping. This comes in to form of
CompositeApi.cs
andApi.cs
.CompositeApi
provides extension methods forIServiceCollection
andIApplicationBuilder
(NSwag is by default configured through these two ASP.NET startup interfaces). The idea here is to provide default plug & play configuration, grouping any customizations of API documentation within the library together under two straightforward APIs.The composite APIs are found here.
Note that the composite APIs rely on some options specified in appsettings.json. This is outlined in the API xml description.
Furthermore, note that the
AddNSwag
extension method ofIServiceCollection
required a few dependencies, so they are grouped under a single classAddNSwagContext
. Instead of requesting the consumer to pass in their own instance of the class, the parameter of the API is instead anAction
delegate with a single parameterAddNSwagContext
. This allows for creation of the class to be moved inside the API and means the consumer only has to allocate the properties of the class.
Alternatively, if only some of the customization found in
Stocks.NSwag
is desired,Api.cs
allows for specific customizations to be added during bootstrapping. Note: the composite APIs use these APIs under the hood. Each API here is built depending on what area of the API documentation is configured.For example,
EnableTagGroups
specifically enables grouping of APIs by their namespace, organizing the API documentation into sections.
/// <summary>
/// Enable grouping of APIs by their namespace.
/// The APIs will then be grouped into sections in the API documentation.
/// </summary>
/// <param name="settings"></param>
/// <param name="controllerAssembly"></param>
/// <returns></returns>
public static AspNetCoreOpenApiDocumentGeneratorSettings EnableTagGroups(this AspNetCoreOpenApiDocumentGeneratorSettings settings, Assembly controllerAssembly)
{
settings.OperationProcessors.Add(new TagProcessor());
settings.DocumentProcessors.Add(new TagGroupProcessor(controllerAssembly));
return settings;
}
The method above is an example of a fluent API; it is an extension method of the NSwag class
AspNetCoreOpenApiDocumentGeneratorSettings
which returns an instance ofAspNetCoreOpenApiDocumentGeneratorSettings
, allowing multiple configurations to be chained together. This is a common pattern used in configuration APIs.The more granular APIs are found here.
The OpenAPI specification document can be customized through processors. There are two types of processor used in this project so far. The first is
IDocumentProcessor
which deals with customization of the document after it has been generated. This is used to supply additional information about the document, such as tag groups. See TagGroupProcessor.cs.The second type of processor is
IOperationProcessor
. This processor is called during document generation, on each API operation (i.e. on each API endpoint passed to the NSwag middleware). A simple example of an operation processor is TagProcessor.cs. This processor specifies a tag on each API endpoint, taken from the respective controller's namespace.
This section of the NSwag customization deals with allowing developers to specify response samples for the API endpoints. This task would go hand-in-hand with API implementation, in order to provider more information to the API consumers on how a typical response from the API will look. There are three distinct sections to this framework:
- Response sample registration.
- Adding response sample providers.
- Response sample processing.
The first part of the framework deals with registration of response samples. This is exposed through the public extension method
AddResponseSamples
on the NSwag classAspNetCoreOpenApiDocumentGeneratorSettings
, during the bootstrapping phase.
/// <summary>
/// Allows registration of response samples through response sample providers located in the specified "documentationAssembly".
/// </summary>
/// <param name="settings"></param>
/// <param name="documentationAssembly"></param>
/// <returns></returns>
public static AspNetCoreOpenApiDocumentGeneratorSettings AddResponseSamples(this AspNetCoreOpenApiDocumentGeneratorSettings settings, Assembly documentationAssembly)
{
var apiProvider = new ApiProvider();
var register = new ResponseSampleRegister();
var providers = documentationAssembly.GetExportedTypes().Where(t => typeof(IResponseSampleProvider).IsAssignableFrom(t));
providers.ForEach(provider =>
{
var providerInstance = (IResponseSampleProvider) Activator.CreateInstance(provider);
providerInstance.Register(apiProvider, register);
});
settings.OperationProcessors.Add(new ResponseSampleProcessor(register));
return settings;
}
Instances of two classes are first created in this method;
ApiProvider
andResponseSampleRegister
.ApiProvider
is a generic class which allows endpoints to be selected through reflection by specifying their controller, return type and controller method.
internal class ApiProvider : IApiProvider
{
public MethodInfo SelectEndpoint<TApiController, TContract>(Expression<Func<TApiController, Task<TContract>>> selector)
{
var methodExpression = (MethodCallExpression)selector.Body;
var method = methodExpression.Method;
return method;
}
}
A
Func
delegate is used to select the endpoint method, with the controller type as a parameter and the response type as a return type. This is wrapped in anExpression
in order to operate on the method's information itself and return it's respectiveMethodInfo
, rather then actually executing the method itself.The second object created is
ResponseSampleRegister
.
internal class ResponseSampleRegister : IResponseSampleRegister
{
private readonly Dictionary<MethodInfo, List<ResponseSample>> _samples = new Dictionary<MethodInfo, List<ResponseSample>>();
public List<ResponseSample> GetResponseSamples(MethodInfo method)
{
return _samples.ContainsKey(method) ? _samples[method] : new List<ResponseSample>();
}
public void AddResponseSample<TContract>(MethodInfo method, TContract response, string name)
{
if (!_samples.ContainsKey(method))
{
_samples.Add(method, new List<ResponseSample>());
}
_samples[method].Add(new ResponseSample { Name = name, Body = JToken.FromObject(response) });
}
}
This class stores all response samples for use by the processor later on. It has a single field which is a dictionary used to store the samples, with the key being the
MethodInfo
of the respective API endpoint for which the sample(s) is providedList<ResponseSample>
.It has two public methods, one to retrieve samples from the register and the other to add them to the register.
Returning to the
Api.cs/AddResponseSamples
method, response sample providers are located through reflection by filtering the public types of the documentation assembly, in this caseStocks.Documentation
. This is achieved by checking if the type implements theIResponseSampleProvider
interface.An instance of each provider is then created and the interface method
Register
is called on each.Finally, the
ResponseSampleProcessor
processor is added to the NSwagConfiguration, passing in the instance ofResponseSampleRegister
, which now contains all the registered response samples.
As mentioned above, response sample providers can be created inside a documentation assembly. These can be used to register response samples for the application's API endpoints. The
IApiProvider
parameter is used to select the endpoint through reflection, while theIResponseSampleRegister
parameter is used to add the response sample(s). These interfaces follow the interface segregation principle, in that they only expose the exact methods required by the developer creating the provider.A response sample provider can be found here.
Finally, the response samples can be processed and added to the document. This is achieved in ResponseSampleProcessor.cs. The method info of the current API operation (endpoint) can be found in the
OperationProcessorContext
parameter. This can be used to search theResponseSampleRegister
passed in during the construction of the processor. If any response samples are registered for the given endpoint, they are added to the document.