AWS Developer Tools Blog
Dive into the AWS SDK for .NET’s Runtime Pipeline and Client Configuration
We often hear from customers that optimizing the AWS SDK for .NET’s behavior when making API calls is an important feature. To make requests to Amazon Web Services, the SDK provides service clients for each AWS service. Configuration settings are used to enable features such as maximum length to wait for a request to complete, or specify API request retry behavior. These settings can improve your application’s performance and make your application more resilient to transient errors.
In this blog post, we will take a deep dive into three ways to configure the AWS service clients that are part of the AWS SDK for .NET. We will start by looking at the ClientConfig class, move onto subclassing AWS service clients, and end with how you can inject custom behavior into the AWS SDK for .NET’s runtime pipeline.
Runtime pipeline overview
The AWS SDK for .NET implements a consistent pattern for making API calls across all AWS services, as shown below.
using Amazon.servicename;
using Amazon.servicename.Model;
var client = new AmazonservicenameClient();
operationnameResponse response = await client.operationname(new operationnameRequest
{
RequestProperty1 = "some data"
});
The colored text represents placeholders that will vary per service. For example, for Amazon S3, “servicename” would be replaced with “S3”, making the service’s root namespace “Amazon.S3”. Each service operation (i.e., API call) has classes which model the request and response payloads. These classes reside in the “Amazon.servicename.Model” namespace – “Amazon.S3.Model” in this case. Service client classes also have a very consistent naming convention. Following our example, the service client class for S3 is AmazonS3Client. Looking at the S3 service’s operation of GetObject, the S3 client’s method signature is:
Amazon.S3.Model.GetObjectResponse GetObject(Amazon.S3.Model.GetObjectRequest request)
As service operations execute, each request flows through a pipeline, known as the runtime pipeline, where it is processed by components known as pipeline handlers. The response received from the AWS service is also processed by the pipeline handlers, only in reverse order.
Each of these pipeline handlers provide specific behaviors. Some of the key pipeline handlers used by the AWS SDK for .NET are:
- Metrics Handler – Logs API call execution times.
- Retry Handler – Implements support for retry operations with support from Retry Policies.
- Error Handler – Processes exceptions encountered as part of processing the request.
- Http Handler – Contains logic for issuing an HTTP request that is independent of the underlying HTTP infrastructure.
While all of the types mentioned in this blog post are currently public, those related to the runtime pipeline are in the Amazon.Runtime.Internal namespace and so are subject to change or removal. As of now, the runtime pipeline is the only means of modifying the behavior of AWS service requests in a centralized way.
Two options exist for customizing the runtime pipeline – setting configuration options for the pipeline handlers used by the SDK, or changing the pipeline handler list – either by modifying an existing pipeline handler or adding your own custom pipeline handler. Each of these can be achieved as noted in the below table.
In the rest of this blog post, we will walkthrough how to use each of these approaches.
Modifying Pipeline Handler Behavior
If the only settings you need to change are available in the ClientConfig class, using this class is the easiest approach to configure the behavior of the SDK’s pipeline handlers. When a service client object is created, an instance of the ClientConfig class can be provided. If one is not provided, the service client will create an instance with default values. One of the easiest ways to create a ClientConfig instance is to use the AWSSDK.Extensions.NETCore.Setup NuGet package. This package provides extension methods that create the ClientConfig instance and populates it using a .NET configuration provider (e.g., JSON configuration provider). For example, if you had an appsettings.json file with this content
{
"AWS": {
"MaxErrorRetry": 5,
"Timeout": "15000"
}
}
You could create a service client instance with these settings using the below code.
public Startup(IConfiguration configuration) {
var awsOptions = configuration.GetAWSOptions();
var s3Client = awsOptions.CreateServiceClient<IAmazonS3>();
}
When using the AWSOptions class, the ClientConfig is copied to each instance of a service client created by the CreateServiceClient method. You can read more in the Configuring AWS SDK with .NET Core blog post.
If you need specific service client instances to either have a unique set of pipeline handlers, or unique settings, subclassing would be a good option.
Modifying Pipeline Handler List
As mentioned earlier, pipeline handlers are a series of objects which process every request made to a service client. This implementation offers a decoupled design that allows for easy customization and extension of the request handling process. In this section, we will cover two ways with which you can customize the runtime pipeline’s list of pipeline handlers 1) subclassing each service client, and 2) using a centralized list of objects known as pipeline customizers.
Service Client Subclassing
The first option to modify the pipeline is by subclassing the specific service client that you would like to customize. Each of the service specific clients provided by the Amazon SDK for .NET (e.g. AmazonS3Client) inherit some base functionality from AmazonServiceClient. In addition to providing the service specific operations, these subclasses can further extend the pipeline by overriding the CustomizeRuntimePipeline method to add/remove/replace pipeline handlers.
If your use case calls for additional modifications to a specific service client class, such as adding custom processing before or after a service request, you can accomplish this by subclassing a particular service client class, such as AmazonS3Client or AmazonDynamoDBClient. Doing so allows you to override the CustomizeRuntimePipeline and add your custom pipeline handler, as depicted below:
public class CustomAmazonS3Client : AmazonS3Client {
...
protected override void CustomizeRuntimePipeline(RuntimePipeline pipeline) {
base.CustomizeRuntimePipeline(pipeline);
pipeline.AddHandler(new MyCustomHandler());
}
}
In addition to overriding and modifying the pipeline with your custom handler, you can also modify the configuration settings that get injected with your custom configuration when you subclass. This may be ideal when you would like your subclassed service client to utilize different settings than the rest of the service clients, because the latter will use the default application settings. You can create a customized client for a specific service that also contains some specific settings such as this example:
public class CustomAmazonS3Client : AmazonS3Client {
public CustomAmazonS3Client() :
base(FallbackCredentialsFactory.GetCredentials(),
new AmazonS3Config { MaxErrorRetry = 5, UseDualstackEndpoint = true, UseAccelerateEndpoint = true }) { }
}
The example above works fine when your use case only requires pipeline modifications for a specific service client, along with the advantage of updating the settings for only your custom service client. If your use case requires a customization that applies to all service clients, then accessing the RuntimePipelineCustomizerRegistry directly is be the best option.
Using the RuntimePipelineCustomizerRegistry
In the Amazon.Runtime.Internal namespace resides the RuntimePipelineCustomizerRegistry class which allows you to modify the runtime pipeline for all service clients created. As of now, this is the only means of modifying the runtime pipeline for all service clients in a centralized way.
As each instance of a service client is created, any implementation of the IRuntimePipelineCustomizer interface registered with the RuntimePipelineCustomizerRegistry is called and can modify the runtime pipeline. The following is a sample implementation of IRuntimePipelineCustomizer.
public class MyPipelineCustomizer : IRuntimePipelineCustomizer {
public string UniqueName { get; } = nameof(MyPipelineCustomizer);
/// <summary>
/// Called on service clients as they are being constructed to customize their runtime pipeline.
/// </summary>
/// <param name="pipeline">Runtime pipeline for newly created service clients.</param>
/// <param name="type">Type object for the service client being created</param>
public void Customize(Type type, RuntimePipeline pipeline) {
pipeline.AddHandler(new CustomHandler());
}
}
Now that the pipeline customizer is created, the next step is to register the instance with the RuntimePipeilneCustomizerRegistry Singleton.
RuntimePipelineCustomizerRegistry.Instance.Register(new MyPipelineCustomizer());
With registration taken place, the MyPipelineCustomizer will be called every time a new service client is created so that the CustomHandler is added to its runtime pipeline.
Conclusion
In this post, we covered how you can optimize the behavior of the AWS SDK for .NET, and customize it according to your needs. We dove deep into the three ways to configure the AWS service clients available through the AWS SDK for .NET. We started by showing you how you can apply custom configuration by using the ClientConfig class, then we moved on to subclassing of AWS service clients, and lastly, we reviewed how to customize the behavior that can apply to all service clients.
Carlos Santos
Carlos Santos is a Microsoft Specialist Solutions Architect with Amazon Web Services (AWS). In his role, Carlos helps customers through their cloud journey, leveraging his experience with application architecture, and distributed system design.
JP Velasco
JP Velasco is a Technical Account Manager with Amazon Web Services (AWS) Enterprise Support. JP helps customers in their AWS journey by solving scaling and optimization challenges to deliver operational excellence in the cloud through the practice of continuous improvement and delivery.