Richie's Blog

Hangin' with the Ministry Platform API Part 2

A second look into how to make the most out of the Ministry Platform API using reflection and some other bits of magic.

This is the second installment of the Ministry Platform API series. For a complete look, take a look at my other posts here:

  1. MP API Part 1
  2. MP API Part 3

Well we're back with another look into making the most out of the Ministry Platform API. In this post we're going to look how to make it suuuuper easy to do the basic CRUD operations against any table in the MP database. An we're going to have fun doing it. Yes, I like to write in the 3rd person, like this guy.

We're gonna start off with building a basic "wrapper" class that will handle all requests.

Getting and Persisting Credentials

In my last post you saw that the MP API requires credentials with every request. A rule of thumb in software development is that if you start copying and pasting code over and over again, it's a sign that things can be optimized. It helps to keep the logic for accessing the api username and password in one place, rather than in 50.

    private const string _passwordSettingName = "mppw";
    private const string _domainSettingName = "mpguid";
    private const string _serverSettingName = "mpserver";
    private readonly string _userName;
    private readonly string _password;
    private readonly string _serverName;


    private apiSoapClient _client;


    public MinistryPlatformDataContext()
    {
        var userName = ConfigurationManager.AppSettings[_domainSettingName];
        var pw = ConfigurationManager.AppSettings[_passwordSettingName];
        var serverName = ConfigurationManager.AppSettings[_serverSettingName];

        if (!String.IsNullOrEmpty(pw) && !String.IsNullOrEmpty(userName) && !String.IsNullOrEmpty(serverName))
        {
            _userName = userName;
            _password = pw;
            _serverName = serverName;
            _client = ObjectFactory.GetWebRequestScoped<apiSoapClient>();
            return;
        }

        if (String.IsNullOrEmpty(userName))
            throw new ConfigurationErrorsException(String.Format(CultureInfo.CurrentCulture, Messages.SettingNotFound, _domainSettingName));

        if (String.IsNullOrEmpty(pw))
            throw new ConfigurationErrorsException(String.Format(CultureInfo.CurrentCulture, Messages.SettingNotFound, _passwordSettingName));

        if (String.IsNullOrEmpty(pw))
            throw new ConfigurationErrorsException(String.Format(CultureInfo.CurrentCulture, Messages.SettingNotFound, _serverSettingName));

    }

    public MinistryPlatformDataContext(string domainGUID, string apiPassword, string serverName)
    {
        if (!String.IsNullOrEmpty(apiPassword) && !String.IsNullOrEmpty(domainGUID) && !String.IsNullOrEmpty(serverName))
        {
            _userName = domainGUID;
            _password = apiPassword;
            _serverName = serverName;
            _client = ObjectFactory.GetWebRequestScoped<apiSoapClient>();
            return;
        }

        if (String.IsNullOrEmpty(domainGUID))
            throw new ConfigurationErrorsException(String.Format(CultureInfo.CurrentCulture, Messages.SettingNotFound, _domainSettingName));

        if (String.IsNullOrEmpty(apiPassword))
            throw new ConfigurationErrorsException(String.Format(CultureInfo.CurrentCulture, Messages.SettingNotFound, _passwordSettingName));

    }

The two constructors are where the credentials are initialized. If you don't provide any (i.e. the default constructor), it's assumed the credentials are stored in the appsettings section of the web or app config of the project.

Managing Resources with an Object Factory

If you've ever had to work with any SOAP interface in the .NET world, you probably already know you have to create a SOAP object in memory and then call the various methods on it to communicate with the service. Creating and destroying it manually is fine for small projects, but when you're building projects that are relying heavily on a service such as this, you'll need to start thinking smart (or lazy, depending on how you look at it).

An object factory in software design is, to put it simply, a way to abstract the creation and management of often used objects in an application. When calling a method to create an object, the caller doesn't have to worry about how to create it or where it's coming from - that's all taken care of. The caller just says "I need an object 'xyz', give me a reference to it".

So in our example, to keep memory resources to a minimum, we'll use a simple implementation of an object factory that will create an instance of the type of object requested (in our case, an apiSoapClient which connects to MP) if it doesn't already exist, and store it in the HTTPRequest object for use during the entire web request.

using System;
using System.Threading;
using System.Web;

namespace MvcApplication1
{
    public class ObjectFactory
    {
        /// <summary> 
        /// Creates a ASP.NET Context scoped instance of an object. This static
        /// method creates a single instance and reuses it whenever this method is
        /// called.
        /// 
        /// This version creates an internal request specific key shared key that is
        /// shared by each caller of this method from the current Web request.
        /// </summary>
        public static T GetWebRequestScoped<T>()
        {
            // Create a request specific unique key 
            return (T)GetWebRequestScopedInternal(typeof(T), null, null);
        }

        /// <summary>
        /// Creates a ASP.NET Context scoped instance of an object. This static
        /// method creates a single instance and reuses it whenever this method is
        /// called.
        /// 
        /// This version lets you specify a specific key so you can create multiple 'shared'
        /// DataContexts explicitly.
        /// </summary>
        /// <typeparam name="TDataContext"></typeparam>
        /// <param name="key"></param>
        /// <returns></returns>
        public static T GetWebRequestScoped<T>(string key)
        {
            return (T)GetWebRequestScopedInternal(typeof(T), key, null);
        }


        public static T GetWebRequestScoped<T>(string key, object[] args)
        {
            return (T)GetWebRequestScopedInternal(typeof(T), key, args);
        }

        /// <summary>
        /// Internal method that handles creating a context that is scoped to the HttpContext Items collection
        /// by creating and holding the object there.
        /// </summary>
        /// <param name="type"></param>
        /// <param name="key"></param>
        /// <param name="connectionString"></param>
        /// <returns></returns>
        static object GetWebRequestScopedInternal(Type type, string key, object[] args)
        {
            object context;

            if (HttpContext.Current == null)
            {
                //if 
                if (args == null)
                    context = Activator.CreateInstance(type);
                else
                    context = Activator.CreateInstance(type, args);

                return context;
            }

            // *** Create a unique Key for the Web Request/Context 
            if (key == null)
                key = "__WRSCDC_" + HttpContext.Current.GetHashCode().ToString("x") + Thread.CurrentContext.ContextID.ToString();

            context = HttpContext.Current.Items[key];
            if (context == null)
            {
                if (args == null)
                    context = Activator.CreateInstance(type);
                else
                    context = Activator.CreateInstance(type, args);

                if (context != null)
                    HttpContext.Current.Items[key] = context;
            }

            return context;
        }
    }
}

This bit of code has some goodies (choice of key, specify args) that I won't go into detail here, but it's fairly self explanatory. If you're interested on other or better implementations, just Google 'C# Object Factory'.

Serializing Props and Attributes with Reflection

Here comes some fun. In order to make an api call to create a record, you need to specify a request string (i.e. "Company=0&First_Name=John&Last_Name=Smith"). This describes what values go into which columns in the database. Well, what if we just put some attributes on a POCO class that represents something in Ministry Platform, for example a Feedback Entry?

[Table("Feedback_Entries")]
public class FeedBackEntry
{
    [Key]
    [Column("Feedback_Entry_ID")]
    public int ID { get; set; }

    [Column("Contact_ID")]
    public int ContactID { get; set; }

    [Column("Entry_Title")]
    public string EntryTitle { get; set; }

    [Column("Feedback_Type_ID")]
    public int FeedbackTypeID { get; set; }

    [Column("Date_Submitted")]
    public DateTime DateSubmitted { get; set; }

    [Column("Visibility_Level_ID")]
    public int VisibilityLevelID { get; set; }

    [Column("Prayer_Counter")]
    public int PrayerCounter { get; set; }

    public string Description { get; set; }

    public string Notes { get; set; }

    [Column("Care_Outcome_ID")]
    public int CareOutcomeID { get; set; }

    [Column("Outcome_Date")]
    public DateTime OutcomeDate { get; set; }
}

With using attributes already available to us from System.DataAnnotations, we can not only specify which attribute is the primary key, but now we can also specify which column to map to in the database. I love this feature... I like having control of the property names in my POCO classes. You can also add the browsable attribute and set it to false if you want to hide a particular property from the serializer.

Now all we need to do to create the request string for the AddRecord call to Ministry Platform is to iterate over each property of the object in question using reflection, and build the request string based on the attributes. Sounds easy enough right?

    private string SerializeForAPI(object item)
    {
        return SerializeForAPI(item, false);
    }

    private string SerializeForAPI(object item, bool isUpdate)
    {
        var j = 0;
        //var keys = props.Keys;
        var props = item.GetType().GetProperties(BindingFlags.Instance | BindingFlags.Public);
        var sb = new StringBuilder();
        foreach (var property in props)
        {
            bool isPrimaryKey = false;
            bool isBrowsable = true; //assume all properties are fair game...

            if (!isUpdate)
            {
                //look for primary key - we'll skip this later
                KeyAttribute attr = property.GetCustomAttribute<KeyAttribute>();
                if (attr != null)
                    isPrimaryKey = true;
            }
            //look for not browsable attr - we'll skip this later as well
            BrowsableAttribute battr = property.GetCustomAttribute<BrowsableAttribute>();
            if(battr != null)
                isBrowsable = false;

            var value = property.GetValue(item, null);

            //if this property doesn't have anything in it, or it's a primary key, or it's not browsable, skip it.
            if (value != null && value.ToString() != string.Empty && !isPrimaryKey && isBrowsable)
            {
                if (value.GetType() == typeof(Boolean))
                    value = ((bool)value).APIEncode();

                ColumnAttribute colAttr = property.GetCustomAttribute<ColumnAttribute>();
                if (colAttr != null)
                    sb.Append(colAttr.Name);
                else
                    sb.Append(property.Name);

                sb.Append("=").Append(value.ToString().APIEncode());

                //only add an ampersand if there's another property to add
                if (++j < props.Count())
                    sb.Append("&");
            }
            else
                j++;


        }
        return sb.ToString();
    }

Creating Records for ANY Table

Now let's look at how we can actually wrap the AddRecord method of the API. Using generics and overloading methods, we can do the following to really simplify adding records to Ministry Platform:

    public int Create<T>(T itemToInsert)
    {
        return Create<T>(itemToInsert, 0);
    }

    public int Create<T>(T itemToInsert, int UserID)
    {
        string primaryKey, tableName = string.Empty;
        GetTableInfo(itemToInsert, out primaryKey, out tableName);
        return Create<T>(itemToInsert, UserID, primaryKey, tableName);
    }

    public int  Create<T>(T itemToInsert, int UserID, string primaryKey)
    {
        return Create<T>(itemToInsert, UserID, primaryKey, GetTableName(itemToInsert));
    }

    public int Create<T>(T itemToInsert, int UserID, string primaryKey, string tableName)
    {
        string[] response = _client.AddRecord(_userName, _password, UserID, tableName, primaryKey, SerializeForAPI(itemToInsert)).Split('|');
        HandleResponse(response, "adding a record");
        return Int32.Parse(response[1]); //returns the integer ID of the newly created record.
    }

The HandleResponse method just determines whether the call to Ministry Platform was successful or not. If it wasn't, it throws an exception. It's possible to not have an exception thrown when calling the API so we need to be alerted when Ministry Platform reports a problem.

private void HandleResponse(string[] response, string method)
{
    int mqID = Int32.Parse(response[0]);
    if (mqID == 0) //if Ministry Platform returns an error
        throw new Exception("An error occured " + method + ". " + response[2]);
}

You may be wondering what the GetTableName and GetTableInfo methods do. They just read the attributes off of the POCO class and it's properties using reflection and then pass the primary key and table name off to the AddRecord api call.

private string GetTableName(object itemToInsert)
{
    //Looks for the table attribute on the object's type. if it's there, then
    //we used the specified name. Else use the type name for the DB.
    TableAttribute tblattr = itemToInsert.GetType().GetCustomAttribute<TableAttribute>();
    if (tblattr != null)
        return tblattr.Name;
    else
        return itemToInsert.GetType().Name;
}

private void GetTableInfo(object item, out string primaryKey, out string tableName)
{
    tableName = GetTableName(item);

    //look for the primary key attribute for each property of the specified type
    foreach(var property in item.GetType().GetProperties())
    {
        KeyAttribute keyAttribute = property.GetCustomAttribute<KeyAttribute>();

        if (keyAttribute != null)
        {
            //look to see if this has a column attribute
            ColumnAttribute colAttr = property.GetCustomAttribute<ColumnAttribute>();
            if (colAttr != null)
                primaryKey = colAttr.Name;
            else
                primaryKey = property.Name;

            return;
        }
    }

    primaryKey = string.Empty;
    throw new ConfigurationErrorsException("No Primary Key defined for the type \"" + item.GetType().Name + "\". Define a primary key for this type to use the Ministry Platform API");
}

So when calling this method it boils down to a few lines of code like this:

//the class 'MinistryPlatformDataContext' contains all the code from above, our 'wrapper'
//class
var dataContext = new MinistryPlatformDataContext();
dataContext.Create<FeedBackEntry>(
    new FeddBackEntry
    {
        //populate fields here, not including primary key field
    });

Ahhh... that just looks good.

Stay Tuned

That's enough for this post. There was a lot of ground covered here. We hit a lot of conecpts like generics, method overloading, attributes and reflection, and the factory pattern to name a few. And believe it or not we still have a lot more ground to cover before we get to the end. More to come on using the ExecuteStoredProcedure, Update, and other Ministry Platform API calls. It'll be mind blowing... I promise.

Okaaaaay... I may have over shot that one a bit... haha.

Tagged with ASP.NET Ministry Platform

About this blog

Richie Faville

Tech. Software Dev. A serving of humor, spiced with adventitious talk. What's not to like? ;) Seriously though, I'm all about making the world a better place through software and other tech. Want to know more about me? Find it here.

Tags

Archive

comments powered by Disqus