Introduction
This document explores a performance optimization technique for Dynamics 365 plugins by implementing centralized token caching using static variables in a plugin base class. Since plugin instances are recreated for every request, holding variables in memory is not feasible. However, static variables—such as a ConcurrentDictionary—defined in a base class can persist across executions, enabling efficient reuse of authentication tokens.
This approach avoids tens or even hundreds of thousands of daily calls to identity management endpoints, which can overload those services. Additionally, plugin execution time can increase by 500 milliseconds to one second per request if authentication is performed repeatedly.
If in-memory caching fails or is not viable, storing tokens in Dataverse and retrieving them as needed may be a better fallback than re-authenticating each time.
🔐 TokenService Implementation for Plugin Token Caching
To optimize authentication performance in Dynamics 365 plugins, a custom TokenService is implemented and injected into the plugin base architecture. This service enables centralized token caching using a **static, read-only **ConcurrentDictionary, which persists across plugin executions.
🧱 Design Overview
The TokenService exposes two methods:
- GetAccessToken(Guid key) – retrieves a cached token if it's still valid.
- SetAccessToken(Guid key, string token, DateTime expiry) – stores a new token with its expiry.
The core of the service is a static dictionary:
private static readonly ConcurrentDictionary<Guid, CachedAccessToken> TokenCache = new();
This dictionary is shared across plugin executions because it's defined in the base class. This is crucial since plugin instances are recreated per request and cannot hold instance-level state.
🧩 Integration into LocalPluginContext
The TokenService is injected into the well-known LocalPluginContext alongside other services like IOrganizationService, ITracingService, and IPluginExecutionContext.
This makes the token service available to all child plugins via the context object.
public ITokenService TokenService { get; }
public LocalPluginContext(IServiceProvider serviceProvider)
{
// Existing service setup...
TokenService = new TokenService(TracingService);
}
🔁 Token Retrieval Logic
GetAccessToken checks if a token exists and whether it’s about to expire:
public string GetAccessToken(Guid key)
{
if (TokenCache.TryGetValue(key, out var cachedToken))
{
var timeRemaining = (cachedToken.Expiry - DateTime.UtcNow).TotalMinutes;
if (timeRemaining > 2)
{
_tracingService.Trace($"Using cached token. Expires in {timeRemaining} minutes.");
return cachedToken.Token;
}
}
return null;
}
If the token is expired or missing, it returns null. It does not fetch a new token itself.
🔄 Token Refresh Responsibility
The responsibility to fetch a new token lies with the child plugin, because:
- It has access to secure configuration values (e.g., client ID, secret, tenant).
- It knows the context of the external service being called.
Once the child plugin fetches a new token, it calls:
TokenService.SetAccessToken(key, token, expiry);
This updates the shared cache for future executions.
🧱 Classic Plugin Base Pattern (Preserved)
public abstract class PluginBase : IPlugin
{
protected internal class LocalPluginContext
{
public IOrganizationService OrganizationService { get; }
public ITracingService TracingService { get; }
public IPluginExecutionContext PluginExecutionContext { get; }
public LocalPluginContext(IServiceProvider serviceProvider)
{
PluginExecutionContext = (IPluginExecutionContext)serviceProvider.GetService(typeof(IPluginExecutionContext));
TracingService = (ITracingService)serviceProvider.GetService(typeof(ITracingService));
OrganizationService = ((IOrganizationServiceFactory)serviceProvider.GetService(typeof(IOrganizationServiceFactory)))
.CreateOrganizationService(PluginExecutionContext.UserId);
}
}
public void Execute(IServiceProvider serviceProvider)
{
try
{
var localContext = new LocalPluginContext(serviceProvider);
localContext.TracingService.Trace("Plugin execution started.");
ExecutePlugin(localContext);
localContext.TracingService.Trace("Plugin execution completed.");
}
catch (Exception ex)
{
var tracingService = (ITracingService)serviceProvider.GetService(typeof(ITracingService));
tracingService.Trace($"Unhandled exception: {ex}");
throw new InvalidPluginExecutionException("An error occurred in the plugin.", ex);
}
}
protected abstract void ExecutePlugin(LocalPluginContext localContext);
}
🧩 TokenService Implementation
public interface ITokenService
{
string GetAccessToken(Guid key);
void SetAccessToken(Guid key, string token, DateTime expiry);
}
public sealed class TokenService : ITokenService
{
private readonly ITracingService _tracingService;
private static readonly ConcurrentDictionary<Guid, CachedAccessToken> TokenCache = new();
public TokenService(ITracingService tracingService)
{
_tracingService = tracingService;
}
public string GetAccessToken(Guid key)
{
if (TokenCache.TryGetValue(key, out var cachedToken))
{
var timeRemaining = (cachedToken.Expiry - DateTime.UtcNow).TotalMinutes;
if (timeRemaining > 2)
{
_tracingService.Trace($"Using cached token. Expires in {timeRemaining} minutes.");
return cachedToken.Token;
}
}
return null;
}
public void SetAccessToken(Guid key, string token, DateTime expiry)
{
TokenCache[key] = new CachedAccessToken(token, expiry);
_tracingService.Trace($"Token stored for key {key} with expiry at {expiry}.");
}
private class CachedAccessToken
{
public string Token { get; }
public DateTime Expiry { get; }
public CachedAccessToken(string token, DateTime expiry)
{
Token = token;
Expiry = expiry;
}
}
}
🧩 Add TokenService to LocalPluginContext
public ITokenService TokenService { get; }
public LocalPluginContext(IServiceProvider serviceProvider)
{
// ... existing setup ...
TokenService = new TokenService(TracingService);
}
🧪 Full Child Plugin Example with Secure Config and Token Usage
public class ExternalApiPlugin : PluginBase
{
private readonly SecureSettings _settings;
public ExternalApiPlugin(string unsecureConfig, string secureConfig)
{
// Parse secure config into settings object
_settings = JsonConvert.DeserializeObject<SecureSettings>(secureConfig);
}
protected override void ExecutePlugin(LocalPluginContext localContext)
{
localContext.TracingService.Trace("ExternalApiPlugin execution started.");
// Get token
string token = AccessTokenGenerator(_settings, localContext);
// Use token to call external API
CallExternalService(token, localContext.TracingService);
localContext.TracingService.Trace("ExternalApiPlugin execution completed.");
}
private string AccessTokenGenerator(SecureSettings settings, LocalPluginContext localContext)
{
var token = localContext.TokenService.GetAccessToken(settings.TokenKeyGuid);
if (!string.IsNullOrEmpty(token))
{
var payload = DecodeJwtPayload(token);
var expiryUnix = long.Parse(payload["exp"]);
var expiryDate = DateTimeOffset.FromUnixTimeSeconds(expiryUnix).UtcDateTime;
if ((expiryDate - DateTime.UtcNow).TotalMinutes > 2)
{
return token;
}
}
// Fetch new token
var newToken = FetchTokenFromOAuth(settings);
var newPayload = DecodeJwtPayload(newToken);
var newExpiry = DateTimeOffset.FromUnixTimeSeconds(long.Parse(newPayload["exp"])).UtcDateTime;
localContext.TokenService.SetAccessToken(settings.TokenKeyGuid, newToken, newExpiry);
return newToken;
}
private Dictionary<string, string> DecodeJwtPayload(string jwt)
{
var parts = jwt.Split('.');
var payload = parts[1].PadRight(parts[1].Length + (4 - parts[1].Length % 4) % 4, '=');
var bytes = Convert.FromBase64String(payload);
var json = Encoding.UTF8.GetString(bytes);
return JsonConvert.DeserializeObject<Dictionary<string, string>>(json);
}
private string FetchTokenFromOAuth(SecureSettings settings)
{
// Simulated token fetch logic
return "eyJhbGciOi..."; // JWT token
}
private void CallExternalService(string token, ITracingService tracingService)
{
tracingService.Trace("Calling external service with token...");
// Simulated API call
}
}
🧩 Wrapping It All Together
By combining these patterns into the base class–child class structure, we get a plugin framework that is:
- ✅ Familiar and extensible
- ⚡️ Optimized for performance with token caching
🗣️ Final Thoughts
These patterns weren’t invented in a vacuum—they were shaped by real customer needs and constraints. Whether you're modernizing legacy plugins or building new ones, I hope these ideas help you deliver more robust, scalable, and supportable solutions.