Skip to content

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 of IAccessTokenProvider. For ech outgoing call the policy will ask the provider for a token, which is then prefixed with a "Bearer: " string and sent in an Authorization header. You can either register the policy yourself, or you can only implement the IAccessTokenPRovider and setup its use by calling a method ConfigureAccessToken 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);