So, I’ve been a bad presenter.
I’ve given my Programming in 4D talk already several times and I haven’t yet uploaded the code for it.
And seeing as I’m going to be giving it again today (at the DevWeek conference in London), I figured that I should finally get my act together and put it online.
Interestingly enough, the other conferences I’ve spoken at either didn’t record it or didn’t put the recording online. Hopefully DevWeek will do better <FingersCrossed/>.
The overall solution
The scenario I talk about in this presentation is (again) a standard one in the world of retail: customers who want to return products that they’ve purchased and get a refund.
I’ve used our new ServiceMatrix tooling to model the solution like this (click for a larger image):
If you’d like to download the complete solution, click here.
Now, we’re going to focus on the RefundPolicy object that you can see towards the bottom left.
The Refund Policy
So, in this scenario, what we’re going to implement is a process whereby if you return your products within 30 days of the purchase, you’ll receive a 100% refund; if you return your products within 60 days you’ll receive a 50% refund, and anything longer than that and no refund for you.
Here’s the code:
public class RefundPolicy : Saga<RefundPolicySagaData>,
IAmStartedByMessages<OrderAccepted>,
IHandleMessages<ProductsReturned>,
IHandleTimeouts<Percent>,
IHandleSagaNotFound
{
public void Handle(OrderAccepted message)
{
Data.OrderId = message.OrderId;
Console.WriteLine("OrderAccepted");
Data.Percent = 100;
RequestTimeout(TimeSpan.FromDays(30), 50.Percent());
RequestTimeout(TimeSpan.FromDays(60), 0.Percent());
}
public void Handle(ProductsReturned message)
{
Console.WriteLine("ProductsReturned");
Bus.Send<IssueRefund>(m => m.Percent = Data.Percent);
MarkAsComplete();
}
public override void ConfigureHowToFindSaga()
{
ConfigureMapping<ProductsReturned>(m => m.OrderId).ToSaga(s => s.OrderId);
}
public void Timeout(Percent state)
{
Console.WriteLine("Timeout");
Data.Percent = state;
if (state == 0)
MarkAsComplete();
}
public void Handle(object message)
{
if (message is ProductsReturned)
Console.WriteLine("No refund for you");
}
}
public class RefundPolicySagaData : ContainSagaData
{
[Unique]
public int OrderId { get; set; }
public Percent Percent { get; set; }
}
And what’s so good about that?
Well, not to steal my own thunder and give you a reason not to watch the video when it comes out, the trick is in the two RequestTimeout calls that are invoked when an OrderAccepted message arrives. You see, what that does is that it takes the data that defines the behavior of the refund policy and persists that via a queue, which will play back the data to our object according to the defined schedule.
This way, if/when the business decides to change the rules (say, reducing the timeframes to 20 days and 40 days), that will only affect users who are placing new purchases. The customers who made a purchase 50 days earlier (when the rules were still 30/60) will get the correct behavior applied to them.
Next steps
Of course, nobody would want a developer to have to open this code and change it in order to make a simple change like 30/60 -> 20/40, so we could externalize the data by creating a dictionary property on the saga where the key is the TimeSpan and the value is the Percent. Then, we could pull those values from either a config file or database and inject them into the property on the saga.
Here’s how the code of the saga would change:
public class RefundPolicy : Saga<RefundPolicySagaData>,
IAmStartedByMessages<OrderAccepted>,
IHandleMessages<ProductsReturned>,
IHandleTimeouts<Percent>,
IHandleSagaNotFound
{
public Dictionary<TimeSpan, Percent> DataDefinitions { get; set; }
partial void HandleImplementation(OrderAccepted message)
{
Data.OrderId = message.OrderId;
Console.WriteLine("OrderAccepted");
Data.Percent = 100;
foreach(var kv in DataDefinitions)
RequestTimeout(kv.Key, kv.Value);
}
And the code to do the property injection would look like this:
public class RefundPolicyDataConfigurator : INeedInitialization
{
public void Init()
{
Dictionary<TimeSpan, Percent> data = null /* get from config instead */;
Configure.Instance.Configurer
.ConfigureProperty<RefundPolicy>(p => p.DataDefinitions, data);
}
}
Classes the implement INeedInitialization are invoked at process startup, and that call to ConfigureProperty instructs the container to set the DataDefinitions property of our RefundPolicy every time it is resolved (which is on every message).
In closing
We now have a solution which allows non-developers to make changes to the refund policy definitions without requiring us to deploy any new code to production. Also, any changes that are made will preserve the promises we made to our users in the past.
Of course, if you want changes to rules to impact all users immediately, this approach wouldn’t be so good.
Again, if you’d like to download the complete solution, the code is here, and if it wasn’t already obvious, this code makes use of NServiceBus quite heavily.
When the recording comes online, I’ll update this post with a link as well as do another blog post.
Hope you like it.