How to implement DSL functionality efficiently?

The last post on the extensibility in xLim series turned out to be information-tense, and yet far from being complete. I’ll continue to concentrate on the topic.

Take a look at this DSL code from Ayende’s blog:

operation "/account/login"

if Principal.IsInRole("Administrators"):
   Allow("Administrators can always log in")
   return

if date.Now.Hour < 9 or date.Now.Hour > 17:
   Deny("Cannot log in outside of business hours, 09:00 - 17:00")

operation "/order/aprrove"
if Principal.IsInRole("Managers"):
   Allow("Managers can always approve orders")
   return

if Entity.TotalCost >= 10_000:
   Deny("Only managers can approve orders of more than 10,000")

Allow("All users can approve orders less than 10,000")

That looks to be concise, nice and flexible, is not it?

Well, .NET can do that too. Take a look at this code now:

public sealed class OrderSecurity : FastSecurityBase
{
   public OrderSecurity(IPrincipal principal, Order entity)
   {
      if (principal.IsInRole("Managers"))
         Allow("Managers can always approve orders");

      else if (entity.TotalCost >= 10000)
         Deny("Only managers can approve orders of more than 10,000");

      else Allow("All users can approve orders less than 10,000");
   }
}

public sealed class LogonSecurity : FastSecurityBase
{
   public LogonSecurity(IPrincipal principal)
   {
      if (principal.IsInRole("Administrators"))
         Allow("Administrators can always log in");
      else if ((DateTime.Now.Hour < 9) || (DateTime.Now.Hour > 17))
         Deny("Cannot log in outside of business hours, 09:00 - 17:00");
   }
}

Now, here are the points why the last approach is better:

  • Flexibility - you are not limited to the DSL-supported features and can do whatever could be done with .NET
  • Clarity - you can read the code
  • Maintenability - you do not need DSL parser to write and maintain, you can write unit tests for this code.
  • Simplicity - you do not have some cryptic syntax to document, there’s IntelliSense, R# support and compile-tme checking.

And the most important reason is that the last approach is efficient. I would not dare to implement DSL in a project that has severe constraints on time, resources and budget (quality is fixed and high). And the last approach… well, it took me just 30 minutes to come up with this sample that plugs into the code and could be used.

Here’s the “framework” code that’s needed for this security “DSL”

public sealed class Ruling
{
   public string Reason { get; private set; }
   public bool IsAllowed { get; private set; }

   public Ruling(bool isAllowed, string reason)
   {
      IsAllowed = isAllowed;
      Reason = reason;
   }
}

public interface ISecurityPolicy
{
   Ruling GetRuling();
}

public abstract class FastSecurityBase : ISecurityPolicy
{
   private Ruling _ruling = new Ruling(false,"Denied by default");

   protected void Allow(string message)
   {
      _ruling = new Ruling(true, message);
   }

   protected void Deny(string message)
   {
      _ruling = new Ruling(false, message);
   }

   public Ruling GetRuling()
   {
      return _ruling;
   }
}

With that you just need to load the policy assemblies into the IoC (batch registration would do it, although I prefer modular approach) and then just ask for for the specific policy like this:

_loadContext = ViewContext.CreateInnerContainer();
_loadContext.Register(order);
ISecurityPolicy policy = _loadContext.ResolveByName<ISecurityPolicy>(policyName);
Ruling ruling = policy.GetRuling();

“_loadContext” and “ViewContext.CreateInnerContainer();” - these names come from the last sample snippet in post on xLim extensibility (you could ignore these and just use any IoC container that provides the proper support for scopes).

Leave a Reply