Async Redis, DynamoDB, Cache, Session, Auth Repo & Auth Providers

Just a quick announcement to document some upcoming changes for anyone using the v5.9.3 pre-release NuGet packages on MyGet.

A large part of this release was focused on adding Async APIs to many of ServiceStack’s providers most of which now have both Sync & Async APIs starting with ICacheClientAsync which all ServiceStack Caching Providers now support in addition to ICacheClient.

Which you can access from inside your ServiceStack Services (and other MVC/Razor base classes) with the CacheAsync property, e.g:

public Task<object> Any(MyRequest request)
{
    var item = await CacheAsync.GetAsync<Item>("key");
    //....
}

Outside of ServiceStack you can the AppHost.GetCacheClientAsync() API to access the ICacheClientAsync provider:

var cache = HostContext.AppHost.GetCacheClientAsync();
var item = await cache.GetAsync<Item>("key");

Nothing different is needed to register ICacheClientAsync which utilizes the existing ICacheClient dependency since all built-in caching providers implements both. In addition you can even use the ICacheClientAsync APIs even when using your own ICacheClient providers as it will return an Async ICacheClientAsync wrapper over Sync ICacheClient APIs.

In order to implement true async caching providers all underlying clients also needed to implement Async APIs.

Redis Async

The biggest client library implemented in this release by sheer API surface area is ServiceStack.Redis. All Redis Client Managers also implement both IRedisClientsManager and IRedisClientsManagerAsync so you can use your existing Redis configuration to access Redis in your Services with GetRedisAsync(), e.g:

public async Task<object> Any(AsyncRedis request)
{
    await using var redis = await GetRedisAsync();
    await redis.IncrementAsync(nameof(AsyncRedis), request.Value);
    
    return new AsyncRedisResponse {
        Value = await redis.GetAsync<long>(nameof(AsyncRedis))
    };
}

Or should you wish you can use both Sync/Async APIs in the same project, e.g:

public async object Any(SyncRedis request)
{
    Redis.Increment(nameof(SyncRedis), request.Value);
    
    return new SyncRedisResponse {
        Value = Redis.Get<long>(nameof(SyncRedis))
    };
}

The async support in ServiceStack.Redis differs from other async APIs in that it aimed for maximum efficiency so uses ValueTask & other modern Async APIs so it requires a minimum v4.7.2+ .NET Framework or .NET Standard 2.0 (i.e. .NET Core) project. All other Async APIs return the more interoperable Task responses for their async APIs.

If you’re using ServiceStack.Redis in your own (i.e. non ServiceStack) projects you could just register IRedisClientsManagerAsync to force usage of async APIs as it only lets you resolve async only IRedisClientAsync and ICacheClientAsync clients, e.g:

public void ConfigureServices(IServiceCollection services)
{
    services.AddSingleton<IRedisClientsManagerAsync>(c => new RedisManagerPool());
}

//... 

public class MyDep
{
    private IRedisClientsManagerAsync manager;
    public MyDep(IRedisClientsManagerAsync manager) => this.manager = manager;

    public async Task<long> Incr(string key, uint value)
    {
        await using var redis = await manager.GetClientAsync();
        return await redis.IncrementAsync(key, value);
    }
}

PocoDynamo Async

Just like ServiceStack.Redis the ServiceStack.Aws PocoDynamo Dynamo DB client now also supports both sync & async APIs where the IPocoDynamo interface inherits IPocoDynamoAsync interface so you can use the same client to make sync & async API calls whilst continue to use the
same PocoDynamo registration.

Azure Table Storage Async

Likewise AzureTableCacheClient also implements ICacheClientAsync which

container.Register<ICacheClient>(new AzureTableCacheClient(cacheConnStr));

Async Auth Repositories

All built-in ServiceStack Auth Repositories now also implement IUserAuthRepositoryAsync
which you can use inside ServiceStack Services with the AuthRepositoryAsync property, e.g:

public async Task<object> Post(GetUserAuth request)
{
    var userAuth = await AuthRepositoryAsync.GetUserAuthByUserNameAsync(request.UserName);
    if (userAuth == null)
        throw HttpError.NotFound(request.UserName);
    return userAuth;
}

Outside of ServiceStack you can access it from the AppHost.GetAuthRepositoryAsync() API, e.g:

var authRepo = HostContext.AppHost.GetAuthRepositoryAsync();
await using (authRepo as IAsyncDisposable)
{
    //...
}

Like the caching providers the async Auth Repositories makes use of the existing IAuthRepository registration so no additional configuration is needed. Also your Services can use the IAuthRepositoryAsync APIs above even for your own sync IAuthRepository providers as it will return a IAuthRepositoryAsync wrapper API in its place.

Async Auth Providers

To make usage of the new async API functionality all of ServiceStack’s built-in Auth Providers were rewritten to use the new Async APIs. If you’re only using the existing Auth Providers this will be a transparent detail, however your own Custom Auth Providers will need to change as all existing Sync I/O base class APIs have been refactored into Async APIs.

JWT UseTokenCookie

When JWT is enabled if you wanted to return your Authenticated UserSession into a stateless JWT Token Cookie your clients would’ve needed to request it with UseTokenCookie on the Authenticate Request or in a hidden FORM Input. This capability was only available for Authenticate requests as OAuth requests would’ve needed a separate call to Convert their Server Session into a JWT Cookie.

You can now configure this behavior on the server with the new UseTokenCookie on the JWT Auth Provider which now works for both successful Authenticate Requests & OAuth Sign Ins:

new JwtAuthProvider(appSettings) {
    UseTokenCookie = true,
},

Breaking Changes

Async Auth Providers

The recommendation would be change your existing Auth Providers to use the new Async APIs which all follow the same async method convention, i.e:

  • Has an *Async suffix
  • Takes an optional CancellationToken as its last parameter
  • Returns a Task

Here’s an example of all these changes to convert a sync into an async method:

int Add(int value);

Task<int> AddAsync(int value, CancellationToken token = default);

So if your custom Auth Provider inherits from CredentialsAuthProvider it would now need to implement:

//Async
public class CustomCredentialsAuthProvider : CredentialsAuthProvider
{
    public virtual async Task<bool> TryAuthenticateAsync(IServiceBase authService, 
        string userName, string password, CancellationToken token=default)
    {
        //Add here your custom auth logic (database calls etc)
        //Return true if credentials are valid, otherwise false
    }

    public override async Task<object> AuthenticateAsync(IServiceBase authService, 
        IAuthSession session, Authenticate request, CancellationToken token = default)
    {
        //Fill IAuthSession with data you want to retrieve in the app eg:
        session.FirstName = "some_firstname_from_db";

        //Call base method to Save Session and fire Auth/Session callbacks:
        return await base.AuthenticateAsync(authService, session, tokens, authInfo, token);
    }
}

To simplify migration of existing Auth Providers when upgrading ServiceStack, the popular Auth Providers below used to implement Custom Auth Providers now have a Sync suffix:

  • CredentialsAuthProviderSync
  • BasicAuthProviderSync
  • AuthProviderSync
  • OAuthProviderSync
  • OAuth2ProviderSync

So the easiest way to migrate would be to just add a *Sync suffix to your base class, e.g:

//Sync
public class CustomCredentialsAuthProvider : CredentialsAuthProviderSync
{
    public override bool TryAuthenticate(IServiceBase authService, 
        string userName, string password)
    {
    }

    public override IHttpResult OnAuthenticated(IServiceBase authService, 
        IAuthSession session, IAuthTokens tokens, 
        Dictionary<string, string> authInfo)
    {
        return base.OnAuthenticated(authService, session, tokens, authInfo);
    }
}

Note that all sync providers use continue to use the sync ICacheClient and IAuthRepository and Session APIs whilst the async providers only use the new async providers. Since there are shims to support when no async APIs are available, you can continue to use either async or sync APIs without issue.

Auth Response & URL Redirect Filters

If your AuthProvider implements IAuthResponseFilter to implement for intercepting successful Authenticate Request DTO requests, the interface gains an additional ResultFilterAsync method
for intercepting successful OAuth redirect responses:

public interface IAuthResponseFilter
{
    // Intercept successful Authenticate Request DTO requests
    void Execute(AuthFilterContext authContext);
    
    // Intercept successful OAuth redirect requests
    Task ResultFilterAsync(AuthResultContext context, CancellationToken token);
}

Which you can provide an empty implementation by returning a completed task, e.g:

public Task ResultFilterAsync(AuthResultContext context, CancellationToken token)
     => Task.CompletedTask;

The OAuth URL Filters have changed from being passed an AuthProvider to an AuthContext, if your URL filter made use of the AuthProvider it can be accessed from AuthContext.AuthProvider, e.g:

SuccessRedirectUrlFilter = (authProvider,url) => ...;

SuccessRedirectUrlFilter = (authContext,url) => authContext.AuthProvider ...;

Together these features is used to implement the new JwtAuthProvider.UseTokenCookie feature.

Session Save APIs

Whilst ServiceStack has been changed to use async APIs it will only fire OnSaveSessionAsync() AppHost callback to save the session. So if you previously have overridden OnSaveSession in your AppHost to intercept when sessions are saved you’ll need to change it to override OnSaveSessionAsync instead, e.g:

[Obsolete("Use OnSaveSessionAsync")]
public override void OnSaveSession(IRequest httpReq, IAuthSession session, TimeSpan? expiresIn = null)
{
}

public override Task OnSaveSessionAsync(IRequest httpReq, IAuthSession session, TimeSpan? expiresIn = null, CancellationToken token=default)
{
}

Note that if you have code that calls the sync SaveSession() to save a Users Session you would either need to change it to use SaveSessionAsync() or override both APIs above if you’re intercepting session saves.

That’s the main gotcha’s from the refactor to use async APIs, most of the time the Async APIs are just additive so it shouldn’t effect existing code.

Authenticate & Register Services

If you’re using HostContext.ResolveService<T> to call either the AuthenticateService or RegisterService APIs, e.g. to Authenticate or impersonate a user:

using var authService = HostContext.ResolveService<AuthenticateService>(req);
var response = authService.Post(new Authenticate
{
    provider = Name,
    UserName = userName,
    Password = password
});

The Post() API implementation is now a deprecated “sync over async” implementation which although will continue to work, you’re encouraged to change it to use the async version, e.g:

var response = await authService.PostAsync(new Authenticate { ... });

If you do run into any other issues after upgrading to v5.9.3+ please let me know and I’ll rectify them ASAP.

1 Like

Optional *Async Suffixes

In addition to the expanded support for Async, your Services can now optionally have the *Async suffix which by .NET Standard guidelines (& my preference) is preferred for Async methods to telegraph to client call sites that its response should be awaited.

If both exists (e.g. Post() and PostAsync()) the *Async method will take precedence & be invoked instead.

Allowing both is useful if you have internal services directly invoking other Services using HostContext.ResolveService<T>() where you can upgrade your Service to use an Async implementation without breaking existing clients, e.g. this is used in RegisterService.cs:

[Obsolete("Use PostAsync")]
public object Post(Register request)
{
    try
    {
        var task = PostAsync(request);
        return task.GetResult();
    }
    catch (Exception e)
    {
        throw e.UnwrapIfSingleException();
    }
}

/// <summary>
/// Create new Registration
/// </summary>
public async Task<object> PostAsync(Register request)
{
    //... async impl
}            

To change to use an async implementation whilst retaining backwards compatibility with existing call sites, e.g:

using var service = HostContext.ResolveService<RegisterService>(Request);
var response = service.Post(new Register { ... });

This is important if the response is ignored as the C# compiler wont give you any hints to await the response which can lead to timing issues where the Services is invoked but User Registration hasn’t completed as-is often assumed.

The other option is to rename your method to use *Async suffix so the C# compiler will fail on call sites so you can replace the call-sites to await the async Task response, e.g:

using var service = HostContext.ResolveService<RegisterService>(Request);
var response = await service.PostAsync(new Register { ... });

Support for ValueTask

Not sure if it was explicitly mentioned, but Services can now return the more optimal ValueTask<object> responses.

1 Like

Is there a tentative release date for 5.9.3?

v5.9.3 is on MyGet now, hoping to publish v5.9.4 on NuGet towards end of the month.