Skip to content

Samples

Akash Kava edited this page Nov 10, 2021 · 8 revisions

Configure Workflow Service

public class WorkflowContext: EternityContext {

   public WorkflowContext(IServiceProvider services):
        base(new EternityAzureStorage(("ET", "azure storage connection string...")), 
            services, new EternityClock()) {
   }
}

// register this as background service
public class WorkflowBackgroundService : BackgroundService {
    private readonly WorkflowContext workflowContext;
    private readonly TelemetryClient telemetryClient;

    public WorkflowBackgroundService(WorkflowContext workflowContext, TelemetryClient telemetryClient)
    {
        this.workflowContext = workflowContext;
        this.telemetryClient = telemetryClient;
    }

    protected async override Task ExecuteAsync(CancellationToken stoppingToken) {
        while (!stoppingToken.IsCancellationRequested) {
            try {
                await workflowService.ProcessMessagesAsync(cancellationToken: stoppingToken);
            } catch (Exception ex) {
                telemetryClient.TrackException(ex);
            }
        }
    }
}

Register Service Scope

To enable Microsoft.DependencyInjection.Extensions Scope, add following in configure method.

services.AddEternityServiceScope();

This will make every activity execute in separate service scope, you can inject scoped services in Activities.

Create new workflow

// create new workflow and execute now
var id = await SignupWorkflow.CreateAsync(context, "sample@gmail.com");

// raise an event...
await context.RaiseEventAsync(id, SignupWorkflow.Verify, verificationCode);

Signup Example

Lets assume we want to verify email address of user before signup, we want to set max timeout to 45 minutes and maximum 3 retries.

Activities are methods of the same class marked with [Activity] attribute and methods must be public and virtual.

Activities can also be scheduled in future by passing a parameter marked with [Schedule] attribute as shown below.

public class SignupWorkflow : Workflow<SignupWorkflow, string, string> {

    // name of external event
    public const string Resend = nameof(Resend);

    // name of external event
    public const string Verify = nameof(Verify);

    public override async Task<string> RunAsync(string input)
    {
        var maxWait = TimeSpan.FromMinutes(15);
        var code = (this.CurrentUtc.Ticks & 0xF).ToString();
        await SendEmailAsync(input, code);
        for (int i = 0; i < 3; i++)
        {
            var (name, result) = await WaitForExternalEventsAsync(maxWait, Resend, Verify);
            switch(name)
            {
                case Verify:
                    if(result == code)
                    {
                        return "Verified";
                    }
                    break;
                case Resend:
                    await SendEmailAsync(input, code, i);
                    break;
            }
        }
        return "NotVerified";
    }

    [Activity]
    public virtual async Task<string> SendEmailAsync(
        string emailAddress, 
        string code, 
        int attempt = -1,
        [Inject] MockEmailService emailService = null) {
        await Task.Delay(100);
        emailService.Emails.Add((emailAddress, code, CurrentUtc));
        return $"{emailService.Emails.Count-1}";
    }
}

Mobile Sample

For mobile, on iOS, there is no way to generate the code, so you can use Schedule method by importing .Mobile namespace as shown below.

var maxWait = TimeSpan.FromMinutes(15);
var code = (this.CurrentUtc.Ticks & 0xF).ToString();
await SendEmailAsync(input, code);
for (int i = 0; i < 3; i++)
{
    var (name, result) = await WaitForExternalEventsAsync(maxWait, Resend, Verify);
    switch(name)
    {
        case Verify:
            if(result == code)
            {
                return "Verified";
            }
            break;
        case Resend:
            // note this will use method delegate and will ensure that we are passing
            // same type of parameters, the only problem is you will have to supply all
            // default parameters as well
            await this.ScheduleAsync( SendEmailAsync, input, code, i, null);
            break;
    }
}
return "NotVerified";

Renew Membership

In the following example, we are creating Renew Membership Workflow when user registers for one year. In the following example, we will renew the membership after 364 days from the current utc date. The workflow will be suspended immediately and it will not occupy any memory. After 364 days, workflow will restart and will continue to renew. However, if user decides to cancel, you can raise an event with context that will cause workflow to cancel.

public class RenewMembershipWorkflow: Workflow<RenewMembershipWorkflow,long,string> {

    public const string Cancel = nameof(Cancel);
    
    public async Task<string> RunAsync(long id) {

        var till = TimeSpan.FromDays(364);
        var (name, result) = await this.WaitForExtenralEvents(till, Cancel);
        if (name == Cancel) {
            // user has cancelled the membership, exit..
            return "Cancelled";
        }
        // at this time, this workflow will be suspended and removed from the execution
        // internally it will throw `ActivitySuspendedException` and it will start
        // just before the given timespan

        for(int i = 0; i<3; i++) {
            var success = await RewewAsync(id, at);
            if(success) {

                // restart the same workflow
                await RenewMembershipWorkflow.CreateAsync(this.Context, id);

                return "Done";
            }

            // try after 3 days again...
            at = TimeSpan.FromDays(3);
        }

        // renewal failed...
        return "Failed";

    }

    [Activity]
    public virtual async Task<bool> RenewAsync(
        long id, 
        [Schedule] TimeSpan at, 
        [Inject] IPaymentService paymentService = null,
        [Inject] IEmailService emailService = null
        ) {

        var result = await paymentService.ChargeAsync(id);
        if(result.Success) {
            return true;
        }
        await emailService.SendFailedRenewalAsync(id);
        return false;
    }   
}

Blocking Users by IP Address

Surprisingly, Eternity Workflow can be used as interactive state machine. Lets review following example where in we want to block user for particular IP address if more than 3 failed login attempts occur in short period of time (lets say 1 minute).

public class BanUserByIPWorkflow: Workflow<BanUserByIPWorkflow, string, string> {

    public const string Offense = nameof(Offense);

    public async Task<string> RunAsync(string ip) {

        // PreserveTimeSpan preserves workflow for specified time
        // after workflow was successfully executed
        // We can use this property to ban ip address for 5 minutes
        // and after 5 minutes, workflow will be deleted

        PreserveTimeSpan = TimeSpan.FromMinutes(5);

        // We will wait for external event by our microservice to raise event
        // if an offence event was raised, we increase counter and if 3 offense
        // were raised in less than 3 minutes, we will return "Banned". In this case
        // workflow will be kept alive for 5 minutes..
        for(int i = 0; i < 3; i++) {
            var (name,result) = await this.WaitForExternalEvents(
                TimeSpan.FromSeconds(60),
                Offense);
            if(name == Offense) {
                continue;
            }
            // no offense in 60 seconds
            // delete immediately...
            PreserveTimeSpan = TimeSpan.Zero;
            return "NotBanned";
        }
        return "Banned";
    }
}

Usage

We will create or update workflow when failed login attempt occurs. If workflow exists, we raise an event for Offence.

[HttpPost]
public async Task<IActionResult> LoginPage(
    [FromService] EternityContext context,
    [FromBody] Body model
) {

    if(loginFailed) {
        var ipAddress = ....
        // azure table storage requires some encoding
        var id = Uri.EscapeDataString(ipAddress);
        var r = await BanUserByIPWorkflow.GetStatusAsync(context,
            id);
        if(r == null) {
            // create a new workflow 
            await BanUserByIPWorkflow.CreateAsync(context, 
                 id);
        } else {
            // register additional offense
            await context.RaiseEventAsync(
                  id,
                  BanUserByIPWorkflow.Offense);
        }
     }
     ...
}

Following method will first check if BanUserByIPWorkflow with given ID (IP Address) exists with result "Banned".

[HttpGet]
public async Task<IActionResult> LoginPage(
    [FromService] EternityContext context
) {
    var ipAddress = ....
    // azure table storage requires some encoding
    var id = Uri.EscapeDataString(ipAddress);
    var r = await BanUserByIPWorkflow.GetStatusAsync(context,
        id);
    if(r?.Result == "Banned") {
         return Forbidden();
    }
    ....
}

Interesting part is, you don't need to create special table and need to set a timer to delete bad IP address entries. Everything occurs and retains as if workflows are always existing in memory retaining correct states. And these workflows are machine independent, so they can be used by various machines using same Azure Storage. And changes exists everywhere.

Clone this wiki locally