ESB 2.0 Custom Resolver: Part 1

Recently I encountered a situation where we wanted to leverage SQL server as and repository for endpoints. The resolver consists of two parts: the repository (SQL Table in this example) and the custom resolver assembly. I will be focused on the custom resolver for this  discussion.

The custom resolver consists of an assembly and configuration entries. In part one we will discuss the code that goes into the assembly. In part two we will talk about wiring up the assembly so that the ESB will use the new assembly, as well as how to make the resolver configurable.

Per the pattern set up by the ESB Guidance team, each resolver should be in its own assembly. This assembly will need to include references to the following assemblies:

  • Microsoft.Practices.ESB.Cache
  • Microsoft.Practices.ESB.Common
  • Microsoft.Practices.ESB.Configuration
  • Microsoft.Practices.ESB.Exception.Management
  • Microsoft.Practices.ESB.Resolver
  • Microsoft.XLANGs.BaseTypes

The naming convention for the assembly is ESB.Resolver.MyResolver. In our example the assembly is ESB.Resolver.SQL. A class that implements IResolverProvider is required for the custom resolver. The naming convention for this class is ResolveProvider.

The IResolverProvider interface implements three overloaded variations of the Resolve method. Each of these need to be implemented to ensure that the resolver works in all traditional contexts. All three methods should be executing the same logic. With that in mind, the ESB Guidance team created a ResolveMyResolver(string config, string resolver, Dictionary<string, string> resolutionOrig) method that each Resolve method calls. I have included my sample below.

private Dictionary<string, string> ResolveSql(string config, string resolver, Dictionary<string, string> resolutionOrig)
        {
            // fix the resolver if it needs it
            if (!resolver.Contains(ResolutionHelper.MonikerSeparator))
                resolver = resolver + ResolutionHelper.MonikerSeparator;

            try
            {
                // check for cache...return data structure
                Dictionary<string, string> sqlValues = RetrieveFromCache(config);

                //No Resolution cached
                if (sqlValues == null)
                {
                    sqlValues = new Dictionary<string, string>();

                    //Extract Parameters
                    Dictionary<string, string> queryParams = ResolutionHelper.GetFacts(config, resolver);
                    string serverName =
ResolutionHelper.GetConfigValue(queryParams, false, "serverName");
                    string dbName = ResolutionHelper.GetConfigValue(queryParams, false, "dbName");
                    string serviceName = ResolutionHelper.GetConfigValue(queryParams, false, "serviceName");
                    //Build connection String
                    string connectionString = String.Format("Data Source={0}; Initial Catalog={1}; Integrated Security=SSPI;", serverName, dbName);
                    //Execute SQL query to retrieve EndPoint
                    SqlDataReader sqlReader = null;

                    try
                    {
                        sqlReader = ResolveSqlEndpoint(serviceName, connectionString, sqlReader);
                        if (sqlReader.HasRows)
                        {
                            sqlReader.Read();

                            //Transfer result to Resolution
                            Resolution resolution = new Resolution();
                            resolution.EndpointConfig = sqlReader["EndPointConfig"].ToString();
                            resolution.TransportLocation = sqlReader["TransportLocation"].ToString();
                            resolution.FixJaxRpc = Convert.ToBoolean(sqlReader["JaxRPC"]);
                            resolution.MessageExchangePattern = sqlReader["MessageExchangePattern"].ToString();
                            resolution.TransportType = sqlReader["TransportType"].ToString();
                            resolution.TransportNamespace = sqlReader["TransportNamespace"].ToString();
                            resolution.Action = sqlReader["Action"].ToString();

                            //Transfer results to the Dictionary
                            ResolverMgr.SetResolverDictionary(resolution, sqlValues);

                            if (null != sqlValues)
                                resolutionOrig = sqlValues;
                        }

                        sqlReader.Close();

                    }
                    catch (Exception ex)
                    {
                        EventLogger.Write(MethodInfo.GetCurrentMethod(), ex);
                        throw;
                    }

                    //add result to cache
                    SetCache(sqlValues, config);
                }
            }
            catch (Exception ex)
            {
                EventLogger.Write(MethodInfo.GetCurrentMethod(), ex);
                throw;
            }

            return resolutionOrig;
        }

Stepping through the code, the first step is to repair the resolver string, if needed. The resolver string is also referred to as the Moniker. A Moniker is formatted as follows Resolver:\\. For example the UDDI Moniker is UDDI:\\. In our example, our Moniker is SQL:\\. This first code snippet simply applies the :\\ if it missing.

The next step retrieves the resolution from the cache, if it exists. We will dive into this more in depth in part two.

Next, the parameters are extracted from the config string. The config string is the list of key/values that are included with the Moniker to create the EndPoint string. Our config string contains three key/values: Server Name, DB Name, and Service Name. These values are required to query our SQL repository. The ResolutionHelper.GetConfigValue method is used to extract the parameters from the config string.

Next, the SQL connection string is created and the SqlDataReader is initialized. A stored procedure that takes the Service Name and returns the resolution data for that Service is executed to populate the SqlDataReader. A Resolution object is instantiated and the values from the SqlDataReader are used to populate values in the Resolution class. The Resolution object is then used to create the output of the method using the ResolverMgr.SetResolverDictionary method.

The final step before returning the Dictionary result is to add the resolution to the cache. Caching will be described in more depth in part two.

In part two we will discuss caching, configuration, and wiring up the custom resolver to the ESB.

No comments: