Showing posts with label C#. Show all posts
Showing posts with label C#. Show all posts

Monday, November 11, 2013

Display MongoDB Data Using ASP.NET MVC

In last post I presented a logging server solution for the mobile platform using node.js and MongoDB. Today I will build a ASP.NET MVC application to display the logged data stored in MongoDB.

First download MongoDB latest driver from github. There're a few download options out there. I just used the msi installer, and two key dlls, MongoDB.Driver.dll and MongoDB.Bson.dll, were installed in my C:\Program Files (x86)\MongoDB\CSharpDriver 1.8.3\ folder. Then we are ready to create a ASP.NET MVC 3 or MVC 4 project using basic template in Visual Studio 2010, and add MongoDB.driver.dll and MongoDB.Bson.dll to the project References.

The mongoDB database can be running inside the same machine or in a separate machine. The only difference is the connection string, which follows the format of "mongodb://[username:password@]host1[:port1][,host2[:port2],...[,hostN[:portN]". In my example here I have one MongoDB instance running locally so my connection string is "mongodb://localhost:27017".

Next we create a log item model and a repository to interact with MongoDB with three actions available: search logs within a time span, get original log detail by its ID, and delete a log by its ID (for simplicity reason exception handling is skipped):

using System;
using System.Collections.Generic;
using System.Linq;
 
using MongoDB.Driver;
using MongoDB.Bson;
using MongoDB.Driver.Builders;
 
namespace LogsReporting
{
    public class LogItem
    {
        public ObjectId ID { get; set; }
        public DateTime Date { get; set; }
        public string OS { get; set; }
        public string Version { get; set; }
        public string Level { get; set; }
        public string Manufacturer { get; set; }
        public string Model { get; set; }
        public string ScreenSize { get; set; }
        public string Language { get; set; }
        public string Orientation { get; set; }
        public string Timezone { get; set; }
        public string Message { get; set; }
    }
 
    public interface ILogsRepository 
    {
        string GetOriginalLog(string id);
        void DeleteLog(string id);
        IEnumerable<LogItem> GetLogItems(DateTime from, DateTime to);
    }
 
    public class LogsRepository : ILogsRepository
    {
        private MongoCollection<BsonDocument> _collection; 
        public LogsRepository()
        {
            string connectionString = "mongodb://localhost:27017";
            MongoServer server = new MongoClient(connectionString).GetServer();
            MongoDatabase db = server.GetDatabase("Data");
            _collection = db.GetCollection("Logs");
        }
 
        public string GetOriginalLog(string id)
        {
            ObjectId bsonId;
            if (ObjectId.TryParse(id, out bsonId))
            {
                var item = _collection.FindOneById(bsonId);
                if (item != null)
                    return item.ToString();
            }
            return string.Empty;
        }
 
        public void DeleteLog(string id)
        {
            ObjectId bsonId;
            if (ObjectId.TryParse(id, out bsonId))
            {
                _collection.Remove(Query.EQ("_id", bsonId));
            }
        }
 
        public IEnumerable<LogItem> GetLogItems(DateTime from, DateTime to)
        {
            List<LogItem> logs = new List<LogItem>();
            var docs = _collection.Find(Query.And(Query.GTE("time", from), Query.LTE("time", to)))
                .SetSortOrder(SortBy.Descending("time"));
            foreach (var doc in docs)
            {
                LogItem log = new LogItem()
                {
                    ID = doc.GetValue("_id").AsObjectId,
                    Date = TimeZone.CurrentTimeZone.ToLocalTime(doc.GetValue("time").ToUniversalTime()),
                    OS = doc.GetValue("os", "").ToString(),
                    Version = doc.GetValue("version", "").ToString(),
                    Manufacturer = doc.GetValue("manufacturer", "").ToString(),
                    Model = doc.GetValue("model", "").ToString(),
                    Language = doc.GetValue("lang", "").ToString(),
                    ScreenSize = doc.GetValue("screen", "").ToString(),
                    Orientation = doc.GetValue("orientation", "").ToString(),
                    Timezone = doc.GetValue("timezone", "").ToString(),
                    Level = doc.GetValue("level", "").ToString(),
                    Message = doc.GetValue("log", "").ToString()
                };
                if (!string.IsNullOrEmpty(log.Message) && log.Message.Length > 40)
                    log.Message = log.Message.Substring(0, 40) + " ...";
 
                logs.Add(log);
            }
            return logs;
        }
    }
}
MongoDB is schemaless, and can store arbitrary format of data. For convenience let's assume the logged data has following fields: time, os, version, manufacturer, model, language, screen-size, orientation, time-zone, log-level, and log message (refer to Windows 8 logging & reporting). Only "time" field (time stamp) is mandatory here so we could safely filter log entries by time.

The controller provides three actions: show recent logs, show the detail of a given log and delete a log. For demo purpose we only show data logged in the past week:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
 
namespace LogsReporting
{
    public class LogsController : Controller
    {
        private static readonly ILogsRepository _logs = new LogsRepository();
 
        public ActionResult Index()
        {
            var logs = _logs.GetLogItems(DateTime.Now.AddDays(-7), DateTime.Now);
            return View(logs);
        }
 
        public string Detail(string id)
        {
            var originalLog = _logs.GetOriginalLog(id);
            return originalLog;
        }
 
        public ActionResult Delete(string id)
        {
            _logs.DeleteLog(id);
            return RedirectToAction("Index");
        }
    }
}
The presentation layer is pretty straightforward: a grid view shows the logs' info with "Detail" and "Delete" links. Clicking "Detail" link will open up a new window to display the raw log message. A confirmation popup prompts for a "Delete" action:
@model IEnumerable<LogsReporting.LogItem>
 
<script type="text/javascript">
    function showDetailPopup(id) {
        var url = '@Url.Action("Detail")' + '/' + id;
        window.open(url, "detailWindow", 'width=600px,height=400px');
    }
</script> 
 
<h2 style="text-align: center;">Logs Reporting</h2>
 
<table cellpadding="5px">
    <tr>
        <th>@Html.DisplayNameFor(model => model.Date)</th>
        <th>@Html.DisplayNameFor(model => model.OS)</th>
        <th>@Html.DisplayNameFor(model => model.Version)</th>
        <th>@Html.DisplayNameFor(model => model.Manufacturer)</th>
        <th>@Html.DisplayNameFor(model => model.Model)</th>
        <th>@Html.DisplayNameFor(model => model.ScreenSize)</th>
        <th>@Html.DisplayNameFor(model => model.Language)</th>
        <th>@Html.DisplayNameFor(model => model.Orientation)</th>
        <th>@Html.DisplayNameFor(model => model.Timezone)</th>
        <th>@Html.DisplayNameFor(model => model.Level)</th>
        <th>@Html.DisplayNameFor(model => model.Message)</th>
        <th></th>
    </tr>
@{int i = 0;}
@foreach (var item in Model) {
    <tr style='font-size: 9pt; @(i++%2==0 ? "background-color: #bbbbbb" : "")'> 
        <td>@Html.DisplayFor(modelItem => item.Date)</td>
        <td>@Html.DisplayFor(modelItem => item.OS)</td>
        <td>@Html.DisplayFor(modelItem => item.Version)</td>
        <td>@Html.DisplayFor(modelItem => item.Manufacturer)</td>
        <td>@Html.DisplayFor(modelItem => item.Model)</td>
        <td>@Html.DisplayFor(modelItem => item.ScreenSize)</td>
        <td>@Html.DisplayFor(modelItem => item.Language)</td>
        <td>@Html.DisplayFor(modelItem => item.Orientation)</td>
        <td>@Html.DisplayFor(modelItem => item.Timezone)</td>
        <td>@Html.DisplayFor(modelItem => item.Level)</td>
        <td>@Html.DisplayFor(modelItem => item.Message)</td>
        <td>
            <a href="#" onclick="showDetailPopup('@item.ID');">Details</a> |
            @Html.ActionLink("Delete", "Delete", new { id = item.ID }, 
                new { onclick = "return confirm('Are you sure to delete this log entry?');" })
        </td>
    </tr>
}
</table>

Following screen-shot shows how the view looks like:

Updated on Nov. 12 Just for fun I created a traditional ASP.NET web form .aspx page to show the exact same view:

<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="LogReporting.aspx.cs" Inherits="LogsReporting.LogReporting" %>
 
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
 
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
    <title>Logs Reporting</title>
    <style type="text/css">
        h2 {text-align: center;}
        table {text-align: left; padding: 5px;}
        table td, .detail {font-size: 9pt; padding: 5px;}
    </style>
    <script src="Scripts/jquery-1.7.1.min.js" type="text/javascript"></script>
    <script type="text/javascript">
        $(document).ready(function () {
            $("tr:odd").css("background-color", "#bbbbbb");
        });
    </script>
</head>
<body>
    <form id="form1" runat="server">
    <div>
        <asp:Panel ID="gridPanel" runat="server">
            <h2>Logs Reporting</h2>
            <asp:GridView ID="gvLogs" runat="server" AutoGenerateColumns="False" GridLines="None"
                OnRowDataBound="gvLogs_RowDataBound" EmptyDataText="No logs available">
                <Columns>
                    <asp:BoundField DataField="Date" HeaderText="Date"></asp:BoundField>
                    <asp:BoundField DataField="OS" HeaderText="OS"></asp:BoundField>
                    <asp:BoundField DataField="Version" HeaderText="Version"></asp:BoundField>
                    <asp:BoundField DataField="Language" HeaderText="Language"></asp:BoundField>
                    <asp:BoundField DataField="Manufacturer" HeaderText="Manufacturer"></asp:BoundField>
                    <asp:BoundField DataField="Model" HeaderText="OS"></asp:BoundField>
                    <asp:BoundField DataField="ScreenSize" HeaderText="Version"></asp:BoundField>
                    <asp:BoundField DataField="Level" HeaderText="Level"></asp:BoundField>
                    <asp:BoundField DataField="Message" HeaderText="Message"></asp:BoundField>
                    <asp:TemplateField>
                        <ItemTemplate>
                            <asp:LinkButton ID="lbtDetail" runat="server" Text="Detail"></asp:LinkButton>
                            <asp:LinkButton ID="lbtDelete" runat="server" Text="Delete" OnClick="lbtDelete_Click"
                                OnClientClick="return confirm('Are you sure to delete this log entry?');"></asp:LinkButton>
                        </ItemTemplate>
                    </asp:TemplateField>
                </Columns>
            </asp:GridView>
        </asp:Panel>
 
        <asp:Panel ID="detailPanel" runat="server" CssClass="detail">
            <asp:Literal ID="logDetail" runat="server"></asp:Literal>
        </asp:Panel>
    </div>
    </form>
</body>
</html>
The code behind:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Configuration;
 
using MongoDB.Driver;
using MongoDB.Bson;
using MongoDB.Driver.Builders;
 
namespace LogsReporting
{
    public partial class LogReporting : System.Web.UI.Page
    {
        private ILogsRepository _repo = new LogsRepository();
 
        protected void Page_Load(object sender, EventArgs e)
        {
            bool showDetail = !string.IsNullOrEmpty(Request.QueryString["id"]);
            detailPanel.Visible = showDetail;
            gridPanel.Visible = !showDetail;
 
            if (showDetail)
            {
                logDetail.Text = _repo.GetOriginalLog(Request.QueryString["id"]);
            } 
            else if (!IsPostBack)
            {
                var dataSource = _repo.GetLogItems(DateTime.Today.AddDays(-7), DateTime.Now);
                bindDataSource(dataSource);
            } 
        }
 
        private void bindDataSource(IEnumerable<LogItem> logs)
        {
            gvLogs.DataSource = logs;
            gvLogs.DataBind();
        }
 
        protected void gvLogs_RowDataBound(object sender, GridViewRowEventArgs e)
        {
            if (e.Row.RowType == DataControlRowType.DataRow)
            {
                LogItem item = e.Row.DataItem as LogItem;
                LinkButton lbtDetail = e.Row.FindControl("lbtDetail") as LinkButton;
                LinkButton lbtDelete = e.Row.FindControl("lbtDelete") as LinkButton;
                string detailUrl = string.Format("{0}?id={1}", Request.Url.AbsolutePath, item.ID);
                lbtDetail.Attributes.Add("onClick", "window.open('" + detailUrl + "', '', 'width=600,height=400')");
                lbtDelete.CommandArgument = item.ID.ToString();
              }
        }
 
        protected void lbtDelete_Click(object sender, EventArgs e)
        {
            var logID = Convert.ToString(((LinkButton)sender).CommandArgument);
            _repo.DeleteLog(logID);
            var dataSource = _repo.GetLogItems(DateTime.Today.AddDays(-7), DateTime.Now);
            bindDataSource(dataSource);
        }
    }
}

Friday, May 24, 2013

Adding Custom SOAP Header in .NET

An extra layer has been added to our back-end services (implemented by Java). Now all requests to those services require a set of custom SOAP headers. Existing client apps that consuming those services need to be updated for such change. A colleague asked me what's the best way to do the task in .NET and SoapExtension is my answer. SoapExtension has been available since .NET 1.1. There're many resources online about it and following code is my simple implementation:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

using System.IO;
using System.Web.Services.Protocols;

namespace SoapHeaderTest
{
    public class TestSoapExtension : SoapExtension
    {
        Stream oldStream;
        Stream newStream;

        public override Stream ChainStream(Stream stream)
        {
            oldStream = stream;
            newStream = new MemoryStream();
            return newStream;
        }

        public override object GetInitializer(LogicalMethodInfo methodInfo, SoapExtensionAttribute attribute)
        {
            return null;
        }

        public override object GetInitializer(Type WebServiceType)
        {
            return null;
        }

        public override void Initialize(object initializer)
        {
        }

        public override void ProcessMessage(SoapMessage message)
        {
            switch (message.Stage)
            {
                case SoapMessageStage.BeforeSerialize:
                    break;
                case SoapMessageStage.AfterSerialize:
                    AddSoapHeader(); // Add SOAP header before the request is sent
                    break;

                case SoapMessageStage.BeforeDeserialize:
                    Copy(oldStream, newStream); // No modification on response, simply copy over
                    newStream.Position = 0; // Rewind the stream
                    break;
                case SoapMessageStage.AfterDeserialize:
                    break;
                default:
                    throw new Exception("invalid stage");
            }
        }

        // Add custom SOAP header
        private void AddSoapHeader()
        {
            string headerTemplate =
                @"<soap:Header>
                    <h:myHeader xmlns:h=""http://company.com/webservice/soapheader/"">
                        <h:version>{0}</h:version>
                        <h:code>{1}</h:code>
                        <h:proxy>{2}</h:proxy>
                    </h:myHeader>
                </soap:Header>";
            //TODO: populate header values here
            string soapHeader = string.Format(headerTemplate, 1, 2, 3);

            // Get original SOAP message
            newStream.Position = 0;
            string soapMessage = (new StreamReader(newStream)).ReadToEnd();

            // Create a string with header data inserted to the original SOAP Xml before the <soap:Body> tag
            string newSoapMessage = soapMessage.Insert(soapMessage.IndexOf("<soap:Body"), soapHeader);

            // Write new SOAP message to the new stream
            newStream.Position = 0;
            TextWriter writer = new StreamWriter(newStream);
            writer.Write(newSoapMessage);
            writer.Flush();

            // Copy the content from the new stream to the old stream
            newStream.Position = 0;
            Copy(newStream, oldStream);
        }

        private void Copy(Stream from, Stream to)
        {
            TextReader reader = new StreamReader(from);
            TextWriter writer = new StreamWriter(to);
            writer.WriteLine(reader.ReadToEnd());
            writer.Flush();
        }
    }
}
To use the SoapExtension, simply register it in web.config for web applications, or app.config for Form, class libraries and console apps:
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
    <system.web>
      <webServices>
        <soapExtensionTypes>
          <add type="SoapHeaderTest.TestSoapExtension, SoapHeaderTest" priority="1" group="Low"/>
        </soapExtensionTypes>
      </webServices>
    </system.web>
</configuration> 
After registration in web.config or app.config, all requests and responses will go through the custom SoapExtension pipe line.

Sunday, July 22, 2012

Including Product Tags in nopCommerce Import/Export

Product tags are not included in Excel import/export by default in nopCommerce solution. Product tags are many-to-many relation to products. Following code illustrates how to import/export product tags in nopCommerce (added to Nop.Services.ExportImport in service layer):
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Nop.Core.Domain.Catalog;
using Nop.Services.Catalog;
using OfficeOpenXml;

namespace Nop.Services.ExportImport
{
    /// <summary>
    /// Import/Export helper class
    /// </summary>
    public class ImportExportHelper
    {
        public static string[] AddCustomColumns(string[] properties)
        {
            string[] newProperties = new string[] 
            {       
                "ProductTags",      
            };
            string[] returnProperties = properties.Union(newProperties).ToArray();
            return returnProperties;
        }

        public static string GetProductTagsString(Product product)
        {
            var result = new StringBuilder();
            for (int i = 0; i < product.ProductTags.Count; i++)
            {
                var pt = product.ProductTags.ToList()[i];
                result.Append(pt.Name);
                if (i != product.ProductTags.Count - 1)
                    result.Append(", ");
            }
            return result.ToString();
        }

        public static string[] ParseProductTags(string productTags)
        {
            var result = new List<string>();
            if (!String.IsNullOrWhiteSpace(productTags))
            {
                string[] values = productTags.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
                foreach (string val1 in values)
                    if (!String.IsNullOrEmpty(val1.Trim()))
                        result.Add(val1.Trim());
            }
            return result.ToArray();
        }

        public static void SaveProductTags(IProductService _productService, 
            IProductTagService _productTagService, Product product, string[] productTags)
        {
            if (product == null)
                throw new ArgumentNullException("product");

            //product tags
            var existingProductTags = product.ProductTags.OrderByDescending(pt => pt.ProductCount).ToList();
            var productTagsToDelete = new List<ProductTag>();
            foreach (var existingProductTag in existingProductTags)
            {
                bool found = false;
                foreach (string newProductTag in productTags)
                {
                    if (existingProductTag.Name.Equals(newProductTag, StringComparison.InvariantCultureIgnoreCase))
                    {
                        found = true;
                        break;
                    }
                }
                if (!found)
                {
                    productTagsToDelete.Add(existingProductTag);
                }
            }
            foreach (var productTag in productTagsToDelete)
            {
                product.ProductTags.Remove(productTag);
                //ensure product is saved before updating totals
                _productService.UpdateProduct(product);
                _productTagService.UpdateProductTagTotals(productTag);
            }
            foreach (string productTagName in productTags)
            {
                ProductTag productTag = null;
                var productTag2 = _productTagService.GetProductTagByName(productTagName);
                if (productTag2 == null)
                {
                    //add new product tag
                    productTag = new ProductTag()
                    {
                        Name = productTagName,
                        ProductCount = 0
                    };
                    _productTagService.InsertProductTag(productTag);
                }
                else
                {
                    productTag = productTag2;
                }
                if (!product.ProductTagExists(productTag.Id))
                {
                    product.ProductTags.Add(productTag);
                    //ensure product is saved before updating totals
                    _productService.UpdateProduct(product);
                }
                //update product tag totals 
                _productTagService.UpdateProductTagTotals(productTag);
            }
        }

    }

    /// <summary>
    /// Partial class of ExportManager
    /// </summary>
    public partial class ExportManager : IExportManager
    {
        public void UpdateCustomProductProperties(ExcelWorksheet worksheet, int row, ref int column, Product product)
        {
            string productTags = ImportExportHelper.GetProductTagsString(product);
            worksheet.Cells[row, column].Value = productTags;
            column++;
        }

    }

    /// <summary>
    /// Partial class of ImportManager
    /// </summary>
    public partial class ImportManager : IImportManager
    {
        public void UpdateCustomProductProperties(ExcelWorksheet worksheet, string[] properties, int row, Product product)
        {
            string productTags = Convert.ToString(worksheet.Cells[row, GetColumnIndex(properties, "ProductTags")].Value);
            ImportExportHelper.SaveProductTags(_productService, _productTagService, product, ImportExportHelper.ParseProductTags(productTags));
        }
    }
}
In order to use above code, the original ExportManager and ImportManager need to be modified a bit. Go to Nop.Services.ExportImport/ExportManger.cs, inside the ExportProductsToXlsx method:

1. Update property definition and add following code after line 532:
properties = ImportExportHelper.AddCustomColumns(properties);
2. Export product tags using custom function and add following code after line 797:
UpdateCustomProductProperties(worksheet, row, ref col, p);

Update Nop.Services.ExportImport/ExportManger.cs constructor and inject IProductTagService:
 public partial class ImportManager : IImportManager
    {
        private readonly IProductService _productService;
        private readonly ICategoryService _categoryService;
        private readonly IManufacturerService _manufacturerService;
        private readonly IPictureService _pictureService;
        private readonly IProductTagService _productTagService;

        public ImportManager(IProductService productService, ICategoryService categoryService,
            IManufacturerService manufacturerService, IPictureService pictureService, 
            IProductTagService productTagService)
        {
            this._productService = productService;
            this._categoryService = categoryService;
            this._manufacturerService = manufacturerService;
            this._pictureService = pictureService;
            this._productTagService = productTagService;
        }
Then update ImportProductsFromXlsx method inside ExportManger.cs:

1. Update property definition; add following code after line 142:
properties = ImportExportHelper.AddCustomColumns(properties);
2. Import product tags using custom function add following code after line 318 AND line 402:
UpdateCustomProductProperties(worksheet, properties, iRow, product);

Rebuild the solution and test out.

Create Custom Scheduled Task in nopCommerce

nopCommerce is a popular open source eCommerce solution built by ASP.NET MVC framework. We can create a custom scheduled task to scan the products periodically and do some business logic there, e.g. sending out email notification if some condition reached for certain products. In order to do that we need to define a record in the ScheduleTask table. Following script inserts a scheduled task record into database that would make nopCommerce invoke 'Nop.Services.Messages.ProductScanService' every 10 minutes:
INSERT INTO ScheduleTask(Name, Seconds, [Type], [Enabled], StopOnError)
 VALUES('ProductScanService schedule', 600, 'Nop.Services.Messages.ProductScanService', 1, 0) 
The ProductScanService class implements ITask interface, and is added to nop.Services.Messages in the service layer (tested in the latest version of 2.60):
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Nop.Services.Tasks;
using Nop.Services.Catalog;
using Nop.Services.Logging;

namespace Nop.Services.Messages
{
    class ProductScanService : ITask
    {
        private readonly IProductService _productService;
        private readonly ILogger _logger;

        /// <summary>
        /// Dependency injection here
        /// </summary>
        public ProductScanService(IProductService productService, ILogger logger)
        {
            this._productService = productService;
            this._logger = logger;
        }

        /// <summary>
        /// Executes the task
        /// </summary>
        public void Execute()
        {
            _logger.Information(string.Format("ProductScanService starts at {0} (UTC {1})", 
                DateTime.Now, DateTime.UtcNow));
            ScanAllProducts();
            _logger.Information(string.Format("ProductScanService ends at {0} (UTC {1})",
                DateTime.Now, DateTime.UtcNow));
        }

        /// <summary>
        /// Do business logic here
        /// </summary>
        private void ScanAllProducts()
        {
            // Do stuff with _productService
        }
    }
}

Wednesday, March 21, 2012

Task-based Asynchronous Programming in .NET 4.5

.NET 4.5 introduces the new "async" and "await" keywords to simplify the asynchronous programming. They are task-based asynchronous model, and are extensively used in the new Metro applications for better responsive UI experience. Following code example illustrate the simple usage of "async" and "await" (run in Visual Studio 11 Beta):
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Net;
using System.Net.Http;
using System.IO;
using System.Diagnostics;

namespace ConsoleApplication1
{
    class Program
    {
        static void Main(string[] args)
        {
            StringBuilder syncContent = new StringBuilder(), asyncContent = new StringBuilder();
            Stopwatch watch = new Stopwatch();
            string testUrl = "http://www.google.com";
            GetWebContent(testUrl); // Warming up network connection

            Console.WriteLine("Sync web request calls start...");
            watch.Start();
            for (int i = 0; i < 100; i++)
            {
                syncContent.Append(GetWebContent(testUrl));
            }
            watch.Stop();
            Console.WriteLine("Sync web calls completed within {0} ms", watch.ElapsedMilliseconds);
            Console.WriteLine("Sync web content lengh: {0}", syncContent.ToString().Length);

            List<Task<string>> tasks = new List<Task<string>>();
            Console.WriteLine("\nSync web request calls start...");
            watch.Restart();
            for (int i = 0; i < 100; i++)
            {
                tasks.Add(GetWebContentAsync(testUrl));
            }
            watch.Stop();

            Console.WriteLine("Async web calls returned within {0} ms", watch.ElapsedMilliseconds);
            watch.Restart();
            Task.WaitAll(tasks.ToArray());
            watch.Stop();
            tasks.ForEach(v => asyncContent.Append(v.Result));
            Console.WriteLine("ASync web calls completed within {0} ms", watch.ElapsedMilliseconds);
            Console.WriteLine("Async web content lengh: {0}", asyncContent.Length);

            Console.Read();
        }

        static async Task<string> GetWebContentAsync(string url)
        {
            var webClient = new WebClient();
            var content = await webClient.DownloadStringTaskAsync(url);
            return content;
        }

        static string GetWebContent(string url)
        {
            var webClient = new WebClient();
            var content = webClient.DownloadString(url);
            return content;
        }
    }
}
The synchronous method is included in the demo cod for comparison purpose. The console app result:



Basically you need to add "async" keyword to the function definition telling compiler they could be asynchronous call(s) inside that method; "async" methods can return Task (no actual return value), Task<T> (return value of T) or void (only used in event handler without any return).

Inside a method marked as "async", you are able to use "await" keyword for a call that promises return or return value. Once "await" keyword is used, compiler will do some magic stuff for you and make that asynchronous invocation happen. Many IO and network related methods in .NET 4.5 have been refactored to support this task-based asynchronous model. Those asynchronous methods, with naming convention of xxxxAsync where xxxx is the method name, are out-of-box and can be used directly with "await" keyword.

This async programming model is much easier to work with comparing to the old .NET asynchronous programming model. In old days, developers need to spend a lot of effort to handle those callback functions such as Beginxxxx/Endxxxx methods and IAsyncResult parameter. That's painful and sometimes it's impossible to do nested async calls and error handling. With the new task-based async model, all those hustles are gone.

You can easily wrap a method as async method using TPL introduced in .NET 4.0. Code snippet below shows one way to achieve this:
        static async Task<double> MyComplicatedHeavyCalculationAsync(double x, double y)
        {
            double value = await Task<double>.Run(() => MyComplicatedHeavyCalculation(x, y));
            return value;
        }

        static double MyComplicatedHeavyCalculation(double x, double y)
        {
            // Do your complicated work ...
            return x + y;
        }
All sound good. But how about the thread context such as updating the UI content? In WPF, Silverlight and Metro apps you are most likely safe to do so since compiler are smart enough to inject the delegate code that could marshall the update back to the UI thread. For ASP.NET things are a bit more tricky. You have thread pool and httpcontext for each request is dynamically generated. Good news is that the new ASP.NET 4.5 will support task-based asynchronous model in certain ways, refer next version ASP.NET white paper for detail.

Thursday, October 13, 2011

Using SharePoint WebProvisioned Event to Update Newly Created Site

I've been working on a SharePoint 2010 Site Definition that includes a bunch of Lists and a default page, and the new site (SPWeb) built by the Site Definition should keep old SharePoint 2007 UI because the users are used to the old looks and feels. A DataFormWebPart on the default page shows recent updates for a few different Lists. The problem is that those ListIDs are hard-coded in side DFWP, but ListIDs will be changed when a new site is created. Some tweaks on DFWP, e.g. changing ListID to ListName, could make it reusable, but that only works for single datasource but won't work for multiple datasources (AggregateDataSource). I tried AllUsersWebPart and View approach to provision the WebPart instance to the default page but without luck.

To resolve the problem I use tokens in the DFWP and replace those tokens with real ListIDs after site is created, as following:
<SharePoint:SPDataSource runat="server" DataSourceMode="List" UseInternalName="true" 
            UseServerDataFormat="true" selectcommand="&lt;View&gt;&lt;/View&gt;">
    <SelectParameters><asp:Parameter Name="ListID" DefaultValue="$LIST_TOKEN:{Project Documents}$"/>
    </SelectParameters>
    <DeleteParameters><asp:Parameter Name="ListID" DefaultValue="$LIST_TOKEN:{Project Documents}$"/>
    </DeleteParameters>
    <UpdateParameters><asp:Parameter Name="ListID" DefaultValue="$LIST_TOKEN:{Project Documents}$"/>
    </UpdateParameters>
    <InsertParameters><asp:Parameter Name="ListID" DefaultValue="$LIST_TOKEN:{Project Documents}$"/>
    </InsertParameters>
</SharePoint:SPDataSource>
In above DFWP configuration the "$LIST_TOKEN:{Project Documents}$" token will be replaced by the List/Library ID of "Project Documents". The other issue is that sometimes some WebParts on the default page are automatically closed after the new site is provisioned for some unknown reason. So there're a few issues I need to fix for the Site Definition:

1. Convert SP2010 site back to SharePoint 2007 UI
2. Change master page and css
3. Change DFWP's list Ids on default page
4. Open closed WebParts if exists

The solution is use SPWebEventReceiver to update site properties and the default page:
using System;
using System.Security.Permissions;
using System.Text.RegularExpressions;
using System.Web.UI.WebControls.WebParts;

using Microsoft.SharePoint;
using Microsoft.SharePoint.WebPartPages;

namespace SiteDefinition
{
    /// <summary>
    /// Web Provision Events
    /// </summary>
    public class INGProjectSiteEventReceiver : SPWebEventReceiver
    {
        /// <summary>
        /// Update a site (SPWeb) when it's created
        /// 1. Convert site back to SharePoint 2007 UI
        /// 2. Update master page and css file
        /// 3. Update DFWP's list Ids on default page
        /// 4. Open closed WebParts if exists
        /// </summary>
        public override void WebProvisioned(SPWebEventProperties properties)
        {
            if (properties.Web.WebTemplateId == 11001) // Only applied to targetted template
            {
                properties.Web.UIVersion = 3;
                properties.Web.MasterUrl = properties.Web.Site.RootWeb.MasterUrl;
                properties.Web.AlternateCssUrl = properties.Web.Site.RootWeb.AlternateCssUrl;
                properties.Web.Update();

                UpdateDefaultPageWebPartListIDs(properties.Web);
                OpenClosedWebParts(properties.Web);
            }

            base.WebProvisioned(properties);   
        }

        /// <summary>
        /// Update default page's DataFormWebParts' ListID property with corresponding List ID (GUID)
        /// </summary>
        private void UpdateDefaultPageWebPartListIDs(SPWeb web)
        {

            SPFile defaultPage = web.RootFolder.Files["Default.aspx"];
            if (defaultPage.Exists)
            {
                System.Text.ASCIIEncoding coding = new System.Text.ASCIIEncoding();
                byte[] byteArrayText = defaultPage.OpenBinary();

                if (byteArrayText.Length > 0)
                {
                    string origHtml = coding.GetString(byteArrayText);
                    string newHtml = ReplaceDataSourceToken(origHtml, web);
                    if (!string.IsNullOrEmpty(newHtml))
                    {
                        byte[] newByteArray = coding.GetBytes(newHtml);
                        defaultPage.SaveBinary(newByteArray);
                    }
                }
            }
        }

        /// <summary>
        /// Pre-defined tokens are used inside default page and we need to replace them here:
        /// e.g. "\$LIST_TOKEN:{Tasks}\$" will be replaced with Tasks' List ID (GUID)
        /// </summary>
        private string ReplaceDataSourceToken(string text, SPWeb web)
        {
            string pattern = @"\$LIST_TOKEN:{.*?}\$";
            Match mc = Regex.Match(text, pattern);
            if (mc.Success)
            {
                while (mc.Success)
                {
                    if (!string.IsNullOrEmpty(mc.Value))
                    {
                        string listName = mc.Value.Substring(13, mc.Length - 15);

                        SPList list = web.Lists.TryGetList(listName);
                        if (list != null)
                            text = string.Format("{0}{1}{2}",  
                                text.Substring(0, mc.Index), list.ID.ToString("B"), text.Substring(mc.Index + mc.Length));
                        else
                            text = string.Format("{0}$LIST_NOT_FOUND:[{1}]{2}",
                                text.Substring(0, mc.Index), listName, text.Substring(mc.Index + mc.Length);
                        
                        mc = Regex.Match(text, pattern);
                    }
                }
            }
            return text;
        }

        /// <summary>
        /// Sometimes WebParts are closed automatically and this method opens all closed webParts in the default page
        /// </summary>
        private void OpenClosedWebParts(SPWeb web)
        {
            SPFile defaultPage = web.RootFolder.Files["Default.aspx"];
            if (defaultPage.Exists)
            {
                using (SPLimitedWebPartManager wpManager = 
                    defaultPage.GetLimitedWebPartManager(PersonalizationScope.Shared))
                {
                    foreach (Microsoft.SharePoint.WebPartPages.WebPart wp in wpManager.WebParts)
                    {
                        if (wp.IsClosed)
                        {
                            wpManager.OpenWebPart(wp);
                            wpManager.SaveChanges(wp);
                        }
                    }                
                }
            }
        }
    }
}

Tuesday, April 26, 2011

Dynamically Invoke Generic Method In .NET


Today I cleaned up a lengthy method in our project which I think it's worthy of a post. Our project has a repository that caches all business objects, we called them entities and the entity list is long. The cache is loaded/refreshed when repository is first accessed or some entities have been changed in the back-end store. The method for loading entities is something like:
void LoadEntitiesCache()
{
    List<EntityType1> entity1List = DataStore.GetAllEntitiesByType<EntityType1>();
    SaveEntitiesToCache(Entity1List);
    List<EntityType2> entity2List = DataStore.GetAllEntitiesByType<EntityType2>();
    SaveEntitiesToCache(Entity2List);
    List<EntityType3> entity3List = DataStore.GetAllEntitiesByType<EntityType3>();
    SaveEntitiesToCache(Entit32List);
    ....
    List<EntityTypeN> entityNList = DataStore.GetAllEntitiesByType<EntityTypeN>();
    SaveEntitiesToCache(EntityNList);
}
The way of my clean-up is something like:
void LoadEntitiesCache()
{
    Type [] allEntityTypes = new Type [] { EntityType1, EntityType2, ..., EntityTypeN };
    foreach(Type entityType in allEntityTypes)
    {
        List<entityType> entityList = DataStore.GetAllEntitiesByType<entityType>();
        SaveEntitiesToCache(entityList);
    }
}
The logic is there but above code won't work because you can't invoke a generic method like that, instead reflection must be used for such usage. Following code example shows how to use reflection to invoke a generic method (entity/cache update logic is not included):
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Reflection;
using System.Collections.ObjectModel;
using System.Collections.Concurrent;

public interface IEntity
{
    string Identifier { get; }
    string Type { get; }
}

public class Book : IEntity
{
    public string ISBN { get; set; }
    public string Category { get; set; }
    public string Identifier { get { return ISBN; } }
    public string Type { get { return Category; } }
}

public class CD : IEntity
{
    public string Name { get; set; }
    public string Category { get; set; }
    public string Identifier { get { return Name; } }
    public string Type { get { return Category; } }
}

public class KeyedEntityCollection<T> : KeyedCollection<string, T> where T : IEntity
{
    protected override string GetKeyForItem(T item)
    {
        return item.Identifier;
    }
}

public class Repository
{
    static ConcurrentDictionary<Type, object> _entitiesCache = new ConcurrentDictionary<Type, object>();

    void LoadEntitiesCache()
    {
        // Load all entities from back-end store and save them in _entitiesCache
        DataSource dataSource = new DataSource();

        Type[] entityTypes = new Type[] { typeof(Book), typeof(CD) };
        MethodInfo methodInfo = typeof(DataSource).GetMethod("GetEntitiesByType");
        foreach (var entityType in entityTypes)
        {
            object _cacheObject = null;
            if (_entitiesCache.TryGetValue(entityType, out _cacheObject) && _cacheObject != null)
                continue;

            try
            {
                // Invoke DataStore.GetEntitiesByType<T>() dynamically
                MethodInfo genericMethod = methodInfo.MakeGenericMethod(entityType);
                IEnumerable dataReturn = genericMethod.Invoke(dataSource, null) as IEnumerable;

                // Create EntityCollection dynamically
                Type[] types = new Type[] { entityType };
                Type entityCollectionType = typeof(KeyedEntityCollection<>);
                Type genericType = entityCollectionType.MakeGenericType(types);
                IList genericCollection = Activator.CreateInstance(genericType) as IList;

                if (dataReturn != null)
                {
                    foreach (var entity in dataReturn)
                    {
                        genericCollection.Add(entity);
                    }
                }
                _entitiesCache.AddOrUpdate(entityType, genericCollection, 
                    (type, existingValue) => { return genericCollection; });
            }
            catch (Exception ex)
            {
                // Log error
                throw;
            }
        }
    }

    public Repository()
    {
        LoadEntitiesCache();
    }

    public KeyedEntityCollection<T> GetEntities<T>() where T : IEntity
    {
        object cachedEntityCollection = null;
        _entitiesCache.TryGetValue(typeof(T), out cachedEntityCollection);
        return cachedEntityCollection as KeyedEntityCollection<T>;
    }

    public T GetEntityById<T>(string identifier) where T : IEntity
    {
        var entityCollection = GetEntities<T>();
        if (entityCollection != null && entityCollection.Contains(identifier))
        {
            return entityCollection[identifier];
        }
        return default(T);
    }
}

internal class DataSource
{
    // Get data from database, or xml files, or whatever data store
    public IEnumerable<T> GetEntitiesByType<T>() where T : IEntity
    {
        // Mock some test data here
        if (typeof(T) == typeof(Book))
        {
            List<Book> bookSource = new List<Book>();
            Enumerable.Range(1, 100).ToList().ForEach(
                i => bookSource.Add(new Book() { ISBN = i.ToString(), Category = "Computer" }));
            return bookSource as IEnumerable<T>;
        }
        else if (typeof(T) == typeof(CD))
        {
            List<CD> cdSource = new List<CD>();
            Enumerable.Range(1, 100).ToList().ForEach(
                i => cdSource.Add(new CD() { Name = i.ToString(), Category = "Music" }));
            return cdSource as IEnumerable<T>;
        }
        else return null;
    }
}

class Program
{
    static void Main(string[] args)
    {
        Repository repo = new Repository(); 
        var books = repo.GetEntities<Book>();           // Get all books
        var secondBook = repo.GetEntityById<Book>("2"); // Get the second book
        var cds = repo.GetEntities<CD>();               // Get all CDs
        var thirdCD = repo.GetEntityById<CD>("3");      // Get the third CD
        Console.Read();
    }
}

Friday, January 28, 2011

Implementing Simple Custom Rules By Code

In my previous two posts Microsoft Workflow Rules Engine was used to execute the business rules. That maybe a bit overkill if there're only a small set of rules to run and the use cases of them are relatively simple. In that case we could implement the rules and control their execution directly. The simple console app below demos the concept of custom rules implementation:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace ConsoleApplication1
{
    class Program
    {
        #region demo domain objects
        public class Product
        {
            public string Name { get; set; }
            public decimal Price { get; set; }
        }

        public class ShoppingCart
        {
            public List<Product> SelectedProducts { get; set; }
            public List<string> CouponCodes { get; set; }
            public decimal TotalAmount { get { return SelectedProducts.Sum(prod => prod.Price); } }
            internal decimal Discount { get; set; }

            public ShoppingCart()
            {
                SelectedProducts = new List<Product>();
                CouponCodes = new List<string>();
                Discount = 0;
            }
        }
        #endregion demo domain objects

        #region promotion objects 
        class CouponPromotion  
        {
            public string Name { get; set; }
            public string PromotionCode { get; set; }
            public decimal Discount { get; set; }
            public DateTime ExpiredOn { get; set; }
        }

        class TotalAmountPromotion // discount if total amount is greater than a number
        {
            public string Name { get; set; }
            public decimal TotalAmount { get; set; }
            public decimal Discount { get; set; }
            public DateTime ExpiredOn { get; set; }
        }
        #endregion promotion object

        #region custom rules interface and implementation
        interface ICustomRule
        {
            void Execute(ShoppingCart cart);
        }

        class CouponPromotionRule : ICustomRule
        {
            private CouponPromotion mCouponPromotion;
            public CouponPromotionRule(CouponPromotion promotion)
            {
                this.mCouponPromotion = promotion;
            }

            public void Execute(ShoppingCart cart)
            {
                if (mCouponPromotion.ExpiredOn >= DateTime.Today && cart.CouponCodes.Contains(mCouponPromotion.PromotionCode))
                {
                    Console.WriteLine(mCouponPromotion.Name + " applied");
                    cart.Discount += mCouponPromotion.Discount;
                }
            }
        }

        class TotalAmountGreaterRule : ICustomRule
        {
            private TotalAmountPromotion mTotalAmountPromotion;
            public TotalAmountGreaterRule(TotalAmountPromotion promotion)
            {
                this.mTotalAmountPromotion = promotion;
            }

            public void Execute(ShoppingCart cart)
            {
                if (mTotalAmountPromotion.ExpiredOn >= DateTime.Today && cart.TotalAmount >= mTotalAmountPromotion.TotalAmount)
                {
                    Console.WriteLine(mTotalAmountPromotion.Name + " applied");
                    cart.Discount += mTotalAmountPromotion.Discount;
                }
            }
        }
        #endregion custom rules interface and implementation

        static void RuleTest(ShoppingCart cart)
        {
            // Mock CouponPromotionRule and TotalAmountRules
            CouponPromotionRule rule1 = new CouponPromotionRule(new CouponPromotion()
            {
                Name = "Coupon Promotion Rule 1",
                PromotionCode = "123456789",
                Discount = 10,
                ExpiredOn = new DateTime(2100, 1, 1)
            });

            CouponPromotionRule rule2 = new CouponPromotionRule(new CouponPromotion()
            {
                Name = "Coupon Promotion Rule 2",
                PromotionCode = "111111111",
                Discount = 20,
                ExpiredOn = new DateTime(2000, 1, 1)
            });
            TotalAmountGreaterRule rule3 = new TotalAmountGreaterRule(new TotalAmountPromotion()
            {
                Name = "Total Amount Rule 1",
                TotalAmount = 1000,
                Discount = 100,
                ExpiredOn = new DateTime(2100, 1, 1)
            });

            ICustomRule[] rules = new ICustomRule[] { rule1, rule2, rule3 };
            foreach (var rule in rules)
            {
                rule.Execute(cart);
            }
        }

        static void Main(string[] args)
        {
            ShoppingCart shoppingCart = new ShoppingCart();
            Enumerable.Range(1, 5).ToList().ForEach(i => shoppingCart.SelectedProducts.Add(
                   new Product() { Name = "Product" + i.ToString(), Price = 100 * i }));
            shoppingCart.CouponCodes.Add("123456789");
            shoppingCart.CouponCodes.Add("111111111");
            Console.Write("\nShopping Cart before rules execution -- ");
            Console.WriteLine("Total : ${0}\tDiscount : ${1}", shoppingCart.TotalAmount, shoppingCart.Discount);

            RuleTest(shoppingCart);

            Console.Write("Shopping Cart after rules execution -- ");
            Console.WriteLine("Total : ${0}\tDiscount : ${1}", shoppingCart.TotalAmount, shoppingCart.Discount);

            Console.Read();
        }      
    }
}

The result of the test app:

Thursday, January 27, 2011

Create Windows Workflow 4.0 Rules By Custom String

In my previous post, I presented a way to create Windows Workflow rules by CodeDom expression. Sometimes it's hard to build a meaningful UI for authoring business rules. In such case we may need to use custom string to construct WF rules. The key part is how to parse the rule condition/action string. We could do similar things in WF RuleSetDialog. Can we re-use the parser used by RuleSetDialog? The answer is yes. Beau Crawford gave a solution for WF 3.5. Basically WF internally has a rule condition and action parser under System.Workflow.Activities.Rules.Parser inside System.Workflow.Activities assembly, but the parser is not exposed to public. In order to re-use the internal parser we need to use reflection.

Following code example demos how to archive this by slight modification of the code used in previous post:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.CodeDom;
using System.Reflection;
using System.Workflow.Activities.Rules;
using System.Workflow.ComponentModel.Compiler;

class Product
{
public string Name { get; set; }
public decimal Price { get; set; }
}

class ShoppingCart
{
public List<Product> SelectedProducts { get; set; }
public List<string> CouponCodes { get; set; }
public decimal TotalAmount { get { return SelectedProducts.Sum(prod => prod.Price); } }
internal decimal Discount { get; set; }

public ShoppingCart()
{
SelectedProducts = new List<Product>();
CouponCodes = new List<string>();
Discount = 0;
}
}

class CouponPromotion
{
public string Name { get; set; }
public string PromotionCode { get; set; }
public decimal Discount { get; set; }
public DateTime ExpiredOn { get; set; }
}

/// <summary>
/// WFRuleParser is helper class to parse Windows Workflow condition and action.
/// Invoke ParseCondition and ParseAction two internal methods
/// defined in System.Workflow.Activities.Rules.Parser by reflection
/// </summary>
class WFRuleParser
{
public static RuleExpressionCondition ParseCondition(Type targetObjectType, string expression)
{
RuleValidation ruleValidation = new RuleValidation(targetObjectType, null);
return ExecuteMethod("ParseCondition",
new object[] { ruleValidation }, new object[] { expression }) as RuleExpressionCondition;
}

public static List<RuleAction> ParseAction(Type targetObjectType, string expression)
{
RuleValidation ruleValidation = new RuleValidation(targetObjectType, null);
return ExecuteMethod("ParseStatementList",
new object[] { ruleValidation }, new object[] { expression }) as List<RuleAction>;
}

// Invoke private/internal method using reflection
private static object ExecuteMethod(string methodName, object[] ctorParameters, object[] methodParameters)
{
string ParserTypeName ="System.Workflow.Activities.Rules.Parser, System.Workflow.Activities,
Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"
;
try
{
Type type = Type.GetType(ParserTypeName);
ConstructorInfo constructor = type.GetConstructor(BindingFlags.NonPublic | BindingFlags.Instance,
null, new Type[] { typeof(RuleValidation) }, null);
object instance = constructor.Invoke(ctorParameters);
MethodInfo method = instance.GetType().GetMethod(methodName,
BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
return method.Invoke(instance, methodParameters);
}
catch (Exception ex)
{
throw ex;
}
}
}

class WFRuleBuilder
{
// Create a Rule for a CouponPromotion
public static Rule GetCouponPromotionRule(CouponPromotion couponPromo)
{
Type targetObjectType = typeof(ShoppingCart); // Shopping Cart is the input object for Rules Engine

// condition => if coupone code is matched and promotion is not expired
var conditionString = string.Format(
"this.CouponCodes.Contains(\"{0}\") && DateTime.Today < DateTime.Parse(\"{1}\")",
couponPromo.PromotionCode, couponPromo.ExpiredOn.ToString("yyyy-MM-dd"));
var condition = WFRuleParser.ParseCondition(targetObjectType, conditionString);

// action => add discount
var actionString = string.Format("this.Discount = this.Discount + {0}", couponPromo.Discount);
var action = WFRuleParser.ParseAction(targetObjectType, actionString);

return new Rule(couponPromo.Name, condition, action, null);
}
}

class Program
{
static void Main(string[] args)
{
// Mock 5 products in shopping cart:
// [{"Product1":{Price:$100, Category:"Category1"}}, {"Product2":{Price: $200, Category:"Category1"}}, ...]
ShoppingCart shoppingCart = new ShoppingCart();
Enumerable.Range(1, 5).ToList().ForEach(i => shoppingCart.SelectedProducts.Add(
new Product() { Name = "Product" + i.ToString(), Price = 100 * i }));
shoppingCart.CouponCodes.Add("123456789");
shoppingCart.CouponCodes.Add("111111111");
Console.Write("\nShopping Cart before rules execution -- ");
Console.WriteLine("Total : ${0}\tDiscount : ${1}", shoppingCart.TotalAmount, shoppingCart.Discount);

// Mock 2 CouponPromotions
CouponPromotion promo1 = new CouponPromotion()
{
Name = "Promotion1", PromotionCode = "123456789", Discount = 100, ExpiredOn = new DateTime(2000, 1, 1)
};
CouponPromotion promo2 = new CouponPromotion()
{
Name = "Promotion2", PromotionCode = "111111111", Discount = 100, ExpiredOn = new DateTime(2100, 1, 1)
};


// Build a RuleSet with above two rules
RuleSet ruleSet = new RuleSet();
ruleSet.Rules.Add(WFRuleBuilder.GetCouponPromotionRule(promo1));
ruleSet.Rules.Add(WFRuleBuilder.GetCouponPromotionRule(promo2));

// Print Rule information
Console.WriteLine("\nRules defined in the RuleSet:");
ruleSet.Rules.ToList().ForEach(rule =>
{
Console.WriteLine("\"{0}\" RuleCondition: \n{1}", rule.Name, rule.Condition);
Console.WriteLine("\"{0}\" RuleAction: \n{1}", rule.Name, rule.ThenActions[0]);
});

// Execute Rules
RuleValidation validation = new RuleValidation(typeof(ShoppingCart), null);
if (ruleSet.Validate(validation))
{
RuleEngine engine = new RuleEngine(ruleSet, validation);
engine.Execute(shoppingCart);
}
else // Validation failed, print errors
{
StringBuilder errors = new StringBuilder();
foreach (ValidationError validationError in validation.Errors)
{
errors.Append(validationError.ErrorText);
}
Console.WriteLine("Validation errors:{0}", errors);
}

Console.Write("\nShopping Cart after rules execution -- ");
Console.WriteLine("Total : ${0}\tDiscount : ${1}", shoppingCart.TotalAmount, shoppingCart.Discount);

Console.Read();
}
}
A CouponPromotion has a coupon code and an expiration date property. At run-time, rules engine will check all coupon codes stored in a ShoppoingCart, then apply the discount if they are valid. The logic is:
if ShoppingCart.Contains(couponPromotion.CouponCode) && couponPromotion is not expired
then add CurrentCouponPromotion.Discount to ShoppingCart
Remember the target object the rule engine runs against on is a ShoppingCart instance. The rule action would be update ShoppingCart's Discount property. The exact strings to be parsed for two promotions defined in above example code are:
Promotion1 condition string:
this.CouponCodes.Contains("123456789") && System.DateTime.Today < System.DateTime.Parse("2000-01-01")
Promotion1 action string:
this.Discount = this.Discount + 100
Promotion2 condition string:
this.CouponCodes.Contains("111111111") && System.DateTime.Today < System.DateTime.Parse("2100-01-01")
Promotion2 action string:
this.Discount = this.Discount + 100
Obviously Promotion1 has expired and only Promotion1 is valid and applied. The console app result: