Whenever possible, use Dependency Injection (DI). If your application/framework does not allow that, you can instantiate clients also without Dependency Injection as described at the end of this page.
Note that in order to authenticate with user credentials, you will need to understand integration options and application registration.
Instantiating clients with Dependency Injection¶
The platform SDK supports inversion of control (IOC) 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. Here we describe key methods for initialization of the DHI.Platform.SDK
clients.
The IAccessTokenProvider
interface¶
The recommended way of authentication is to use user credentials and token based authentication. To make this happen, the client needs a Bearer token acquired during the user authentication flow. The sole responsibility of the DHI.Platform.SDK.IAccessTokenProvider
implementation is to return the token from GetTokenAsync
method.
DHI.Platform.SDK.Web
provides several implementations that you can reuse as explained in Building BFF. If you cannot reuse any, subclass from PlatformWebAccessTokenProviderBase
and implement the GetTokenAsync
method and potentially override IsTokenExpired
. If none of these options meet your needs, implement your own IAccessTokenProvider
.
.DI.AddPlatformClients
extension¶
This extension method 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 ofIClientPolicy
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 May 2024 there are two ways how to authorize with the SDK - using a Bearer token, or using an Open API key.
Using a Bearer token (requires an implementation of IAccessTokenProvider
with Task<string> GetTokenAsync();
method):
collection.AddPlatformClients(o => { o.ConfigureAccessToken(tokenProvider); });
By default, the services are configured against the production environment. If you ever need to choose a different environment, use the PlatformEnvironment
configuration option:
collection.AddPlatformClients(o =>
{
o.ConfigureAccessToken(tokenProvider);
o.PlatformEnvironment = PlatformEnvironment.Dev;
});
.Spatial.DI.AddPlatformSpatialClients
extension¶
When you install the DHI.Platforms.SDK.Spatial
package, you get access to AddPlatformSpatialClients
extension. This should be used in conjunction with AddPlatformClients
extension described earlier. It is applied to the result object of the AddPlatformClients
call, like for example here:
collection.AddPlatformClients(o =>
{
o.ConfigureAccessToken(tokenProvider);
})
.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.ConfigureAccessToken(tokenProvider);
});
var descriptor =
new ServiceDescriptor(
typeof(IComputeClient),
sp => new ComputeClient("http://mycustomuri", sp.GetServices<IClientPolicy>()),
ServiceLifetime.Transient);
collection.Replace(descriptor);
.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 methodConfigureOpenApiKey
on thePlatformOptions
object, or by registering the policy in your own way if you do not want to use thePlatformOptions
. -
Alternative way of authorization is to provide a Bearer token. For that there is
AccessTokenPolicy
, which accepts an implementation ofIAccessTokenProvider
. For each 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");
Shared Access Signature (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 impact performance, because all requests have to travel through the gateway. In some cases, it may be beneficial to make the SDK 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 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. - When set to
UsePlatformSasTokensOptions.No
, the calls will use the gateway as by default.
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.ConfigureAccessToken(tokenProvider);
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);
Instantiating clients without Dependency Injection¶
This section provides guidelines on how to setup EventSubscriberClient
and ComputeClient
in case your application/framework is unable to leverage AddPlatformClients
and Microsoft's dependency injection container.
Set up logging provider and creating logger for EventSubscriberClient
:
using var loggerFactory = LoggerFactory.Create(builder => builder.AddConsole());
var logger = loggerFactory.CreateLogger<EventSubscriberClient>();
Set up authorization policy. This sample uses OpenApiKeyPolicy
and that openapikey
has to be provided:
var openApiKeyPolicy = new OpenApiKeyPolicy(openapikey);
Set up ServiceUriProvider
and SasTokenClient
:
var platformEnvironment = PlatformEnvironment.Prod;
var serviceUriProvider = new ServiceUriProvider(platformEnvironment, openApiKeyPolicy);
var sasTokenClient = new SasTokenClient(platformEnvironment, openApiKeyPolicy);
Instantiate EventSubscriberClient
:
var eventSubscriberClient = new EventSubscriberClient(logger, serviceUriProvider, sasTokenClient);
Instantiate ComputeClient
and create an execution:
var computeClient = new ComputeClient(serviceUriProvider, sasTokenClient, eventSubscriberClient);
await computeClient
.CreateExecution(projectId)
.WithModelRun(new FemEngineHD(), "https://coreenginedev0inputs.blob.core.windows.net/data/lake-seq/lake1.m21fm")
.WithRemoteInput(
new RemoteInput("https://coreenginedev0inputs.blob.core.windows.net/data/lake-seq/lake1.m21fm", "lake1.m21fm"),
new RemoteInput("https://coreenginedev0inputs.blob.core.windows.net/data/lake-seq/lake2.m21fm", "lake2.m21fm"),
new RemoteInput("https://coreenginedev0inputs.blob.core.windows.net/data/lake-seq/lake.mesh", "lake.mesh"))
.WithMessageHandler(new MessageHandler())
.ExecuteAndWaitAsync();