Archivo por meses: enero 2014

Business Rule Engine en Windows Azure #windowsazure

Introducción

Trabajando con un partner mexicano de Guadalajara me encontré con el siguiente requerimiento:

Necesito crear flujos de trabajo que evalúen reglas de negocio que estén fuera del código. Además necesito que estos flujos de trabajo se expongan como servicios para que diferentes sistemas los llamen.

Para los que desarrollan en .NET desde la version 3.5 del framework tienen disponible Workflow Fundation(WF). En este artículo, vamos a desarrollar una solución muy simple para ambos requerimientos utilizando WF4.  La idea es utilizar las capacidades que tiene WF para trabajar con reglas, una de las partes más interesantes de WF. El escenario a desarrollar se muestra en el diagrama 1.

00

La solución final se basa en construir un proyecto Workflow Services, que es la publicación de manera nativa de los WF como servicios usando WCF, los que se publicarán en un Web Role de Windows Azure. Estos workflows pueden evaluar un conjunto de regalas de negocio que están definidas en un archivo XML externos al código. La idea es almacenar el set de regalas de negocio en Windows Azure Blob Storage, de manera que estén disponibles para los Workflow Services. Complementario a lo anterior, vamos a construir un editor y probador de reglas de negocio en una aplicación Windows de consola. Está aplicación nos permite cambiar las reglas en tiempo de ejecución sin necesidad de volver a hacer deploy porque están absolutamente desacopladas del código.

Este articulo está dividido en dos partes, primero vamos a construir una biblioteca que nos facilite el trabajo con reglas y una aplicación de consola para probarla. En la segunda parte vamos a usar la biblioteca desde los Workflows para evaluar reglas en tiempo de ejecución.

Primera parte: Biblioteca y pruebas

Biblioteca de ayuda para usar Reglas de Negocio

Comenzamos por crear una biblioteca de ayuda para utilizar las reglas de negocio de WF desde la aplicación de consola para administración   y  evaluación desde los Wrokflow services en tiempo de ejecución.  Esta biblioteca nos simplifica todo el manejo de las reglas de negocio y está compuesta por la clase BREClient  la que se muestra en el siguiente diagrama.

01

Clase BREClient: Edición de reglas

Lo primero que necesitamos hacer es poder cargar o crear RuleSet. Para ello necesitamos utilizar desde nuestra biblioteca el Blob Storage de Windows Azure. Para ello debemos agregar las librerías clientes de Windows Azure Storage, como se muestra en la siguiente imagen.

3

Como segundo paso agregamos los Assemblies de Windows Worflow Fundation, como se muestra en la siguiente imagen.

4

El método LoadRuleSet de la clase BREClient nos permite cargar o crear un RuleSet desde un archivo XML que se almacena en un contenedor específico del Blob Storage.

///
/// Load or create RulseSet from XMl file RuleSetXmlFile stoarge in RuleSetContainer
        ///
        ///Container´s name
        ///Xml File´s name
        ///
        private RuleSet LoadRuleSet(string RuleSetContainer,string RuleSetXmlFile)
        {
            RuleSet ruleset;
            XmlTextReader reader;
            try
            {
                CloudBlobContainer container = blobClient.GetContainerReference(RuleSetContainer);
                CloudBlockBlob blockBlob = container.GetBlockBlobReference(RuleSetXmlFile);

                if (BlobExist(blockBlob))
                {
                    //Load RuleSet From XML
                    using (var memoryStream = new MemoryStream())
                    {
                        blockBlob.DownloadToStream(memoryStream);
                        memoryStream.Position = 0;
                        reader = new XmlTextReader(memoryStream);
                        WorkflowMarkupSerializer serializer = new WorkflowMarkupSerializer();
                        object results = serializer.Deserialize(reader);
                        ruleset = (RuleSet)results;
                    }
                }
                else
                {
                    //Create New RuleSet
                    ruleset = new RuleSet();
                }
            }
            catch (Exception X)
            {
                //TODO: manage exception
                throw X;
            }
            return ruleset;
        }

El segundo método que necesitamos en la clase BREClient es  SaveRuleSet, que nos permite persistir el RuleSet en un archivo XML en el Blob Storage.

private void SaveRuleSet(string RuleSetContainer,string RuleSetXmlFile, RuleSet ruleset)
        {
            MemoryStream memoryStream = new MemoryStream();
            XmlTextWriter writer = new XmlTextWriter(memoryStream, null);
            WorkflowMarkupSerializer serializer = new WorkflowMarkupSerializer();
            serializer.Serialize(writer, ruleset);
            try
            {
                CloudBlobContainer container = blobClient.GetContainerReference(RuleSetContainer);
                CloudBlockBlob blockBlob = container.GetBlockBlobReference(RuleSetXmlFile);
                memoryStream.Position = 0;
                blockBlob.UploadFromStream(memoryStream);
            }
            catch (Exception)
            {
                //TODO: manage exception
                throw;
            }
            memoryStream = null;
        }

Con estos dos métodos podemos editar las reglas de negocio utilizando la interfaz Rule Set Editor, como se muestra en la siguiente imagen.

02

Este editor es parte del Windows Workflow Fundation, por lo que solo tenemos que invocarla para editar las reglas y luego al presionar OK podemos guardarlas para ser usadas desde diferentes Workflows. El método EditRuleDialog carga las reglas desde el archivo XML, lo edita y luego actualiza con los cambios realizados.

///
/// Load or create Ruleset, Edit with Rule Set Editor and save the changes in XML file.
///
///Type Class used in Rules
///Xml File Blob Container
///XML File´s name
public void EditRuleDialog(Type TargetType, string RuleSetContainer, string RuleSetXmlFile)
{
   RuleSet ruleset = null;
   //load or creat if not exits
   ruleset = this.LoadRuleSet(RuleSetContainer, RuleSetXmlFile);
            RuleSetDialog dialog = new RuleSetDialog(TargetType, null, ruleset);
   DialogResult result = dialog.ShowDialog();
   if (result == DialogResult.OK)
     {
        SaveRuleSet(RuleSetContainer, RuleSetXmlFile, dialog.RuleSet);
      }
}

Con esta implementación se puede editar reglas de negocio absolutamente desacopladas del código de los Workflows. El siguiente código permite editar reglas utilizando la clase BREClient.

static void EDitar()
{
  //conn is Azure Storage´s connection string
   BREClient RuleClient = new BREClient(conn);
  //Target Class to use in Rules
  Person mySubjectEval = new Person();
  string RuleContainer = "demo";
  string RuleXmlFile = "PersonCredit.xml";
  RuleClient.EditRuleDialog(typeof(Person), RuleContainer, RuleXmlFile);
}

Clase BREClient: Evaluación de reglas

Una vez creadas las reglas de negocio y almacenadas en el storage de azure podemos utilizarlas desde diferentes clientes, sin importar si son Workflows o aplicaciones .NET de otra naturaleza, ya que la clase BREClient nos dará acceso al Framework de reglas.

La evaluación de las reglas se hace utilizando  la clase BREClient con el método público RuleExecute donde se carga el Rulset y se evalúan las reglas. El método privado hace la evaluación realmente, mientras que el público le provee el contexto para la ejecución.

///
/// Execute Rule evaluation
///
///Object used in rules
///RuleSet
private void RuleExecute( Object Target, RuleSet ruleset)
{
 try
    {
      RuleValidation validation = new RuleValidation(Target.GetType(), null);
      RuleExecution engine = new RuleExecution(validation, Target);
      ruleset.Execute(engine);
     }
catch (Exception X)
      {
        System.Diagnostics.Trace.WriteLine(X.Message, "Error");
        throw X;
       }
}
///
/// Execute rule evaluation with or without rulset´s cache
///
///Rulset´s blob container
///Rulset´s Xml file
///Object used in rules
///Use Rulset Cache
public void RuleExecute(string RuleSetContainer, string RuleSetXmlFile, Object Target, bool UseRuleCache)
{
   RuleSet ruleset = null;
   if (UseRuleCache)
   {
     string key = string.Format("{0}/{1}", RuleSetContainer, RuleSetXmlFile);
     if (!RuleSetCache.ContainsKey(key))
     {
        ruleset = LoadRuleSet(RuleSetContainer, RuleSetXmlFile);
        RuleSetCache.Add(key, ruleset);
      }
      ruleset = (RuleSet)RuleSetCache[key];
      this.RuleExecute(Target, ruleset);
    }
    else
    {
      ruleset = LoadRuleSet(RuleSetContainer, RuleSetXmlFile);
      this.RuleExecute(Target, ruleset);
   }
}

Para probar la evaluación de las reglamos podemos usar el siguiente método donde se crea el objeto subjectX que es de la clase Person que se utiliza en la regla.

static void Test()
{
  Person subjectX= new Person();
  BREClient RuleClient = new BREClient(conn);
  DateTime timezero = DateTime.Now;
  for (int i = 0; i < 10; i++)
  {
    subjectX.Age = i;
    RuleClient.RuleExecute(RuleContainer, RuleXmlFile, subjectX,false);
    Console.WriteLine(string.Format("Age= {0} rule response {1}", i, subjectX.RuleEval));
   }
   DateTime timeOne = DateTime.Now;
   Console.WriteLine("Total time: " + (timeOne - timezero).TotalSeconds.ToString());
   Console.WriteLine("");
}

La regla que estamos usando es la siguiente: si la propiedad Age es menor que 5, la propiedad RuleEval de la clase Person es verdadero sino falso. Esto se muestra en la siguiente imagen.

02

Al ejecutar la prueba obtenemos la siguiente respuesta, que obedece a la regla definida.

03

El tiempo de ejecución es alrededor de 0.95 segundos por llamada. Esto ocurre porque en cada evaluación accede al archivo XML que está en el Blob Storage. En el caso en que las reglas no cambian en tiempo real podemos optimizar los tiempos utilizando un cache local para los RulSet. El siguiente método optimiza las llamada a las mismas reglas pero usando cache.

static void TestCache()
{
  Person subjectX = new Person();
  BREClient RuleClient = new BREClient(conn);
  DateTime timezero = DateTime.Now;
  for (int i = 0; i < 10; i++)
    {
      subjectX.Age = i;
      RuleClient.RuleExecute(RuleContainer, RuleXmlFile, subjectX,true);
      Console.WriteLine(string.Format("Age= {0} rule response {1}", i, subjectX.RuleEval));
     }
     DateTime timeOne = DateTime.Now;
     Console.WriteLine("Total time: " + (timeOne - timezero).TotalSeconds.ToString());
     Console.WriteLine("");
}

Las respuestas a esta segunda llamada toman sólo 0,1,83 segundos por regla porque sólo hace una lectura al blob storage y alamacena en un Hashtable el objeto RuleSet por lo que todas las otras evaluaciones la realiza con el objeto ya guardado en memoria.

04

Segunda parte: Workflows como Servicios

En esta segunda parte vamos a desarrollar el proyecto de Workflow Services, donde implementaremos los Workflows que utilizaran la clase BREClient para evaluar las reglas de negocio alacenadas en el archivo XML en Azure Blob Storage.

Workflow EvalPerson

El primer workflow a desarrollar es la evaluación de un sujeto basado en la clase Person. El servicio recibe como parámetro una instancia de Person y lo regresa al cliente después de hacer la evaluación como se muestra en la siguiente imagen.

05

El mensaje de entrada se llama data y el del tipo HelperBRE.SampleSubject.Person para poder aplicarle las mismas reglas que utilizamos en la primera parte del artículo, con la aplicación de consola que crea, edita y graba reglas de negocio. El WSDL que expone el servicio se muestra en la siguiente imagen.

06

Ahora, si miramos el contenido de la actividad Flow: Rule Evaluation podemos ver que está compuesta por una actividad del tipo InvokeMethod llamada “Rule Eval”, Decision y Assign llamada “SalaryPlus”. Estas actividades y sus conectores se muestran en la siguiente imagen. Además de las actividades, se deben declarar las variables que se utilizaran en el workflow, las que aparecen en la tabla de variables.

07

La actividad “Rule Eval” requiere que se declare el método que se llamará y también que parámetros se utilizarán. Aquí es donde utilizamos el objeto BREClientWF4 de la clase BRClient para invocar la evaluación de las reglas de la misma forma que utilizamos en la primera parte del artículo. Como parámetros de la llamada usamos las variables ya definidas RuleContainer, RuleXmlFile, data y el valor true. La configuración de “Rule Eval” se muestra en la siguiente imagen.

08

Una vez realizada la evaluación de las reglas podemos tomar una decisión basados en la propiedad data.RuleEval, valor que fue fijado en la evaluación de la regla de negocio. Si el valor es verdadero, vamos a modificar el valor de la propiedad data.Salary aumentándolo en 10%.

Por último, después de ejecutar el Flowchart respondemos al cliente que nos ha llamado, regresándole el mismo objeto data pero con los valores modificados en base a la evaluación de las reglas y la asignación de valor en el workflow.

Para probar este workflow ejecutamos el proyecto, que es un Web Role, y podemos utilizar el programa de pruebas WCF Test Cient. Como primera prueba llamamos al Workflows con los valores 0, False y 100 y nos responde 0, True y 100. Esto es porque la evaluacion de la regla de negocio dio True entonces el workflow le aumentó 10% el salario al sujeto evaluado como se muestra en la siguiente imagen.

09

Como segunda prueba ejecutamos el workflow con los valores 10, False y 100 y nos regresa 10, False y  100. Esto es porque la regla de negocio respondió False y no tuvo entonces aumento en el workflow como se muestra en la siguiente imagen.

010

Con estas dos pruebas comprobamos que nuestro Workflow con invocación a las reglas de negocio funciona perfectamente por lo que podemos crear un cliente que nos permite utilizarlo. El siguiente código, muestra como iniciar el Workflow utilizando código.  La idea de este método de prueba es que hace 10 evaluaciones y nos muestra su resultado por pantalla, cada evaluación es una llamada al servicio que expone el Workflow.

static void TestWF4WFC()
{
  Person subjectX = new Person();
  srvEvalPersona.ServiceClient proxy = new srvEvalPersona.ServiceClient();
  DateTime timezero = DateTime.Now;
  for (int i = 0; i < 10; i++)
    {
     subjectX.Age = i;
     subjectX.Salary = 100;
     proxy.EvalPersona(ref subjectX);
     Console.WriteLine(string.Format("Age= {0} rule response {1} and Salary={2}", i,subjectX.RuleEval,subjectX.Salary));
     }
     DateTime timeOne = DateTime.Now;
     Console.WriteLine("Total time: " + (timeOne - timezero).TotalSeconds.ToString());
     Console.WriteLine("");
}

La respuesta a cada llamada se muestra en la siguiente imagen, los resultados son coherente con la regla definida. Ahora, el tiempo de ejecución de las 10 evaluaciones toma 10.49 segundo porque estamos haciendo 10 llamadas al servicio y cada llamada a su vez carga el RulSet desde el storage.

011

Podemos optimizar esto, construyendo un Workflow que haga la evaluación de varias personas en una sola llamada y usar RuleSet Cache para que sea lo más rápido posible la evaluación. Este nuevo workflow tiene la siguiente estructura: recepción de la petición, ciclo ForEach donde se evalúa cada persona y respuesta al cliente como se muestra en la siguiente imagen.

A diferencia del primer workflow, este recibe como mensaje un objeto del tipo Persons[], es decir un arreglo de personas a evaluar.

012

El archivo WSDL que describe a este nuevo Workflow se muestra en la siguiente imagen.

013

A diferencia del primer workfow, aquí tenemos que evaluar a todas las personas que vienen en la llamada por lo que utilizamos la actividad ForEach, donde cada persona a evaluar es asignada al objeto subject. Luego en cada iteración se hace el llamado a RuleExecute usando el objeto BREClientWF4 del tipo BREClient y aplicando o no el cambio de salario dependiendo de resultado de la evaluación de la regla de negocio, como se muestra en la siguiente imagen.

014

Para poder probar este nuevo workflow, utilizamos el código que se muestra a continuación. En este procedimiento creamos un arreglo de 10 personas y les asígnanos el mismo salario pero diferente edad a casa uno.

static void TestWF4WFC_Bacth()
{
  Person[] subjectXs = new Person[10];
  for (int i = 0; i < 10; i++)
  {
     subjectXs[i] = new Person();
     subjectXs[i].Age = i;
     subjectXs[i].Salary = 100;
  }
  srvEvalPersonaBatch.ServiceClient proxy = new srvEvalPersonaBatch.ServiceClient();
  DateTime timezero = DateTime.Now;
  //execute
  proxy.EvalPersonaBatch(ref subjectXs);
  foreach (var subject in subjectXs)
  {
    Console.WriteLine(string.Format("Age= {0} rule response {1} and Salary={2}", subject.Age, subject.RuleEval, subject.Salary));
  }
  DateTime timeOne = DateTime.Now;
  Console.WriteLine("Total time: " + (timeOne - timezero).TotalSeconds.ToString());
  Console.WriteLine("");
}

Luego hacemos solo una llamada al servicio y obtenemos la respuesta. Con la respuesta recorremos el arreglo e imprimimos el resultado de la evaluación de cada cliente. En la siguiente imagen podemos ver el resultado de ejecutar la prueba obteniendo resultados coherentes con las regla de negocio y en un tiempo de 0.069 segundos por regla.

015

Conclusiones

En este artículo hemos visto cómo podemos utilizar el motor de reglas de Windows Workflow Fundation desde una aplicación de consola. Esto lo hemos hecho utilizando una clase de apoyo llamada BREClient. Un punto importante a tener en cuenta es el uso de cache local para el RuleSet ya que leer en cada evaluación las reglas desde el archivo XML almacenado en Blob Storage hace muy lento las evaluaciones.

En la segunda parte del artículo, utilizando BREClient desde dos workflow diferentes. El primero es una llamada para evaluar a una persona y el segundo para evaluar a grupos de personas, viendo claramente la optimización de la evaluación al hacer solo una llamada por grupo.

Esta es una solución muy simple para el problema propuesto por el partner, pero muy efectiva. Ademas es una solución estándar por lo cual los costos de desarrollo y mantención son más bajos que en un BRE custom.

El código fuente de este artículo está en GIT, en el siguiente link.

016