Search code examples
c#.netfallbacktransientpolly

Dynamic delegates in Polly’s Fallback


I have the following policy in a PolicyRegistry to be reused globally:

var fallbackPolicy = Policy
        .Handle<DrmException>().OrInner<DrmException>()
        .Fallback(
            fallbackAction: () => { //should commit or dispose the transaction here using a passed in Func or Action },
            onFallback: (exception) => { Log.Error().Exception(exception).Message($"Exception occurred, message: {exception.Message}.").Write(); }
        );

I have the following code which I want to implement the fallbackPolicy in:

   if(Settings.DRM_ENABLED)
       drmManager.ExecuteAsync(new DeleteUser(123).Wait();//HTTP Call, throws DrmException if unsuccessful

       //in some cases, there is an if(transaction == null) here (if transaction was passed as a parameter and needs to be committed here)
       transaction.Commit();//if not thrown - commits the transaction

I would like it to look something like this:

var fallbackPolicy = Policy
            .Handle<DrmException>().OrInner<DrmException>()
            .Fallback(
                fallbackAction: (transaction) => { transaction.Dispose(); },
                onFallback: (exception) => { Log.Error().Exception(exception).Message($"Exception occurred, message: {exception.Message}.").Write(); }
            );    

fallbackPolicy.Execute(() => drmManager.ExecuteAsync(new DeleteUser(123).Wait(), transaction)

As far as I understand the fallbackPolicy.Execute takes Action/Func to be carried out which either succeeds, in which case the fallbackPolicy is not hit, or fails, in which case the fallbackPolicy kicks in with some predefined fallbackAction.

What I would like to do is to pass in two handlers (onFail(transaction) which disposes the transaction and onSuccess(transaction) which commits the transaction) when executing the policy. Is there an easier way of doing it instead of wrapping it or using a Polly's context?


Solution

  • Feels like there are a few separate questions here:

    1. How can I make a centrally-defined FallbackPolicy do something dynamic?
    2. How can I make one FallbackPolicy do two things?
    3. With Polly in the mix, how can I do one thing on overall failure and another on overall success?

    I'll answer these separately to give you a full toolkit to build your own solution - and for future readers - but you'll probably not need all three to achieve your goal. Cut to 3. if you just want a solution.

    1. How can I make a centrally-defined FallbackPolicy do something dynamic?

    For any policy defined centrally, yes Context is the way you can pass in something specific for that execution. References: discussion in a Polly issue; blog post.

    Part of your q seems around making the FallbackPolicy both log; and deal with the transaction. So ...

    2. How can I make one FallbackPolicy do two things?

    You can pass in something dynamic (per above). Another option is use two different fallback policies. You can use the same kind of policy multiple times in a PolicyWrap. So you could define a centrally-stored FallbackPolicy to do just the logging, and keep it simple, non-dynamic:

    var loggingFallbackPolicy = Policy
        .Handle<DrmException>().OrInner<DrmException>()
        .Fallback(fallbackAction: () => { /* maybe nothing, maybe rethrow - see discussion below */ },
             onFallback: (exception) => { /* logging; */ });
    

    Then you can define another FallbackPolicy locally to roll back the transaction on failure. Since it's defined locally, you could likely just pass the transaction variable in to its fallbackAction: using a closure (in which case you don't have to use Context).

    Note: If using two FallbackPolicys in a PolicyWrap, you'd need to make the inner FallbackPolicy rethrow (not swallow) the handled exception, so that the outer FallbackPolicy also handles it.


    Re:

    What I would like to do is to pass in two handlers (onFail(transaction) which disposes the transaction and onSuccess(transaction) which commits the transaction)

    There isn't any policy which offers special handling on success, but:

    3. With Polly in the mix, how can I do one thing on overall failure and another on overall success?

    Use .ExecuteAndCapture(...). This returns a PolicyResult with property .Outcome == OutcomeType.Successful or OutcomeType.Failure (and other info: see documentation)

    So overall, something like:

    var logAndRethrowFallbackPolicy = Policy
        .Handle<DrmException>().OrInner<DrmException>()
        .Fallback(fallbackAction: (exception, context, token) => { 
            throw exception; // intentional rethrow so that the 'capture' of ExecuteAndCapture reacts.  Use ExceptionDispatchInfo if you care about the original call stack.
            },
            onFallback: (exception, context) => { /* logging */ });
    

    At execution site:

    PolicyResult result = myPolicies.ExecuteAndCapture(() => ... ); // where myPolicies is some PolicyWrap with logAndRethrowFallbackPolicy outermost
    if (result.Outcome == OutcomeType.Successful)
        { transaction.Commit(); }
    else
        { transaction.Dispose(); }