Development Simply Put

A blog simplifies main concepts in IT development and provides tips, hints, advices and some re-usable code. "If you can't explain it simply, you don't understand it well enough" -Albert Einstein

  • Development Simply Put

    If you can't explain it simply, you don't understand it well enough.

    Read More
  • Integrant

    Based in the U.S. with offshore development centers in Jordan and Egypt, Integrant builds quality custom software since 1992. Our staff becomes an extension of your team to eliminate the risks of software development outsourcing and our processes...

    Read More
  • ITWorx

    ITWorx is a global software professional services organization. Headquartered in Egypt, the company offers Portals, Business Intelligence, Enterprise Application Integration and Application Development Outsourcing services to Global 2000 companies.

    Read More
  • Information Technology Institute

    ITI is a leading national institute established in 1993 by the Information and Decision Support Centre.

    Read More

2013-04-12

How To Centralize Your Web Application Settings And Decouple Your Code From Back-End Dependent Logic & Code

Every web application needs some business related settings to be set by the application user to fulfill his needs at certain time. These settings could change from time to time but they are still an asset for the application user and for sure for the application developer.

Sometimes developers deal with settings as if they are some secondary things that should not take much attention. For sure they develop some sort of a methodology to manage it but they don't give it much thinking.

I believe that settings should be treated in a much better way as they represent a very important prospective regarding the business needs and some technical needs. So, why waste this.

Also, sometimes you find that the code is written in a way that makes the business related code is so coupled with the settings back-end storage code. For example; if settings are stored into web.config, you find too many lines of code through the application is accessing the web.config to retrieve the setting value to use it and act accordingly. May be this is acceptable at some point but what if you faced a business need which says that you have to store the settings in a SQL database or a separate XML file or a SharePoint list instead of the web.config???? You think this is too much far to happen .... believe me in the business field it happens.

So, you need to decouple your business code from the code responsible for storing and retrieving your settings values. This is not everything, you also need to centralize your settings and start dealing with them as a main asset which you should make use of.

Imagine that your application user asked you to provide him a decent page with decent UI where he can manage his application settings. This UI should provide some input validations and some good stuff. So, will you go look all around your code to find the settings and then start working on the UI and apply the validations for each field,............. This is good but not that good because every time you find a need for an additional setting you will go back to this page to add your new fields and validations. You will finally notice that most of your code and logic is repeated and this is not good.

So, this is what this post about. After working on a design to help me what I wanted to achieve here, I came out with some code which I really find decent enough to overcome many of the issues I faced before. This code is currently used on a commercial web application for a big company and it approved me right. I am proud of it.

So now it is the time to see some code.


Decoupling business code from settings back-end provider code

SettingsDefinitions.cs
This file includes the definitions (classes, enums, interfaces) which we need to decouple our business code from the code related to the back-end we use to store our settings. Our business code will use these definitions to interact with the settings back-end in an indirect way so that our business code doesn't care if this back-end is a web.config, other XML file, SQL database, Oracle database, NTFS file,..... or any other type of back-end. This is a good thing as anytime we need to modify or change this back-end we will not suffer from changing too many code through the whole application, this is beside the cost of testing all the touched and changed code.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace DevelopmentSimplyPut.CommonUtilities.Settings
{
    [Serializable]
    public class SettingCatalogToken
    {
        public BusinessSetting BusinessSettingName { set; get; }
        public string Category { set; get; }
        public string Key { set; get; }
        public string Description { set; get; }
        public bool Mandatory { set; get; }
        public string DefaultValue { set; get; }
        public string Hint { set; get; }
        public bool RequiresIISReset { set; get; }
        public Func<string, bool> Validator { set; get; }
        public Func<string, object> Converter { set; get; }
    }

    [Serializable]
    public class SettingToken
    {
        public SettingCatalogToken SettingDefinition { set; get; }
        public string Value { set; get; }
        public bool ShowHint { set; get; }
        public SettingToken() { }

        public SettingToken(SettingCatalogToken settingDefinition, string value)
        {
            SettingDefinition = settingDefinition;
            Value = value;
        }
    }

 public enum SettingsProviderType
    {
        ConfigStore = 0,
        WebConfig = 1
    }
 
    public interface ISettingsProvider
    {
        string GetSettingValue(string category, string key);
        void AddSettings(List<SettingToken> entries);
    }

    public class SettingsProviderFactory
    {
        public static ISettingsProvider GetProvider(SettingsProviderType providerType)
        {
            ISettingsProvider provider;

            switch (providerType)
            {
                case SettingsProviderType.ConfigStore:
                    provider = new ConfigStoreSettingsProvider();
                    break;
                case SettingsProviderType.WebConfig:
                    provider = new WebConfigSettingsProvider();
                    break;
                default:
                    provider = GetProvider(InternalConstants.DefaultSystemSettingsProvider);
                    break;
            }

            return provider;
        }
    } 
}

As you can see in the code above, our business code will deal with the interface "ISettingsProvider". Any class implementing this interface is assured to have the two methods "string GetSettingValue(string category, string key)" and "void AddSettings(List<SettingToken> entries)". So, whenever our business code needs to interact with our settings back-end -lets now call it provider- it can use this interface signature and defer/delegate the implementation to the run-time. By the way, the code above makes use of the "Strategy" and "Factory" design patterns.

Now, let's suggest some back-end storage providers for our settings so that we can plug into our application to test our code and the decoupling concept it represents.

For the sake of demonstration, I will assume two providers, one as a web.config and the other is a third party module known as "Config Store" which is used with SharePoint to store and retrieve settings from a SharePoint list.

WebConfigSettingsProvider.cs
This file provides the definition of a class representing a settings provider which depends on a web.config file as its back-end. For sure this class should implement the "ISettingsProvider" interface.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Configuration;
using Microsoft.SharePoint.Administration;
using System.Globalization;
using DevelopmentSimplyPut.CommonUtilities.Logging;
using System.Web.Configuration;
using Microsoft.SharePoint;

namespace DevelopmentSimplyPut.CommonUtilities.Settings
{
    public class WebConfigSettingsProvider : ISettingsProvider
    {
        public string GetSettingValue(string category, string key)
        {
            SystemLogger.Logger.LogMethodStart
                (
                    "public string GetSettingValue(string category, string key)",
                    new string[] { "category", "key" },
                    new object[] { category, key }
                );

            string result = string.Empty;

            SPSecurity.RunWithElevatedPrivileges(delegate()
            {
                using (SPSite site = new SPSite(SPContext.Current.Site.Url))
                {
                    try
                    {
                        Configuration config = WebConfigurationManager.OpenWebConfiguration("/", site.WebApplication.Name);
                        if (config.AppSettings.Settings[category.ToLower() + key.ToLower()] != null)
                        {
                            result = config.AppSettings.Settings[category.ToLower() + key.ToLower()].Value;
                            SystemLogger.Logger.LogMethodEnd("public string GetSettingValue(string category, string key)", true);
                        }
                    }
                    catch(Exception ex)
                    {
                        string msg = "Error in retrieveing a setting value from web.config file.";
                        SystemLogger.Logger.LogMethodEnd("public string GetSettingValue(string category, string key)", false);
                        SystemLogger.Logger.LogError(ex, msg);
                        throw;
                    }
                }
            });

            return result;
        }
        public void AddSettings(List<SettingToken> entries)
        {
            SystemLogger.Logger.LogMethodStart
                (
                    "public void AddSettings(SettingToken[] entries)",
                    new string[] { "entries" },
                    new object[] { entries }
                );

            SPSecurity.RunWithElevatedPrivileges(delegate()
            {
                using (SPSite site = new SPSite(SPContext.Current.Site.Url))
                {
                    try
                    {
                        SPWebService service = SPWebService.ContentService;

                        foreach (SettingToken token in entries)
                        {
                            SPWebConfigModification myModification = new SPWebConfigModification();
                            myModification.Path = "configuration/appSettings";
                            myModification.Name = string.Format(CultureInfo.InvariantCulture, "add[@key=\"{0}\"]", token.SettingDefinition.Category.ToLower() + token.SettingDefinition.Key.ToLower());
                            myModification.Sequence = 0;
                            myModification.Owner = "System";
                            myModification.Type = SPWebConfigModification.SPWebConfigModificationType.EnsureChildNode;
                            myModification.Value = string.Format(CultureInfo.InvariantCulture, "<add key=\"{0}\" value=\"{1}\"/>", token.SettingDefinition.Category.ToLower() + token.SettingDefinition.Key.ToLower(), token.Value);
                            site.WebApplication.WebConfigModifications.Add(myModification);
                            service.Update();
                            service.ApplyWebConfigModifications();
                        }

                        SystemLogger.Logger.LogMethodEnd("public void AddSettings(SettingToken[] entries)", true);
                    }
                    catch (Exception ex)
                    {
                        string msg = "Error in adding setting entries in web.config file.";
                        SystemLogger.Logger.LogError(ex, msg);
                        SystemLogger.Logger.LogMethodEnd("public void AddSettings(SettingToken[] entries)", false);
                        throw;
                    }
                }
            });
        }
    }
}

As you can see in the code above, the code is dependant on the context it runs inside. For example, the code above runs in a SharePoint environment and that's why it uses some SharePoint APIs to carry out some tasks and logic. But, still our business code doesn't care and this is the beauty of it.

ConfigStoreSettingsProvider.cs
This file provides the definition of a class representing a settings provider which depends on the third party "Config Store" as its back-end. You are not asked to understand every line of code but you should grasp the whole concept behind it.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Globalization;
using COB.SharePoint.Utilities;
using Microsoft.SharePoint;
using DevelopmentSimplyPut.CommonUtilities.Logging;

namespace DevelopmentSimplyPut.CommonUtilities.Settings
{
    public class ConfigStoreSettingsProvider : ISettingsProvider
    {
        public string GetSettingValue(string category, string key)
        {
            SystemLogger.Logger.LogMethodStart
                (
                    "public string GetSettingValue(string category, string key)",
                    new string[] { "category", "key" },
                    new object[] { category, key }
                );

            string result = string.Empty;

            try
            {
                result = ConfigStore.GetValue(category, key);
                SystemLogger.Logger.LogMethodEnd("public string GetSettingValue(string category, string key)", true);
            }
            catch (Exception ex)
            {
                SystemLogger.Logger.LogError(ex, "Error in retrieving a setting value from config store list.");
                SystemLogger.Logger.LogMethodEnd("public string GetSettingValue(string category, string key)", false);
                throw;
            }

            return result;
        }
        public void AddSettings(List<SettingToken> entries)
        {
            SystemLogger.Logger.LogMethodStart
                (
                    "public void AddSettings(SettingToken[] entries)",
                    new string[] { "entries" },
                    new object[] { entries }
                );

            SPSecurity.RunWithElevatedPrivileges(delegate()
            {
                using (SPSite site = new SPSite(SPContext.Current.Site.Url))
                {
                    using (SPWeb web = site.RootWeb)
                    {
                        try
                        {
                            web.AllowUnsafeUpdates = true;
                            SPList configStoreList = web.Lists[InternalConstants.ConfigStoreListName];

                            foreach (SettingToken token in entries)
                            {
                                SPQuery query = new SPQuery();
                                query.Query = string.Format
                                    (
                                        CultureInfo.InvariantCulture,
                                        @"<Where>
                                            <And>
                                                <Eq>
                                                    <FieldRef Name='{0}'/>
                                                    <Value Type='{1}'>{2}</Value>
                                                </Eq>
                                                <Eq>
                                                    <FieldRef Name='{3}'/>
                                                    <Value Type='{4}'>{5}</Value>
                                                </Eq>
                                            </And>
                                        </Where>",
                                        ConfigStore.CategoryField,
                                        "Text",
                                        token.SettingDefinition.Category,
                                        ConfigStore.KeyField,
                                        "Text",
                                        token.SettingDefinition.Key);

                                query.ViewFields = "<FieldRef Name='Title'/>";
                                query.RowLimit = 1;

                                SPListItemCollection items = configStoreList.GetItems(query);

                                if (null != items && items.Count > 0)
                                {
                                    string msg = "Setting with category = \"{0}\" and key = \"{1}\" already exists";
                                    SystemLogger.Logger.LogInfo
                                        (
                                            string.Format
                                                (
                                                    CultureInfo.InvariantCulture,
                                                    msg,
                                                    token.SettingDefinition.Category,
                                                    token.SettingDefinition.Key
                                                )
                                        );

                                    foreach (SPListItem item in items)
                                    {
                                        item[ConfigStore.ValueField] = token.Value;
                                        item["ConfigItemDescription"] = token.SettingDefinition.Description;
                                        item.Update();
                                    }
                                    configStoreList.Update();
                                    SystemLogger.Logger.LogInfo("Setting value is updated to \"" + token.Value + "\"");
                                }
                                else
                                {
                                    SPListItem entry = configStoreList.Items.Add();
                                    entry[ConfigStore.CategoryField] = token.SettingDefinition.Category;
                                    entry[ConfigStore.KeyField] = token.SettingDefinition.Key;
                                    entry[ConfigStore.ValueField] = token.Value;
                                    entry["ConfigItemDescription"] = token.SettingDefinition.Description;
                                    entry.SystemUpdate();
                                    configStoreList.Update();
                                }
                            }

                            SystemLogger.Logger.LogMethodEnd("public void AddSettings(SettingToken[] entries)", true);
                        }
                        catch (Exception ex)
                        {
                            SystemLogger.Logger.LogError(ex, "Error in adding setting entries in config store list.");
                            web.AllowUnsafeUpdates = false;
                            SystemLogger.Logger.LogMethodEnd("public void AddSettings(SettingToken[] entries)", false);
                            throw;
                        }
                    }
                }
            });
        }
    }
}

Now, after we have defined our possible settings providers, we can use our factory "SettingsProviderFactory" throughout our business code to get a run-time instance of a provider and start using it by calling the two methods we know. This is good but there is better.

We can write a class which is responsible for choosing the proper provider and handling some logic to finally provide the rest of our code with a settings provider.

SystemSettingsProvider.cs
This file includes a class which handles some logic to choose the proper settings provider and then pass it to our business code. You will find some methods using some classes which are not explained or mentioned yet so don't worry, this will be covered later in this post.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Collections.ObjectModel;
using DevelopmentSimplyPut.CommonUtilities.Logging;
using DevelopmentSimplyPut.CommonUtilities.Helpers;

namespace DevelopmentSimplyPut.CommonUtilities.Settings
{
    public static class SystemSettingsProvider
    {
        private static ISettingsProvider provider;  
        public static T GetSettingValue<T>(BusinessSetting businessSettingName)
        {
            return GetSettingValue<T>(GetSettingCatalogToken(businessSettingName)); 
        }
        public static T GetSettingValue<T>(SettingCatalogToken settingCatalogToken)
        {
            string value = null;
            T result = default(T);

            if (null != settingCatalogToken)
            {
                try
                {
                    value = Provider.GetSettingValue(settingCatalogToken.Category, settingCatalogToken.Key);
                }
                catch (Exception ex)
                {
                    if (settingCatalogToken.Mandatory)
                    {
                        SystemErrorHandler.HandleError(ex, "\"" + settingCatalogToken.Key + "\" setting is not set");
                    }
                    else
                    {
                        value = settingCatalogToken.DefaultValue;
                    }
                }

                if (!settingCatalogToken.Validator(value))
                {
                    if (settingCatalogToken.Mandatory)
                    {
                        SystemErrorHandler.HandleError("Provided \"" + settingCatalogToken.Key + "\" setting is not valid");
                    }
                    else
                    {
                        value = settingCatalogToken.DefaultValue;
                    }
                }

                result = ((T)settingCatalogToken.Converter(value));
            }
            else
            {
                SystemErrorHandler.HandleError(new Exception("SettingToken is not provided into SettingsCatalog"));
            }

            return result;
        }
        public static string TryGetSettingValue(string category, string key)
        {
            string result = string.Empty;

            try
            {
                result = Provider.GetSettingValue(category, key);
            }
            catch (Exception ex)
            {

            }

            return result;
        }
        public static bool UpdateSettings(List<SettingToken> settings)
        {
            bool result = true;
            List<SettingToken> toBeUpdated = new List<SettingToken>();

            if (null != settings && settings.Count > 0)
            {
                foreach (SettingToken token in settings)
                {
                    string value = token.Value;
                    if (!token.SettingDefinition.Validator(value))
                    {
                        token.ShowHint = true;
                        result = false;
                    }
                    else
                    {
                        token.ShowHint = false;
                        toBeUpdated.Add(token);
                    }
                }
            }

            Provider.AddSettings(toBeUpdated);

            return result;
        }
        public static SettingCatalogToken GetSettingCatalogToken(BusinessSetting businessSettingName)
        {
            return SystemSettingsCatalog.SettingsCatalog.DefaultIfEmpty(null).FirstOrDefault(setting => setting.BusinessSettingName == businessSettingName);
        }
        private static void SetProvider(SettingsProviderType providerType)
        {
            SystemLogger.Logger.LogMethodStart
                (
                    "private static void SetProvider(SettingsProviderType providerType)",
                    new string[] { "providerType" },
                    new object[] { providerType }
                );

            try
            {
                provider = SettingsProviderFactory.GetProvider(providerType);
                SystemLogger.Logger.LogInfo("SystemSetitngsProvider is set to " + providerType.ToString());
                SystemLogger.Logger.LogMethodEnd("private static void SetProvider(SettingsProviderType providerType)", true);
            }
            catch (Exception ex)
            {
                SystemLogger.Logger.LogError(ex, "Failed in setting SettingsProviderType");
                SystemLogger.Logger.LogMethodEnd("private static void SetProvider(SettingsProviderType providerType)", false);
                throw;
            }
        }
        private static ISettingsProvider Provider
        {
            get
            {
                if (provider == null)
                {
                    SetProvider(InternalConstants.DefaultSystemSettingsProvider);
                }
                return provider;
            }
        }
        public static void ResetProvider()
        {
            ResetProvider(InternalConstants.DefaultSystemSettingsProvider);
        }
        public static void ResetProvider(SettingsProviderType providerType)
        {
            SetProvider(providerType);
        }
    }
}

As you can see in the code above, this class decides which provider to use and encapsulates some useful logic to be used throughout the rest of the code. So, whenever you need to interact with a settings provider you use this static class methods to achieve what you want.

As I said before, you may have noticed some strange code in the class above that uses some classes that are not defined or explained yet. This code depends on some code I will provide in the second section of this post. This section is talking out how to deal with your settings as an asset.


Settings as an asset

Every setting in your application has some properties which can describe it. Also it has some actions related to it. Doesn't this ring a bell? Isn't this make you shout out loud the word "Class".

This is true, your settings are not just pairs of keys and values. They are a fully qualified class which has its behavior.

For example, if you have in your application a setting which holds a connection string for a SQL database you use throughout the application, don't you want to check if the value provided by the application user is a valid connection string format as you know that it needs to match a certain regular expression? don't you also need to make sure that the value provided represents an online and running SQL database?

If your answer is "Yes", so why do you depend on a helper method in a utility class to do all your checks and you have to call it explicitly whenever you want to verify a connection string? Why don't you keep this verification logic as close as you can to your setting?

Also, If you mark every setting with a key so that you can retrieve this setting from its back-end anytime, why to use a string -array of characters- which you can misspell? why don't you use a more concrete and stable way like enums? This is what I am trying to achieve in the code below.

SystemSettingsCatalog.cs
This file includes an internal catalog which includes all your application settings as fully qualified instances of a fully qualified class encapsulating their verification and conversion logic.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Collections.ObjectModel;
using DevelopmentSimplyPut.CommonUtilities.Helpers;

namespace DevelopmentSimplyPut.CommonUtilities.Settings
{
 public enum BusinessSetting
    {
        DBConnectionString = 0,
        AdminsGroupName = 1,
  GridPageSize = 2,
        AutoCompleteMinCharCount = 3
    }

    public static class SystemSettingsCatalog
    {
        #region Constructor
        static SystemSettingsCatalog()
        {
            settingsCatalog = new Collection<SettingCatalogToken>();
            
   settingsCatalog.Add(new SettingCatalogToken()
            {
                BusinessSettingName = BusinessSetting.DBConnectionString,
                Category = "DevelopmentSimplyPut",
                Key = "DBConnectionString",
                Description = "Connection string of the SQL database",
                DefaultValue = string.Empty,
                Mandatory = true,
                RequiresIISReset = true,
                Hint = "Should be a valid ConnectionString of an online SQL database",
                Validator = new Func<string, bool>
                    (
                        delegate(string settingValue)
                        {
                            string exceptionMessage;
                            return Utilities.VerifySQLConnectionString(settingValue, out exceptionMessage);
                        }
                    ),
                Converter = new Func<string, object>
                    (
                        delegate(string settingValue)
                        {
                            return settingValue;
                        }
                    )
            });
            settingsCatalog.Add(new SettingCatalogToken()
            {
                BusinessSettingName = BusinessSetting.AdminsGroupName,
                Category = "DevelopmentSimplyPut",
                Key = "AdminsGroupName",
                Description = "Name of the Admins users group(s). Users in this/these group(s) will be allowed to access administration pages. Multiple group names should be separated by \",\"",
                DefaultValue = "AdminGroup",
                Mandatory = false,
                RequiresIISReset = false,
                Hint = string.Empty,
                Validator = new Func<string, bool>
                    (
                        delegate(string settingValue)
                        {
                            return true;
                        }
                    ),
                Converter = new Func<string, object>
                    (
                        delegate(string settingValue)
                        {
                            return settingValue;
                        }
                    )
            });
            settingsCatalog.Add(new SettingCatalogToken()
            {
                BusinessSettingName = BusinessSetting.GridPageSize,
                Category = "DevelopmentSimplyPut",
                Key = "GridPageSize",
                Description = "Number of items to be viewed in each page of the system data grids",
                DefaultValue = "20",
                RequiresIISReset = false,
                Mandatory = false,
                Hint = "Should be an integer greater than 0",
                Validator = new Func<string, bool>
                    (
                        delegate(string settingValue)
                        {
                            int result = 0;
                            return (int.TryParse(settingValue, out result) && result > 0);
                        }
                    ),
                Converter = new Func<string, object>
                    (
                        delegate(string settingValue)
                        {
                            return int.Parse(settingValue);
                        }
                    )
            });
            settingsCatalog.Add(new SettingCatalogToken()
            {
                BusinessSettingName = BusinessSetting.AutoCompleteMinCharCount,
                Category = "DevelopmentSimplyPut",
                Key = "AutoCompleteMinCharCount",
                Description = "The minimum number of characters to be entered into the textbox input fields for the auto-complete functionality to start. Please note that the chosen value will affect the system performance, so try to choose a number as large as you can",
                DefaultValue = "10",
                Mandatory = false,
                RequiresIISReset = false,
                Hint = "Should be an integer greater than 0",
                Validator = new Func<string, bool>
                    (
                        delegate(string settingValue)
                        {
                            int result = 0;
                            return (int.TryParse(settingValue, out result) && result > 0);
                        }
                    ),
                Converter = new Func<string, object>
                    (
                        delegate(string settingValue)
                        {
                            return int.Parse(settingValue);
                        }
                    )
            });
        }
        #endregion

        #region SettingsCatalog
        private static Collection<SettingCatalogToken> settingsCatalog;
        public static Collection<SettingCatalogToken> SettingsCatalog
        {
            get
            {
                return settingsCatalog;
            }
        }
        #endregion
    }
}

As you can see in the code above, all info and actions related to an application setting are encapsulated into one class which can be easily managed and extended anytime.

Also, we have a catalog of all our application settings which we can use every time we need to refer to a certain setting or even list all our settings -as we will see later in this post- beside being able to use enums instead of just strings as identifiers for our settings.


The proof

Now, whenever you need to retrieve a value for a certain setting, you can call it as follows
string dbConnectionStr = SystemSettingsProvider.GetSettingValue<string>(BusinessSetting.DBConnectionString);
string adminGroupName = SystemSettingsProvider.GetSettingValue<string>(BusinessSetting.AdminsGroupName);
int gridPageSize = SystemSettingsProvider.GetSettingValue<int>(BusinessSetting.GridPageSize);
int autoCompleteMinCharCount = SystemSettingsProvider.GetSettingValue<int>(BusinessSetting.AutoCompleteMinCharCount);

Also, as a proof of concept, I implemented a settings management page which can be used to change the settings values beside providing some valuable info and validations for user inputs.

Using the code below, you can get a page like the one in this screenshot with just few lines of code. This is beside that whenever you add a new setting to your settings catalog, the page is updated automatically and you don't need to apply any changes.

ManageSettings.aspx
<%@ Page Language="C#" AutoEventWireup="true" enableSessionState="True" Inherits="DevelopmentSimplyPut.Pages.ManageSettings, DevelopmentSimplyPut,  Version=1.0.0.0, Culture=neutral, PublicKeyToken=5b3b2dbf31f780b4" %>

<%@ Import Namespace="DevelopmentSimplyPut.CommonUtilities" %>
<%@ Import Namespace="Microsoft.SharePoint" %>
<%@ Import Namespace="Microsoft.SharePoint.ApplicationPages" %>
<%@ Register TagPrefix="SharePoint" Namespace="Microsoft.SharePoint.WebControls"
    Assembly="Microsoft.SharePoint, Version=12.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" %>
<%@ Register TagPrefix="asp" Namespace="System.Web.UI.WebControls" Assembly="System.Web.Extensions, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" %>
<%@ Register TagPrefix="SPSWC" Namespace="Microsoft.SharePoint.Portal.WebControls"
    Assembly="Microsoft.SharePoint.Portal, Version=12.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" %>
<%@ Register TagPrefix="wssawc" Namespace="Microsoft.SharePoint.WebControls" Assembly="Microsoft.SharePoint, Version=12.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" %>
<%@ Register TagPrefix="WebPartPages" Namespace="Microsoft.SharePoint.WebPartPages"
    Assembly="Microsoft.SharePoint, Version=12.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" %>
<%@ Register TagPrefix="PublishingWebControls" Namespace="Microsoft.SharePoint.Publishing.WebControls"
    Assembly="Microsoft.SharePoint.Publishing, Version=12.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" %>
<%@ Register TagPrefix="Nav" Namespace="Microsoft.SharePoint.Publishing.Navigation"
    Assembly="Microsoft.SharePoint.Publishing, Version=12.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" %>
<%@ Register TagPrefix="wssuc" TagName="InputFormSection" Src="~/_controltemplates/InputFormSection.ascx" %>
<%@ Register TagPrefix="wssuc" TagName="InputFormControl" Src="~/_controltemplates/InputFormControl.ascx" %>
<%@ Register TagPrefix="wssuc" TagName="ButtonSection" Src="~/_controltemplates/ButtonSection.ascx" %>
<%@ Register TagPrefix="wssuc" TagName="Welcome" Src="~/_controltemplates/Welcome.ascx" %>
<%@ Register TagPrefix="wssuc" TagName="DesignModeConsole" Src="~/_controltemplates/DesignModeConsole.ascx" %>
<%@ Register TagPrefix="PublishingVariations" TagName="VariationsLabelMenu" Src="~/_controltemplates/VariationsLabelMenu.ascx" %>
<%@ Register TagPrefix="PublishingConsole" TagName="Console" Src="~/_controltemplates/PublishingConsole.ascx" %>
<%@ Register TagPrefix="PublishingSiteAction" TagName="SiteActionMenu" Src="~/_controltemplates/PublishingActionMenu.ascx" %>
<asp:content id="PageTitle" runat="server" contentplaceholderid="PlaceHolderPageTitle">
    Manage Settings
</asp:content>
<asp:content id="PageTitleInTitleArea" runat="server" contentplaceholderid="PlaceHolderPageTitleInTitleArea">
</asp:content>
<asp:content id="Main" runat="server" contentplaceholderid="PlaceHolderMain">
    <script>
    </script>

 <table cellspacing="0" cellpadding="3" width="100%">
     <tr>
         <td>
             <div id="SettingsGridDiv" runat="server" class="grid">
                 <DevelopmentSimplyPutWebControls:EnhancedDataGrid runat="server"
                 ID="grd_Settings"
                 AutoGenerateColumns="False"
                 AllowPaging="false"
                 AllowSorting="false"
                 PageSize="200"
                 CurrentPageIndex="0"
                 VirtualItemCount="0"
                 ExportToExcel="False"
                 BorderStyle="None"
                 Width="100%"
                 GridLines="None"
                 HorizontalAlign="Center"
                 HorizontalScrollBarVisibility="Hidden"
                 SortingUpImageRelativePath="tri-up.gif"
                 SortingDownImageRelativePath="tri.gif"
                 PagingNextImageRelativePath="rmc/pager_next_arrow.png"
                 PagingPrevImageRelativePath="rmc/pager_perv_arrow.png"
                 CssClass="gridStyle-table">
                 <RowStyle CssClass="gridStyle-tr-data" Wrap="False" />
                 <AlternatingRowStyle CssClass="gridStyle-tr-alt-data" Wrap="False" />
                 <HeaderStyle CssClass="gridStyle-tr-header" />
                  <Columns>
                   <asp:TemplateField HeaderText="Category">
                    <ItemTemplate>
                     <asp:Label  ID="lbl_Category" Text='<%# Eval("SettingDefinition.Category") %>'
                      runat="server" />
                    </ItemTemplate>
                    <ItemStyle CssClass="gridStyle-item-td Category-Css" />
                    <HeaderStyle CssClass="gridStyle-header-th Category-Css" Wrap="true" Width="10%"/>
                   </asp:TemplateField>
                   <asp:TemplateField HeaderText="Key">
                    <ItemTemplate>
                     <asp:Label  ID="lbl_Key" Text='<%# Eval("SettingDefinition.Key") %>'
                      runat="server" />
                    </ItemTemplate>
                    <ItemStyle CssClass="gridStyle-item-td Key-Css" />
                    <HeaderStyle CssClass="gridStyle-header-th Key-Css" Wrap="true" Width="25%"/>
                   </asp:TemplateField>
                   <asp:TemplateField HeaderText="Description">
                    <ItemTemplate>
                     <asp:Label  ID="lbl_Description" Text='<%# Eval("SettingDefinition.Description") %>'
                      runat="server" />
                    </ItemTemplate>
                    <ItemStyle CssClass="gridStyle-item-td Description-Css" />
                    <HeaderStyle CssClass="gridStyle-header-th Description-Css" Wrap="true" Width="35%"/>
                   </asp:TemplateField>
                   <asp:TemplateField HeaderText="IIS Reset?">
                    <ItemTemplate>
                     <asp:Label  ID="lbl_IISReset" Text='<%# ((bool)Eval("SettingDefinition.RequiresIISReset"))? "Yes" : "No" %>'
                      runat="server" />
                    </ItemTemplate>
                    <ItemStyle CssClass="gridStyle-item-td IISReset-Css" />
                    <HeaderStyle CssClass="gridStyle-header-th IISReset-Css" Wrap="true" Width="5%"/>
                   </asp:TemplateField>
                   <asp:TemplateField HeaderText="Value">
                    <ItemTemplate>
                     <asp:TextBox id="txt_Value" TextMode="Multiline" runat="server" Width="95%" Text='<%# Eval("Value") %>'/>
                                    <asp:RequiredFieldValidator ID="vld_txt_Value_NotEmpty" Text="Field is required"  ControlToValidate="txt_Value"
                                    runat="server" Display="Dynamic"/>
                                    <asp:CustomValidator ID="vld_txt_Value" runat="server"
                                    CssClass="ErrorMessage" ControlToValidate="txt_Value" Display="Dynamic"/>
                    </ItemTemplate>
                    <ItemStyle CssClass="gridStyle-item-td Value-Css" />
                    <HeaderStyle CssClass="gridStyle-header-th Value-Css" Wrap="true" Width="25%"/>
                   </asp:TemplateField>
                  </Columns>
                  </DevelopmentSimplyPutWebControls:EnhancedDataGrid>
             </div>
         </td>
     </tr>
     <tr>
         <td style="text-align:right">
             <asp:HiddenField ID="hdn_SubmitClicked" Value="0" runat="server" />
             <asp:Button class="form-button" Text="Submit Changes" ID="btn_Submit" OnClick="btn_Submit_Click" runat="server" />
         </td>
     </tr>
 </table>
</asp:content>

ManageSettings.aspx.designer.cs
using System.Web.UI.WebControls;
using AjaxControlToolkit;
using DevelopmentSimplyPut.CommonUtilities.WebControls;
using System.Web.UI.HtmlControls;

namespace DevelopmentSimplyPut.Pages
{
    public partial class ManageSettings
    {
        protected EnhancedDataGrid grd_Settings;
        protected Button btn_Submit;
        protected HiddenField hdn_SubmitClicked;
        protected global::System.Web.UI.HtmlControls.HtmlGenericControl SettingsGridDiv;
    }
}

ManageSettings.aspx.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;
using Microsoft.SharePoint.WebControls;
using System.Web.Configuration;
using Microsoft.SharePoint;
using System.Configuration;
using DevelopmentSimplyPut.CommonUtilities;
using DevelopmentSimplyPut.CommonUtilities.Logging;
using DevelopmentSimplyPut.CommonUtilities.Settings;
using DevelopmentSimplyPut.CommonUtilities.Security;
using DevelopmentSimplyPut.CommonUtilities.Helpers;
using System.Drawing;
using System.Data;
using DevelopmentSimplyPut.Entities;
using DevelopmentSimplyPut.BusinessLayer;
using DevelopmentSimplyPut.CommonUtilities.WebControls;
using System.Collections.ObjectModel;
using System.Web.UI.HtmlControls;
using System.Globalization;

namespace DevelopmentSimplyPut.Pages
{
    public partial class ManageSettings : BasePage
    {
        #region Events
        protected void Page_Load(object sender, EventArgs e)
        {
            if (!IsPostBack)
            {
                BindData();
            }
        }
        protected void btn_Submit_Click(object sender, EventArgs e)
        {
            if (null != Session["SettingsGridDataSource"])
   {
    List<SettingToken> lst = new List<SettingToken>();
    List<SettingToken> source = (List<SettingToken>)Session["SettingsGridDataSource"];
    foreach (GridViewRow row in grd_Settings.Rows)
    {
     if (row.RowType == DataControlRowType.DataRow)
     {
      SettingToken token = source[row.RowIndex];
      string newValue = ((TextBox)row.FindControl("txt_Value")).Text;
      CustomValidator validatior = (CustomValidator)row.FindControl("vld_txt_Value");

      if (!token.SettingDefinition.Validator(newValue))
      {
       token.ShowHint = true;
       validatior.IsValid = false;
       validatior.ErrorMessage = token.SettingDefinition.Hint;
      }
      else
      {
       token.ShowHint = false;
       validatior.IsValid = true;
       lst.Add(token);
      }

      token.Value = newValue;
     }
    }

    SystemSettingsProvider.UpdateSettings(lst);
   }
        }
        #endregion
        #region Methods
        private void BindData()
        {
            try
            {
                SystemLogger.Logger.LogMethodStart("BindData()", null, null);

                List<SettingToken> settings = GetSettinngs();

                if (null != settings && settings.Count > 0)
                {
                    grd_Settings.Visible = true;
                    SettingsGridDiv.Visible = true;
                    grd_Settings.PageIndex = 0;
                    grd_Settings.VirtualItemCount = settings.Count;
                    Session["SettingsGridDataSource"] = settings;
                    grd_Settings.DataSource = settings;
                    grd_Settings.DataBind();
                }
                else
                {
                    SettingsGridDiv.Visible = false;
                    grd_Settings.Visible = false;
                }

                SystemLogger.Logger.LogMethodEnd("BindData()", true);
            }
            catch (Exception ex)
            {
                SystemLogger.Logger.LogError(ex.Message);
                SystemLogger.Logger.LogMethodEnd("BindData()", false);
                SystemErrorHandler.HandleError(ex);
            }
        }
        private List<SettingToken> GetSettinngs()
        {
            List<SettingToken> result = new List<SettingToken>();

            foreach (SettingCatalogToken token in SystemSettingsCatalog.SettingsCatalog)
            {
                string value = SystemSettingsProvider.TryGetSettingValue(token.Category, token.Key);
                value = (string.IsNullOrEmpty(value)) ? string.Empty : value;

                SettingToken finalToken = new SettingToken();
                finalToken.SettingDefinition = token;
                finalToken.Value = value;

                if (!token.Validator(value))
                {
                    finalToken.ShowHint = true;
                }
                else
                {
                    finalToken.ShowHint = false;
                }

                result.Add(finalToken);
            }

            return result;
        }
        #endregion
    }
} 

As you can see, it is too easy to believe. Finally, here are some code of helping classes I used.

InternalConstants.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using DevelopmentSimplyPut.CommonUtilities.Logging;
using DevelopmentSimplyPut.CommonUtilities.Settings;
using DevelopmentSimplyPut.CommonUtilities.Security;
using DevelopmentSimplyPut.CommonUtilities.Helpers;
using Microsoft.SharePoint;
using System.Collections.ObjectModel;

namespace DevelopmentSimplyPut.CommonUtilities
{
    public static class InternalConstants
    {
        #region Settings Provider
        public static SettingsProviderType DefaultSystemSettingsProvider
        {
            get
            {
                return SettingsProviderType.ConfigStore;
            }
        }
        public static string ConfigStoreListName
        {
            get
            {
                return "Config store";
            }
        }
  #endregion
    }
}

Utilities.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using DevelopmentSimplyPut.CommonUtilities.Logging;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Data.Common;
using System.Data.SqlClient;
using System.Web;

namespace DevelopmentSimplyPut.CommonUtilities.Helpers
{
    public static class Utilities
    {
        /// <summary>
        /// Checks whether a given string represents a valid and online ConnectionString for a SQL database
        /// </summary>
        /// <param name="connectionString">String to be verified</param>
        /// <param name="exceptionMessage">Output exception message if exists</param>
        /// <returns></returns>
        public static bool VerifySQLConnectionString(string connectionString, out string exceptionMessage)
        {
            bool result;

            try
            {
                DbConnectionStringBuilder csb = new DbConnectionStringBuilder();
                csb.ConnectionString = connectionString;

                try
                {
                    using (SqlConnection conn = new SqlConnection(connectionString))
                    {
                        conn.Open();
                    }

                    exceptionMessage = null;
                    result = true;
                }
                catch(Exception ex)
                {
                    exceptionMessage = ex.Message;
                    result = false;
                }  
            }
            catch(Exception ex)
            {
                exceptionMessage = ex.Message;
                result = false;
            }

            return result;
        }
    }
}

SystemErrorHandler.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using DevelopmentSimplyPut.CommonUtilities.Logging;
using System.Globalization;
using System.Web.UI;
using System.Web;

namespace DevelopmentSimplyPut.CommonUtilities
{
    public static class SystemErrorHandler
    {
        public static void HandleError(Exception ex, string message)
        {
            string guid = System.Guid.NewGuid().ToString();
            SystemLogger.Logger.LogError(string.Format(CultureInfo.InvariantCulture, "Unexpected error start, GUID = \"{0}\"", guid));

            if (null != ex)
            {
                SystemLogger.Logger.LogError(ex, message);
            }
            else
            {
                SystemLogger.Logger.LogError(message);
            }

            SystemLogger.Logger.LogError(string.Format(CultureInfo.InvariantCulture, "Unexpected error end, GUID = \"{0}\"", guid));
            HttpContext.Current.Response.Redirect
                (
                    string.Format
                    (
                        CultureInfo.InvariantCulture,
                        "{0}/Error.aspx?generalmsg={1}&msg={2}&guid={3}",
                        InternalConstants.PagesDirectoryAbsolutePath,
                        HttpContext.Current.Server.UrlEncode(InternalConstants.UnexpectedErrorMsg),
                        HttpContext.Current.Server.UrlEncode(message),
                        HttpContext.Current.Server.UrlEncode(guid)
                    ), true
                );
        }
        public static void HandleError(Exception ex)
        {
            HandleError(ex, string.Empty);
        }
        public static void HandleError(string message)
        {
            HandleError(new Exception(".."), message);
        }
        public static void HandleError(string exceptionMessage, string message)
        {
            HandleError(new Exception(exceptionMessage), message);
        }
    }
}


You can download the code from here


2013-03-15

Having Fun With JavaScript And GreaseMonkey


Having Fun With JavaScript And GreaseMonkey

This time I am going to tell you about a great Firefox extension/addon called GreaseMonkey.

I think everyone of us has faced the situation when he found that some website is missing something which could have been done by just a bit of JavaScript. But, unfortunately we can do nothing regarding this except contacting the website author to try to convince him that this change is really good and he should do it.

I can now tell you that you don't have to go through all this hustle because you now have GreaseMonkey which provides you with the ability to run JavaScript across certain websites. It is like injecting the JavaScript you need into a website but for sure at your side only, not in the source website itself.

Using GreaseMonkey you can perform so many cool things. There are so many free scripts others wrote which you can search and use as you wish. You can find these scripts here or here


Me myself have wrote some scripts which made my life easier. Some alter web pages UI, others do some business depending on some rules, ...... but the most powerful ones which I really love are the links elongators.

As you know there are some services for shortening links like "1Tool", "TakeMyFile" and many otheres. These services takes a long URL and returns back to you a short one which you can share and post anywhere you wish. The only annoying thing regarding this is that when you use the short link you will be directed to a page with a time counter or ads or some annoying thing till you are finally re-directed to the main URL. This really made me mad.

This could be somehow acceptable when you just need to browse a certain URL, but what about a bunch of them. We all know about forums and how we can find in one thread an attachment which is so big in size that it is divided into a huge number of part files uploaded to some online hosting service like RapidShare or whatever. In this case, lets say that number of links is 30, will you click on each one of these 30 links to be redirected to some annoying page -each time of the 30 times- and wait for some counter and finally get your original link??!!!

This encouraged me to write my own GreaseMonkey scripts to undo what the shortening services already done by getting the original links and replacing the shortened ones in the page with their corresponding original ones.

This is just a sample of what you can do with GreaseMonkey and that's why I really encourage you give it a try, you will love it.

To know what you can do with GreaseMonkey, you can have a look on the code sample below
// ==UserScript==

// @name          1Tool Short Links Elongator

// @namespace     DevelopmentSimplyPut(developmentsimplyput.blogspot.com)

// @description   Elongates all 1Tool short links to their direct links

// @include       *

// ==/UserScript==

String.prototype.ReplaceAll = function(stringToFind,stringToReplace){
    var temp = this;
    var index = temp.indexOf(stringToFind);

        while(index != -1){

            temp = temp.replace(stringToFind,stringToReplace);
            index = temp.indexOf(stringToFind);
        }
        return temp;
    }

Array.prototype.unique = function () {
 var r = new Array();
 o:for(var i = 0, n = this.length; i < n; i++)
 {
  for(var x = 0, y = r.length; x < y; x++)
  {
   if(r[x]==this[i])
   {
    continue o;
   }
  }
  r[r.length] = this[i];
 }
 return r;
}

function Run(Urls)
{
 if(Urls.length>0)
 {
  for(i=0;i<Urls.length;i++)
   GetDirectLink(Urls[i]);
 }
}

function GetDirectLink(str)
{
 GM_xmlhttpRequest(
       {
        method: "GET",
        url: 'http://www.yahoo.com/.php?id=' + str.replace('http://1tool.biz/',""),
        headers:{'User-agent': 'Mozilla/4.0 (compatible) Greasemonkey', 'Content-type':'application/x-www-form-urlencoded'},
        onload:function(result)
        {
         var parts1=new Array();
         var parts2=new Array();
         parts1=result.responseText.split('onclick="NewWindow(');
         parts2=parts1[1].split("'");
         result=parts2[1];
         (document.getElementsByTagName("body"))[0].innerHTML=(document.getElementsByTagName("body"))[0].innerHTML.ReplaceAll(str,result);
        }
       }
      );
}

var DirectUrls=new Array();
var Urls=new Array();
var UrlsPattern=/http:\/\/1tool\.biz\/(?:\w*)/g;
Urls=(document.getElementsByTagName("body"))[0].innerHTML.match(UrlsPattern);

Urls=Urls.unique();
Run(Urls);

That's it. This is just a scratch on the surface but not everything. You can check it with yourself and find what you can do with GreaseMonkey.

At last, you can have a look on my scripts here or here and for sure your feedback is so welcomed.


Resources & Links
  1. Greasespot
  2. Greasemonkey :: Add-ons for Firefox
  3. Userscripts.org: Power-ups for your browser
  4. AhmedTarekHasan's Scripts on GreasyFork.org
  5. AhmedTarekHasan's Scripts on Openuserjs.org

2013-03-08

How To Create SQL Custom User-Defined Aggregate Functions

How To Create SQL Custom User-Defined Aggregate Functions


Code samples on this post can be downloaded from here


Sometimes you need to perform some complex SQL queries depending on some aggregate operations or functions. In most cases, these complex operations could be achieved by series of queries with joins and groupings, but, sometimes it is almost impossible to achieve this by the basic queries and functions natively supported by SQL.

For example, assume you have a table of buyers called "Buyers" and a table of products called "Products". Also, each product has some properties like size which may have multiple values per product. Now, the business says that we need to list all the products but not with all the size values instead we will only list the most commonly purchased size.

To be more clear, lets assume that 10 buyers bought a certain mobile phone with size 5, while other 2 buyers bought the same phone but with size 6. Then, when we list this mobile phone product, we will only list mobile phone with the size 5 cause it is the most common purchased one.

So, to achieve this using only native SQL queries you will find it so complex especially when you need to apply the same logic on other columns (in our example, not only the size column). This leads us to custom user-defined aggregate functions.

SQL Server doesn’t have so many aggregate functions to use, only the basics such as COUNT, MIN, MAX and few others are implemented but still the list is quite small. However, SQL Server includes CLR integration which we can use to add our own custom user-defined aggregate functions.

To do this, we need to:
  1. Create a class library project targeting .NET Framework 3.5 or below (as the maximum supported .NET Framework version for SQL Server 2008 is 3.5)
  2. Implement the necessary structures
  3. Register the assembly into SQL
  4. Register the aggregate functions into SQL
These are the main basic steps to make your own SQL custom user-defined aggregate functions. So, now lets explain each step.

1. Create a class library project targeting .NET Framework 3.5 or below
You can do so by creating a new visual studio project of type class library. This is straight forward.

2. Implement the necessary structures
To do this, you need to know that:
  • An aggregate is created by defining a "Struct" not a "Class"
  • This struct must be decorated with the "SqlUserDefinedAggregate" attribute.
  • This attribute has some options which we need to set properly to achieve the desired behavior
    • Format:
      • Serialization format for the struct
      • The possible values are Native, Unknown and UserDefined
      • This attribute decides how SQL will serialize and deserialize your struct members
      • In case of "Native" format, the framework handles all the necessary steps to serialize and deserialize the structure.
      • In case of "UserDefined" format, the framework will use the serialize and deserialize methodlogy you provide for your struct through implementing the "IBinarySerialize" interface
      • Also, in case of "UserDefined" format, we must set the "MaxByteSize" option, find it explained below
    • IsInvariantToDuplicates (bool): 
      • Decides whether receiving the same value twice or more affects the result or not
      • Sometimes you need to work on unique values, like computing the max value in a column, you don't need to re-compute or re-compare an already processed value as it is already decided if it is the max value or another value is greater
    • IsInvariantToNulls (bool):
      • Decides whether receiving a NULL value changes the result or not
      • Sometimes you need to ignore the NULL values in your operation, then, you need to set this to true, else, false
    • IsInvariantToOrder (bool):
      • Decides whether the order of values affects the result or not
    • IsNullIfEmpty (bool):
      • Decides whether an empty set results to a NULL value or not
    • Name (string):
      • Name of the aggregate method which will be used in SQL to call our aggregate function
    • MaxByteSize (int):
      • Sets the maximum size of the aggregate instance.
      • -1 represents a value larger than 8000 bytes, up to 2 gigabytes
      • This is required when using the "UserDefined" format and providing our own serialization and de-serialization logic
  • To define this struct, we must provide implementation for some methods
    • Init():
      • Used in order to initialize any needed resources or variables
      • Called only once when the aggregate is called
    • Accumulate():
      • Decides what action to take when receiving a new row value
      • Called once for each row
      • It is mistakenly thought that this method should provide the actual implementation of the aggregate but this is not a must as we will see later
    • Merge():
      • Used because sometimes if the the column to aggregate contains a large number of data the SQL CLR may split those rows into groups and aggregate each group individually and then merge the results of those groups again using the Merge() method
    • Terminate():
      • Used to free any used resources and to return the result
      • Called at the end of the evaluation procedure
    • The sequence of invocation should be like this

3. Register the assembly into SQL
This can be done as follows:
  1. After creating your class library, build your DLL
  2. Run an SQL query to register this DLL into SQL for SQL to be able to recognize and use your struct
  3. This SQL query will be provided in the examples section

4. Register the aggregate functions into SQL
This can be done by running an SQL query which will register our new aggregate function into SQL to be used later. This SQL query will be provided in the examples section.

Now, it is time to see some examples


Example 1
We want to define a new aggregate function to calculate the count of NULLs in a column.

CountNulls.cs
using System;
using System.Data;
using System.Data.SqlClient;
using System.Data.SqlTypes;
using Microsoft.SqlServer.Server;
using System.Collections.ObjectModel;
using System.Linq;
using System.Xml.Serialization;
using System.IO;
using System.Text;
using System.Collections;

namespace DevelopmentSimplyPut.SQLAggregateFunctions
{
    [System.Serializable]
    [Microsoft.SqlServer.Server.SqlUserDefinedAggregate(
       Microsoft.SqlServer.Server.Format.UserDefined,
       IsInvariantToDuplicates = false, // receiving the same value again changes the result
       IsInvariantToNulls = false,      // receiving a NULL value changes the result
       IsInvariantToOrder = true,       // the order of the values doesn't affect the result
       IsNullIfEmpty = true,            // if no values are given the result is null
       MaxByteSize = -1,
       Name = "CountNulls"              // name of the aggregate
    )]
    public struct CountNulls
 {
  private int counter;
  public void Init()
  {
   counter = 0;
  }
  public void Accumulate(object Value)
  {
   if (Value == DBNull.Value)  // count just the rows that are not equal to NULL
    counter++;
  }
  public void Merge(CountNulls Group)
  {
   this.counter += Group.Counter; // when merge is needed the counter of other groups should be added
  }

  public SqlString Terminate()
  {
   return new SqlString(counter.ToString()); //returning the results
  }
 }
}

RegisterAssemblyAndCreatingAggregate.sql
--Turning on CLR functionality
--By default, CLR is disabled in SQL Server so to turn it on
--we need to run this command against our database
EXEC sp_configure 'clr enabled', 1
GO
RECONFIGURE
GO

-- Creating the SQL assembly and linking it to the C# library DLL we created
CREATE ASSEMBLY SQLAggregateFunctions
AUTHORIZATION dbo
FROM 'C:\SQLAggregateFunctions.dll'
WITH PERMISSION_SET = SAFE
GO

CREATE AGGREGATE dbo.CountNulls (@value nvarchar(MAX)) RETURNS nvarchar(MAX)
EXTERNAL NAME SQLAggregateFunctions.[DevelopmentSimplyPut.SQLAggregateFunctions.CountNulls]
--EXTERNAL NAME SQLAssemblyName.[C#NameSpace".C#ClassName].C#MethodName


/*
DROP AGGREGATE dbo.CountNulls
DROP ASSEMBLY SQLAggregateFunctions
*/

Testing.sql
SELECT dbo.CountNulls(MyColumn)
FROM MyTable


Example 2
We want to define a new aggregate function to get the most common value in a column

MostCommon.cs
using System;
using System.Data;
using System.Data.SqlClient;
using System.Data.SqlTypes;
using Microsoft.SqlServer.Server;
using System.Collections.ObjectModel;
using System.Linq;
using System.Xml.Serialization;
using System.IO;
using System.Text;
using System.Collections;

namespace DevelopmentSimplyPut.SQLAggregateFunctions
{
    [System.Serializable]
    [Microsoft.SqlServer.Server.SqlUserDefinedAggregate(
       Microsoft.SqlServer.Server.Format.UserDefined,
       IsInvariantToDuplicates = false, // receiving the same value again changes the result
       IsInvariantToNulls = false,      // receiving a NULL value changes the result
       IsInvariantToOrder = true,       // the order of the values doesn't affect the result
       IsNullIfEmpty = true,            // if no values are given the result is null
       MaxByteSize = -1,
       Name = "MostCommon"              // name of the aggregate
    )]
    public struct MostCommon : IBinarySerialize
    {
        private Collection<string> pool;
        private string result;
        private int count;

        public void Init()
        {
            pool = new Collection<string>();
            result = string.Empty;
            count = -1;
        }
        public void Accumulate(string value)
        {
            if (null != value)
            {
                pool.Add(value.ToString());
            }
        }
        public void Merge(MostCommon group)
        {
            if (null != group.pool && group.pool.Count > 0)
            {
                foreach (string entry in group.pool)
                {
                    pool.Add(entry);
                }
            }
        }
        public SqlString Terminate()
        {
            string[] distinctValues = pool.Distinct().ToArray();

            foreach (string distinctValue in distinctValues)
            {
                int tempCount = pool.Count(s => s.Equals(distinctValue, StringComparison.OrdinalIgnoreCase));

                if (tempCount > count)
                {
                    count = tempCount;
                    result = distinctValue;
                }
            }

            return new SqlString(result.ToString());
        }

        #region IBinarySerialize Members
        public void Read(System.IO.BinaryReader reader)
        {
            this.result = reader.ReadString();
            this.count = reader.ReadInt32();
            int itemsCount = reader.ReadInt32();
            
            pool = new Collection<string>();

            for (int i = 0; i < itemsCount; i++)
         {
             this.pool.Add(reader.ReadString());
         } 
        }
        public void Write(System.IO.BinaryWriter writer)
        {
            writer.Write(this.result);
            writer.Write(this.count);
            writer.Write(pool.Count);
            
            foreach (string entry in pool)
            {
                writer.Write(entry);
            } 
        }
        #endregion
    }
}

RegisterAssemblyAndCreatingAggregate.sql
--Turning on CLR functionality
--By default, CLR is disabled in SQL Server so to turn it on
--we need to run this command against our database
EXEC sp_configure 'clr enabled', 1
GO
RECONFIGURE
GO

-- Creating the SQL assembly and linking it to the C# library DLL we created
CREATE ASSEMBLY SQLAggregateFunctions
AUTHORIZATION dbo
FROM 'C:\SQLAggregateFunctions.dll'
WITH PERMISSION_SET = SAFE
GO

CREATE AGGREGATE dbo.MostCommon (@value nvarchar(MAX)) RETURNS nvarchar(MAX)
EXTERNAL NAME SQLAggregateFunctions.[DevelopmentSimplyPut.SQLAggregateFunctions.MostCommon]
--EXTERNAL NAME SQLAssemblyName.[C#NameSpace".C#ClassName].C#MethodName


/*
DROP AGGREGATE dbo.MostCommon
DROP ASSEMBLY SQLAggregateFunctions
*/

CreateTestDB.sql
USE [TestDB01]
GO

SET ANSI_NULLS ON
GO

SET QUOTED_IDENTIFIER ON
GO

CREATE TABLE [dbo].[T1](
 [Col1] [nvarchar](100) NOT NULL,
 [Col2] [int] NOT NULL,
 [Col3] [int] NOT NULL,
 CONSTRAINT [PK_T1] PRIMARY KEY CLUSTERED 
(
 [Col1] ASC,
 [Col2] ASC,
 [Col3] ASC
)WITH (PAD_INDEX  = OFF, STATISTICS_NORECOMPUTE  = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS  = ON, ALLOW_PAGE_LOCKS  = ON) ON [PRIMARY]
) ON [PRIMARY]

GO


INSERT INTO T1(Col1, Col2, Col3)
VALUES('Ahmed', 1, 1)
, ('Ahmed', 1, 2)
, ('Ahmed', 1, 5)
, ('Ahmed', 1, 6)
, ('Ahmed', 2, 2)
, ('Ahmed', 3, 4)
, ('Ahmed', 4, 2)
, ('Ahmed', 5, 2)
, ('Ahmed', 6, 2)
, ('Tarek', 2, 1)
, ('Tarek', 2, 3)
, ('Tarek', 2, 4)
, ('Tarek', 2, 5)
, ('Tarek', 2, 7)
, ('Tarek', 2, 9)
, ('Tarek', 3, 5)
, ('Tarek', 4, 5)
, ('Tarek', 6, 5)
, ('Hasan', 2, 3)
, ('Hasan', 2, 7)
, ('Hasan', 5, 0)
, ('Hasan', 5, 1)
, ('Hasan', 5, 4)
, ('Hasan', 8, 6)

Test.sql
USE [TestDB01]
GO

SELECT
Col1
, (SELECT dbo.MostCommon(Col2) FROM T1 where main.Col1 = Col1 GROUP BY Col1) AS CommonCol2
, (SELECT dbo.MostCommon(Col3) FROM T1 where main.Col1 = Col1 GROUP BY Col1) AS CommonCol3
FROM T1 AS main
GROUP BY Col1


That's it for now. You can read more about this topic on the internet and know more about what you can and can't do using this technique. You can make use of the resources below.


Resources
  1. Custom Aggregates in SQL Server - CodeProject
  2. How to implement your own aggregate function in SQLCLR, SQL Server 2005 - Bashar Kokash' Blog
  3. System.Data.SqlTypes Namespace ()
  4. IBinarySerialize Interface (Microsoft.SqlServer.Server)


2013-02-27

Items Combinations Generation Library

[Update] This library is now replaced with a new one. The new one is more advanced and enhanced. You can check it on Possibilities Cube Library - A Library Smart Enough To Calculate All Possibilities With Logical Conditions


Sometimes you have a set of items (could be numbers, names, custom entities, .....) and you need to get all possible combinations each consists of a given number of items from the bigger set.

To understand what I am saying, imagine that you need to write a program by which a user can define some birthday gifts. Each gift has an id, name, description, price, ...... Your program is expected to provide all possible combinations of gifts that a father can get to his son given that the number of gift items doesn't exceed 5 and the price doesn't exceed LE 150.

To solve the problem above, which is a mathematical problem in the first place, you can use some of the mathematical algorithms. But, only for the sake of demonstration, I will assume that we will use a brute force approach to solve the problem.

So, to do the job, at some point in your code, you will need to generate all the possibilities and combinations of gift items that the father can buy for his son. Sure not all these combinations are valid and you will need to filter them according to your business but at least it is a starting point.

So, to get all possible combinations, you can use the same logic used in the binary tables. For example, in binary, any bit can be 0 or 1 and nothing else. So, if we need to have all possible sets consisting of 2 bits, we will get
0 , 0
0 , 1
1 , 0
1 , 1

These are all possible combinations you can get in this case. But, in case of sets consisting of 3 bits instead of 2, we will get
0 , 0 , 0
0 , 0 , 1
0 , 1 , 0
0 , 1 , 1
1 , 0 , 0
1 , 0 , 1
1 , 1 , 0
1 , 1 , 1

and so on........

So, this leads us to the "Items Combinations Generation Library" that I am presenting. This library provides a class with some methods which you can use to get all possible combinations of any set of items. It returns an array of indexes of items you have. So, when you get [(0 , 0), (0 , 1), (1 , 0), (1 , 1)] these numbers refer to the indexes of the items in your collection of items.

Ok, this is the time for the code.


The Library Code

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

namespace DevelopmentSimplyPut.CommonUtilities
{
    public class Possibilities
    {
        #region Properties
        int numberOfItems;
        public int NumberOfItems
        {
            get { return numberOfItems; }
            set 
            {
                if (value > 0)
                {
                    numberOfItems = value;
                }
                else
                {
                    throw new Exception("NumberOfItems must be +ve and greater than 0.");
                }
            }
        }
        int numberOfInstancesPerPossibility;
        public int NumberOfInstancesPerPossibility
        {
            get { return numberOfInstancesPerPossibility; }
            set
            {
                if (value > 0)
                {
                    numberOfInstancesPerPossibility = value;
                }
                else
                {
                    throw new Exception("NumberOfInstancesPerPossibility must be +ve and greater than 0.");
                }
            }
        }
        int maxRowIndex;
        int maxColumnIndex;
        #endregion Properties

        #region Constructors
        public Possibilities(int _numberOfItems, int _numberOfInstancesPerPossibility)
        {
            NumberOfItems = _numberOfItems;
            NumberOfInstancesPerPossibility = _numberOfInstancesPerPossibility;
            maxRowIndex = intPow(NumberOfItems, NumberOfInstancesPerPossibility) - 1;
            maxColumnIndex = NumberOfInstancesPerPossibility - 1;
        }
        #endregion Constructors

        #region Methods
        public int[,] GetPossibilities()
        {
            int[,] result = new int[maxRowIndex + 1, maxColumnIndex + 1];

            for (int i = 0; i <= maxRowIndex; i++)
            {
                int[] rowResults = GetPossiblityByIndex(i);
                for (int k = 0; k < rowResults.Length; k++)
                {
                    result[i, k] = rowResults[k];
                }
            }
            
            return result;
        }
        public int[] GetPossiblityByIndex(int rowIndex)
        {
            int[] result = null;

            if (rowIndex >= 0)
            {
                if (rowIndex <= maxRowIndex)
                {
                    result = new int[maxColumnIndex + 1];

                    for (int i = 0; i <= maxColumnIndex; i++)
                    {
                        result[i] = GetPossiblityByIndex(rowIndex, i);
                    }
                }
                else
                {
                    throw new Exception(string.Format("rowIndex can not be greater than {0}", maxRowIndex));
                }
            }
            else
            {
                throw new Exception("rowIndex must be +ve or equal to 0.");
            }

            return result;
        }
        public int GetPossiblityByIndex(int rowIndex, int columnIndex)
        {
            int result = 0;

            if (rowIndex >= 0 && columnIndex >= 0)
            {
                if (rowIndex > maxRowIndex)
                {
                    throw new Exception(string.Format("rowIndex can not be greater than {0}", maxRowIndex));
                }
                else if (columnIndex > maxColumnIndex)
                {
                    throw new Exception(string.Format("columnIndex can not be greater than {0}", maxColumnIndex));
                }
                else
                {
                    int numberOfHops = intPow(NumberOfItems, columnIndex);
                    result = GetPossiblityByIndex(NumberOfItems, numberOfHops, rowIndex);
                }
            }
            else
            {
                throw new Exception("rowIndex and columnIndex must be +ve or equal to 0.");
            }

            return result;
        }
        private int GetPossiblityByIndex(int numberOfItems, int numberOfHops, int rowIndex)
        {
            int result = 0;
            int maxItemIndex = numberOfItems - 1;
            result = rowIndex / numberOfHops;
            result = result % numberOfItems;
            return result;
        }
        private int intPow(int a, int b)
        {
            int result = 0;

            if (0 == b)
            {
                result = 1;
            }
            else if (1 == b)
            {
                result = a;
            }
            else
            {
                result = a;
                for (int i = 0; i < b - 1; i++)
                {
                    result *= a;
                }
            }
            
            return result;
        }
        #endregion Methods
    }
}


Testing Windows Forms Application

MainForm.Designer.cs
namespace TestApp
{
    partial class MainForm
    {
        /// <summary>
        /// Required designer variable.
        /// </summary>
        private System.ComponentModel.IContainer components = null;

        /// <summary>
        /// Clean up any resources being used.
        /// </summary>
        /// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
        protected override void Dispose(bool disposing)
        {
            if (disposing && (components != null))
            {
                components.Dispose();
            }
            base.Dispose(disposing);
        }

        #region Windows Form Designer generated code

        /// <summary>
        /// Required method for Designer support - do not modify
        /// the contents of this method with the code editor.
        /// </summary>
        private void InitializeComponent()
        {
            this.btnRun = new System.Windows.Forms.Button();
            this.label1 = new System.Windows.Forms.Label();
            this.label2 = new System.Windows.Forms.Label();
            this.txtNumOfItems = new System.Windows.Forms.TextBox();
            this.txtNumOfInstances = new System.Windows.Forms.TextBox();
            this.rtxtOutput = new System.Windows.Forms.RichTextBox();
            this.SuspendLayout();
            // 
            // btnRun
            // 
            this.btnRun.Location = new System.Drawing.Point(264, 6);
            this.btnRun.Name = "btnRun";
            this.btnRun.Size = new System.Drawing.Size(47, 54);
            this.btnRun.TabIndex = 0;
            this.btnRun.Text = "Run";
            this.btnRun.UseVisualStyleBackColor = true;
            this.btnRun.Click += new System.EventHandler(this.btnRun_Click);
            // 
            // label1
            // 
            this.label1.AutoSize = true;
            this.label1.Location = new System.Drawing.Point(4, 12);
            this.label1.Name = "label1";
            this.label1.Size = new System.Drawing.Size(85, 13);
            this.label1.TabIndex = 1;
            this.label1.Text = "Number of items";
            // 
            // label2
            // 
            this.label2.AutoSize = true;
            this.label2.Location = new System.Drawing.Point(4, 40);
            this.label2.Name = "label2";
            this.label2.Size = new System.Drawing.Size(105, 13);
            this.label2.TabIndex = 2;
            this.label2.Text = "Number of instances";
            // 
            // txtNumOfItems
            // 
            this.txtNumOfItems.Location = new System.Drawing.Point(116, 9);
            this.txtNumOfItems.Name = "txtNumOfItems";
            this.txtNumOfItems.Size = new System.Drawing.Size(137, 20);
            this.txtNumOfItems.TabIndex = 3;
            // 
            // txtNumOfInstances
            // 
            this.txtNumOfInstances.Location = new System.Drawing.Point(115, 40);
            this.txtNumOfInstances.Name = "txtNumOfInstances";
            this.txtNumOfInstances.Size = new System.Drawing.Size(137, 20);
            this.txtNumOfInstances.TabIndex = 4;
            // 
            // rtxtOutput
            // 
            this.rtxtOutput.Location = new System.Drawing.Point(8, 66);
            this.rtxtOutput.Name = "rtxtOutput";
            this.rtxtOutput.Size = new System.Drawing.Size(299, 217);
            this.rtxtOutput.TabIndex = 5;
            this.rtxtOutput.Text = "";
            // 
            // MainForm
            // 
            this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F);
            this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
            this.ClientSize = new System.Drawing.Size(315, 291);
            this.Controls.Add(this.rtxtOutput);
            this.Controls.Add(this.txtNumOfInstances);
            this.Controls.Add(this.txtNumOfItems);
            this.Controls.Add(this.label2);
            this.Controls.Add(this.label1);
            this.Controls.Add(this.btnRun);
            this.Name = "MainForm";
            this.Text = "Test Application";
            this.ResumeLayout(false);
            this.PerformLayout();

        }

        #endregion

        private System.Windows.Forms.Button btnRun;
        private System.Windows.Forms.Label label1;
        private System.Windows.Forms.Label label2;
        private System.Windows.Forms.TextBox txtNumOfItems;
        private System.Windows.Forms.TextBox txtNumOfInstances;
        private System.Windows.Forms.RichTextBox rtxtOutput;
    }
}

MainForm.cs
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using System.IO;
using DevelopmentSimplyPut.CommonUtilities;

namespace TestApp
{
    public partial class MainForm : Form
    {
        public MainForm()
        {
            InitializeComponent();
        }

        private void btnRun_Click(object sender, EventArgs e)
        {
            rtxtOutput.Text = string.Empty;

            Possibilities container = new Possibilities(int.Parse(txtNumOfItems.Text), int.Parse(txtNumOfInstances.Text));
            int[,] allPossibilities = container.GetPossibilities();

            for (int i = 0; i < allPossibilities.GetLength(0); i++)
            {
                for (int k = 0; k < allPossibilities.GetLength(1); k++)
                {
                    rtxtOutput.Text += allPossibilities[i, k].ToString() + " , ";
                }

                rtxtOutput.Text += Environment.NewLine;
            }

            MessageBox.Show("Done");
        }
    }
}

Running this program you will get the results in the screenshots below

Items Combinations Generation Library

Items Combinations Generation Library

Items Combinations Generation Library


Finally, you can download the source code from here


2013-02-22

Extensible Logging Library For Sharepoint With ULS Logging Support

What I am presenting here is a library used for logging in Sharepoint 2010.

What makes this library a good choice?
  1. Its code is clean and maintainable
  2. Its code is extensible as you can easily add support to many third party logging services with just some few code lines
  3. It provides a single entry point static class for usage instead of many classes

Which logging services does this library currently support?
Currently the library supports the OOTB Unified Logging Service (ULS) but for sure you can add the support for other logging services like log4net and others with just some few lines of code.


Is the code simple or complicated?
  1. The first code part: The main library code is simple and follow the "Bridge" and "Factory" design patterns. I had already wrote a post on these two design patterns with an example of how to use them. If you didn't read this post and it is your first time to hear about these two design patterns, I really encourage you to read One Of The Methodologies To Write Clean, Maintainable & Extensible Software
  2. The second code part: The main entry point static class which enables you to use the library in an easy way. This class is so simple and clear
  3. The third code part: The implementations of the logging services you wish the library can support. The complexity of this part depends on the logging service itself. Some may be simple and others may be complex

Can we see the code?

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

namespace DevelopmentSimplyPut.CommonUtilities.Logging
{
    public enum LoggingLevel
    {
        Debug = 0,
        Info = 1,
        Warn = 2,
        Error = 3,
        Fatal = 4
    }

    public interface ILogger
    {
        void Configure(Dictionary<string,object> settings);
        void Log(string message, LoggingLevel level);
        void Log(Exception exception, LoggingLevel level);
        void Log(Exception exception, string message, LoggingLevel level);
    }

    public abstract class Logger : ILogger
    {
        public abstract void Configure(Dictionary<string, object> settings);
        public abstract void Log(string message, LoggingLevel level);
        public abstract void Log(Exception exception, LoggingLevel level);
        public abstract void Log(Exception exception, string message, LoggingLevel level);
    }
}

LoggerFactory.cs:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using DevelopmentSimplyPut.CommonUtilities.Logging.ULS;

namespace DevelopmentSimplyPut.CommonUtilities.Logging
{
    public static class LoggerFactory
    {
        public static ILogger GetLoggerInstance(SystemLoggerType loggerType, Dictionary<string, object> settings)
        {
            ILogger result = null;

            switch (loggerType)
            {
                case SystemLoggerType.ULS:
                    result = ULSLogger.Current;
                    break;
                default:
                    result = ULSLogger.Current;
                    break;
            }

            result.Configure(settings);
            return result;
        }
    }
}

SystemLogger.cs:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using DevelopmentSimplyPut.CommonUtilities.Helpers;

namespace DevelopmentSimplyPut.CommonUtilities.Logging
{
    public enum SystemLoggerType
    {
        ULS = 0
        //, Log4Net = 1
    }

    public static class SystemLogger
    {
        private static ILogger logger;
        private static void SetLogger(SystemLoggerType loggerType)
        {
            switch (loggerType)
            {
                case SystemLoggerType.ULS:
                    Dictionary<string, object> settings = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);
                    settings.Add("ProductDiagName", "DevelopmentSimplyPut");
                    logger = LoggerFactory.GetLoggerInstance(SystemLoggerType.ULS, settings);
                    break;
            }
        }

        /// <summary>
        /// Gets current initialized system logger. If not already initialized, initializes a new default logger and returns it.
        /// </summary>
        public static ILogger Logger
        {
            get
            {
                if (logger == null)
                {
                    SetLogger(SystemLoggerType.ULS);
                }
                return logger;
            }
        }
        /// <summary>
        /// Resets current system logger to the default logger and finally returns this logger instance.
        /// </summary>
        /// <returns></returns>
        public static ILogger ResetLogger()
        {
            return ResetLogger(SystemLoggerType.ULS);
        }
        /// <summary>
        /// Resets current system logger to a specific logger type and finally returns this logger instance.
        /// </summary>
        /// <param name="loggerType"></param>
        /// <returns></returns>
        public static ILogger ResetLogger(SystemLoggerType loggerType)
        {
            SetLogger(loggerType);
            return logger;
        }
    }
}

ULSLogger.cs:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.SharePoint.Administration;
using System.Globalization;

namespace DevelopmentSimplyPut.CommonUtilities.Logging.ULS
{
    public class ULSLogger : Logger
    {
        private string productDiagName = string.Empty;
        private ULSLogger()
        {
        }
        
        private static ULSLoggingService CurrentServices
        {
            get;
            set;
        }

        private static ULSLogger current;
        public static ULSLogger Current
        {
            get
            {
                if (current == null)
                {
                    current = new ULSLogger();
                }
                return current;
            }
        }
        
        public override void Configure(Dictionary<string, object> settings)
        {
            if (settings != null && settings.Count > 0 && settings.Any(record => record.Key.ToUpperInvariant() == "ProductDiagName".ToUpperInvariant()))
            {
                productDiagName = Convert.ToString(settings["ProductDiagName"]);
                CurrentServices = new ULSLoggingService(productDiagName);
            }
        }

        public override void Log(string message, LoggingLevel level)
        {
            Log(null, message, level);
        }
        public override void Log(Exception exception, LoggingLevel level)
        {
            Log(exception, exception.Message + Environment.NewLine + Environment.NewLine + exception.StackTrace, level);
        }
        public override void Log(Exception exception, string message, LoggingLevel level)
        {
            string msg =
                (null == exception) ? (message) :
                string.Format(CultureInfo.InvariantCulture, "{0}\n\n{1}\n\n{2}", message, exception.Message, exception.StackTrace);

            CurrentServices.Log(msg, level);
        }
    }
}

ULSLoggingService.cs:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.SharePoint.Administration;

namespace DevelopmentSimplyPut.CommonUtilities.Logging.ULS
{
    public class ULSLoggingService : SPDiagnosticsServiceBase
    {
        public ULSLoggingService(string productName) : base(productName, SPFarm.Local)
        {
        }
        
        protected override IEnumerable<SPDiagnosticsArea> ProvideAreas()
        {
            List<SPDiagnosticsArea> areas = new List<SPDiagnosticsArea>
            {
                new SPDiagnosticsArea
                (Name, new List<SPDiagnosticsCategory>
                    {
                        new SPDiagnosticsCategory("Debug", TraceSeverity.None, EventSeverity.None),
                        new SPDiagnosticsCategory("Error", TraceSeverity.Unexpected, EventSeverity.Error),
                        new SPDiagnosticsCategory("Info", TraceSeverity.Monitorable, EventSeverity.Information),
                        new SPDiagnosticsCategory("Warn", TraceSeverity.Medium, EventSeverity.Warning),
                        new SPDiagnosticsCategory("Fatal", TraceSeverity.High, EventSeverity.ErrorCritical),
                    }
                )
            };

            return areas;
        }

        private void LogDebug(string message)
        {
            SPDiagnosticsCategory category = Areas[Name].Categories["Debug"];
            WriteTrace(0, category, category.TraceSeverity, message);
        }
        private void LogError(string message)
        {
            SPDiagnosticsCategory category = Areas[Name].Categories["Error"];
            WriteTrace(0, category, category.TraceSeverity, message);
        }
        private void LogInfo(string message)
        {
            SPDiagnosticsCategory category = Areas[Name].Categories["Info"];
            WriteTrace(0, category, category.TraceSeverity, message);
        }
        private void LogWarn(string message)
        {
            SPDiagnosticsCategory category = Areas[Name].Categories["Warn"];
            WriteTrace(0, category, category.TraceSeverity, message);
        }
        private void LogFatal(string message)
        {
            SPDiagnosticsCategory category = Areas[Name].Categories["Fatal"];
            WriteTrace(0, category, category.TraceSeverity, message);
        }
        public void Log(string message, LoggingLevel level)
        {
            switch (level)
            {
                case LoggingLevel.Debug:
                    LogDebug(message);
                    break;
                case LoggingLevel.Error:
                    LogError(message);
                    break;
                case LoggingLevel.Fatal:
                    LogFatal(message);
                    break;
                case LoggingLevel.Info:
                    LogInfo(message);
                    break;
                case LoggingLevel.Warn:
                    LogWarn(message);
                    break;
                default:
                    LogDebug(message);
                    break;
            }
        }
    }
}


How to use this library?
catch (NullReferenceException ex)
{
 SystemLogger.Logger.Log(ex, LoggingLevel.Error);
}

or

catch (NullReferenceException ex)
{
 SystemLogger.Logger.Log(ex, "This is custom error message for logging", LoggingLevel.Error);
}

or

catch (NullReferenceException ex)
{
 SystemLogger.Logger.Log("This is custom error message for logging", LoggingLevel.Warn);
}

or

SystemLogger.Logger.Log("This is just a hint message for logging", LoggingLevel.Info);

This is just a sample of what you can do using this library. You can browse through the code and you will get the whole thing. For sure you can customize the code to add any other functionality you wish to have or modify an existing one.

This code is already used in commercial solutions and it proved to be working efficiently among the regular needs but you can apply your changes as I said before.


That's it, hope you find this library useful and I will be waiting for your feedback.
Bye.


2013-02-01

How To Call WCF Web Service With Authentication Credentials

When you deal with WCF web services, sometimes you need to call a web service with certain authentication credentials -username (with/without domain) and password- to be able to take some high privilege actions.

Me myself faced such case and tried to find the proper way to do it. So, I found that the reference I created to the web service has a property called "Credentials" through which I can provide my credentials.

But, when I tried my code, I found that my request is still not authenticated. After further investigations, I decided to search msdn for all properties related to the authentication topic to see if I am missing something.

So, I found that my reference class to my web service inherits from "System.Web.Services.Protocols.SoapHttpClientProtocol" class. So, by the aid of google, I found some interesting results. It is not enough to set the "Credentials" property of my reference. I have three more properties to set to achieve what I want, so lets see some code.

//Creating a reference to your WCF web service
MyCustomService service = new MyCustomService();

/*
AllowAutoRedirect is "true" to automatically redirect the client to follow server redirects; otherwise, "false". The default is "false".

If you send authentication information, such as a user name and password,
you do not want to enable the server to redirect, because this can compromise security
*/
service.AllowAutoRedirect = false;

/*
PreAuthenticate is "true" to pre-authenticate the request; otherwise, "false". The default is "false".

When PreAuthenticate is false, a request is made to the Web service method
without initially attempting to authenticate the user. If the Web service allows anonymous
access, then the Web service method is executed, else, a 401 HTTP return code is sent back.
*/
service.PreAuthenticate = true;

/*
UseDefaultCredentials is "true" if the Credentials property is set to the value of the CredentialCache.DefaultCredentials property; otherwise, "false".
*/
service.UseDefaultCredentials = false;

//Providing credentials
service.Credentials = new NetworkCredential("userName", "password", "domain");

That's it, after setting the four properties "AllowAutoRedirect", "PreAuthenticate", "UseDefaultCredentials" and "Credentials" properly I could get my call to my web service authenticated.


References
System.Web.Services.Protocols.SoapHttpClientProtocol
HttpWebClientProtocol.AllowAutoRedirect
WebClientProtocol.PreAuthenticate
WebClientProtocol.UseDefaultCredentials
WebClientProtocol.Credentials