Instantiating clients from DI¶
The platform SDK supports inversion of control pattern by providing extensions for registering services (or interfaces of client classes) within Microsoft's dependency injection container - represented by interface IServiceCollection
. The user still needs to provide some input to complete the configuration, and there are some extension points that can be used to customize the behavior of the SDK.
DHI.Platform.SDK.DI.AddPlatformClients extension¶
The extension will register all relevant implementations of client interfaces into the given service collection.
- by default, all interface implementations are registered as singletons.
- all client classes are instantiated by resolving a collection of IClientPolicy
implementations into its constructor. This means that any implementations of IClientPolicy
interface you register will be injected into the clients. You can use that to control the behavior of http calls, log calls or troubleshoot problems.
- the registration procedure expects one authorization policy to be defined. As of 09-03-2021 there are two ways how to authorize with the SDK - using a Bearer token, or using an Open API key.
DHI.Platform.SDK.Spatial.DI.AddPlatformSpatialClients extension¶
When you install the DHI.Platforms.SDK.Spatial
package, you get access to AddPlatformSpatialClients
extension. This should be used in conjuction with AddPlatformClients
extension described earlier. It is applied to the result object of the AddPlatformClients
call, like for example here:
collection.AddPlatformClients(o =>
{
o.ConfigureOpenApiKey(openapikey);
o.PlatformEnvironment = PlatformEnvironment.Dev;
})
.AddPlatformSpatialClients();
The extension will add the IMultidimensionalClient
and IGISClient
to the service collection.
Customizing instantiation of client classes¶
It is possible to register services from the SDK using your own methods, or to replace certain registrations with your custom version. This might be needed if you want to call the service running on a url that your SDK version does not have embedded. By default the service addresses for the predefined environments are either embedded in the SDK, or can be discovered at runtime. If you however need to provide a different url, you can do so.
The following example replaces the singleton registration of the ComputeClient
with a transient one that uses a custom url.
var collection = new ServiceCollection();
collection.AddPlatformClients(o =>
{
o.ConfigureOpenApiKey(openapikey);
o.PlatformEnvironment = PlatformEnvironment.Dev;
});
var descriptor =
new ServiceDescriptor(
typeof(IComputeClient),
sp => new ComputeClient("http://mycustomuri", sp.GetServices<IClientPolicy>()),
ServiceLifetime.Transient);
collection.Replace(descriptor);
DHI.Platform.SDK.Configuration.IClientPolicy interface¶
The IClientPolicy
interface is used to modify the properties of outgoing http requests. Some policies are applied by default, out of control of the user. These default policies will:
- automatically send an SDK version in a header to be logged in the cloud;
- automatically choose the version of the underlying REST api.
Another implementation of IClientPolicy
is typically used for authorization.
- The most basic policy is OpenApiKeyPolicy
, which takes an Open API key (secret string) and passes it in a header to all calls. This allows the metadata service to identify the caller. This can be configured by method ConfigureOpenApiKey
on the PlatformOptions
object, or by registering the policy in your own way if you do not want to use the PlatformOptions
.
- Alternative way of authorization is to provide a Bearer token. For that there is
AccessTokenPolicy
, which accepts an implementation ofIAccessTokenProvider
. For ech outgoing call the policy will ask the provider for a token, which is then prefixed with a "Bearer: " string and sent in anAuthorization
header. You can either register the policy yourself, or you can only implement theIAccessTokenPRovider
and setup its use by calling a methodConfigureAccessToken
on the PlatformOptions object.
Client based on authorization token provided by user¶
The ClientFactory
provides generic methods for creating a client with all supported authorization methods( SAS token, Access token, OpenApiKey) to simplify usage.
The example shows the simplicity of creating a project client.
var factory = new ClientFactory(new ClientFactoryOptions()
{
PlatformEnvironment = PlatformEnvironment.Dev
});
var projectClient = factory.CreateClientSasToken<IProjectClient>("sas-token");
SAS token usage in the SDK¶
By default the SDK calls to any service in the platform goes through a gateway, which is a single service (= single url) that can handle different authentication methods and then proxies calls to other services (such as timeseries service, multidimensional service) as needed. This simplifies the authentication flow, but may have an impact on performance, because all requests now have to perform an extra "hop" through the gateway. If maximum performance (in terms of the request duration) is important, an option is to switch the SDK to use SAS token flow instead of the proxied calls.
The SAS token usage is controlled by a property called UsePlatformSasTokens
. When set to UsePlatformSasTokensOptions.InnerServices
, the calls to inner services (such as timeseries or multidimensional service) will try to call those services directly, as opposed to going through the gateway. However, the calls still need to be authorized somehow. The inner services understand only one authorization mechanism - the SAS tokens. A SAS token is a secure string that can be issued to a user and contains information about the user's privileges related to a given resource (a project or a dataset). So for each call to the inner services, a SAS token has to be provided. When set to UsePlatformSasTokensOptions.AllServices
, the calls to inner services and also directly metadata service will try to call those services directly as is described above.
There is an interface ISasTokenProvider
which is used by the *Client
classes to get the sas token for each call. The SDK will provide a default implementation of this interface - one that always asks a remote service whenever a token is requested. So for each request to the inner service, there will also be an extra request to get the SAS token first. This may defeat the objective to achieve better performance, unless some smarter mechanism to get the SAS tokens is employed, like caching.
Caching of the SAS tokens is not implemented by default, because the SAS tokens belong to individual users in the platform. However, the SDK does not know under which user the requests are being made. The SDK only has mechanism for plugging in some authentication headers, but the user can not always be determined from that. If however your application runs only with a single user context, or the user can be somehow determined for each request, you can simply implement SAS token caching. See the below example.
public class CachedSasTokenProvider : ISasTokenProvider
{
// ISasTokenClient is an interface provided by the SDK that provides access to a remote service issuing the SAS tokens
private readonly ISasTokenClient _sasTokenClient;
private readonly IMemoryCache _memoryCache;
public CachedSasTokenProvider(ISasTokenClient sasTokenClient, IMemoryCache memoryCache)
{
_sasTokenClient = sasTokenClient;
_memoryCache = memoryCache;
}
public async Task<string> GetRecursiveSasTokenAsync(Guid projectId, DateTime? expiration = null)
{
// The caching does not take the user context into account.
// Make sure that this type of caching is safe to do, or adjust the mechanism.
// Otherwise the access rights may become violated.
return await _memoryCache.GetOrCreateAsync($"recursivetoken-{projectId}", async entry =>
{
var token = await _sasTokenClient.GetRecursiveSasTokenAsync(projectId, expiration);
if (expiration != null)
entry.AbsoluteExpiration = expiration; // cache until the requested expiration is reached
else
entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(10); // cache for 10 minutes only - to be safe
return token;
});
}
public async Task<string> GetSasTokenAsync(Guid projectId, Guid? datasetId)
{
// The caching does not take the user context into account.
// Make sure that this type of caching is safe to do, or adjust the mechanism.
// Otherwise the access rights may become violated.
return await _memoryCache.GetOrCreateAsync($"token-{projectId}-{datasetId}", async entry =>
{
var token = await _sasTokenClient.GetSasTokenAsync(projectId, datasetId);
entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(10); // cache for 10 minutes only - to be safe
return token;
});
}
}
var collection = new ServiceCollection();
collection.AddPlatformClients(o =>
{
o.ConfigureOpenApiKey(openapikey);
o.PlatformEnvironment = PlatformEnvironment.Dev0;
o.UsePlatformSasTokens = UsePlatformSasTokensOptions.InnerServices; // turn on sas token usage
});
// add CachedSasTokenProvider to the container
collection.AddSingleton<CachedSasTokenProvider>();
// add cache provider
collection.AddMemoryCache();
// replace the default implementation of the ISasTokenProvider interface with custom implementation
var descriptor =
new ServiceDescriptor(
typeof(ISasTokenProvider),
sp => sp.GetRequiredService<CachedSasTokenProvider>(),
ServiceLifetime.Singleton);
collection.Replace(descriptor);