-
Notifications
You must be signed in to change notification settings - Fork 1
Samples
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}";
}
}
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";
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 userId) {
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(userId, at);
if(success) {
// restart the same workflow with same user id
// however workflow will have new workflow id
await RenewMembershipWorkflow.CreateAsync(this.Context, userId);
return "Done";
}
// try after 3 days again...
at = TimeSpan.FromDays(3);
}
// renewal failed...
return "Failed";
}
[Activity]
public virtual async Task<bool> RenewAsync(
long userId,
[Schedule] TimeSpan at,
[Inject] IPaymentService paymentService = null,
[Inject] IEmailService emailService = null
) {
var result = await paymentService.ChargeAsync(userId);
if(result.Success) {
return true;
}
await emailService.SendFailedRenewalAsync(userId);
return false;
}
}
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";
}
}
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.