async Task<bool> RefreshConfigData()
        {
            // Get the latest config data:
            SHA1Hashed<JObject> config;
            try
            {
                // TODO: add HTTP fetch first and failover to file, like we did before.
                config = await FetchConfigDataFile();
            }
            catch (Exception ex)
            {
                // Create a service collection that represents the parser error:
                this.services = SHA1Hashed.Create(
                    new ServicesOffering(
                        new ServiceCollection
                        {
                            Errors = new List<string>
                            {
                                "{0}".F(ex.Message)
                            },
                            // Empty dictionary is easier than dealing with `null`:
                            Services = new Dictionary<string, Service>(0, StringComparer.OrdinalIgnoreCase)
                        },
                        (JObject)null
                    ),
                    SHA1Hashed.Zero
                );

                return false;
            }

            Debug.Assert(config != null);

            // Parse the config object:
            var tmp = ParseConfigData(config.Value);
            Debug.Assert(tmp != null);

            // Store the new service collection paired with the JSON hash:
            this.services = SHA1Hashed.Create(new ServicesOffering(tmp, config.Value), config.Hash);
            return true;
        }
 IHttpResponseAction errorsService(SHA1Hashed<ServicesOffering> main, Service service)
 {
     return new JsonRootResponse(
         links: new RestfulLink[]
         {
         },
         meta: new
         {
             configHash = main.HashHexString,
             serviceName = service.Name,
             serviceErrors = service.Errors,
             methodsErrors = service.Methods.Where(m => m.Value.Errors.Any()).ToDictionary(
                 m => m.Key,
                 m => new { errors = m.Value.Errors },
                 StringComparer.OrdinalIgnoreCase
             )
         }
     );
 }
 IHttpResponseAction errorsAll(SHA1Hashed<ServicesOffering> main)
 {
     return new JsonRootResponse(
         links: new RestfulLink[]
         {
         },
         meta: new
         {
             configHash = main.HashHexString,
             rootErrors = main.Value.Services.Errors,
             servicesErrors =
                 main.Value.Services.Services.Where(s => s.Value.Errors.Any() || s.Value.Methods.Any(m => m.Value.Errors.Any())).ToDictionary(
                     s => s.Key,
                     s => new
                     {
                         serviceErrors = s.Value.Errors,
                         methodsErrors = s.Value.Methods.Where(m => m.Value.Errors.Any()).ToDictionary(
                             m => m.Key,
                             m => new { errors = m.Value.Errors },
                             StringComparer.OrdinalIgnoreCase
                         )
                     },
                     StringComparer.OrdinalIgnoreCase
                 )
         }
     );
 }
 IHttpResponseAction configAll(SHA1Hashed<ServicesOffering> main)
 {
     return new JsonRootResponse(
         links: new RestfulLink[]
         {
         },
         meta: new
         {
             configHash = main.HashHexString,
             config = main.Value.Config
         }
     );
 }
 IHttpResponseAction errorsMethod(SHA1Hashed<ServicesOffering> main, Service service, Method method)
 {
     return new JsonRootResponse(
         links: new RestfulLink[]
         {
         },
         meta: new
         {
             configHash = main.HashHexString,
             serviceName = service.Name,
             methodName = method.Name,
             methodErrors = method.Errors
         }
     );
 }
 IHttpResponseAction debugAll(SHA1Hashed<ServicesOffering> main)
 {
     // Report all service descriptors as links:
     return new JsonRootResponse(
         links: main.Value.Services.Services.Select(p => RestfulLink.Create(p.Key, "/debug/{0}".F(p.Key))).ToArray(),
         meta: new
         {
             configHash = main.HashHexString
         }
     );
 }
 IHttpResponseAction debugService(SHA1Hashed<ServicesOffering> main, Service service)
 {
     return new JsonRootResponse(
         links: new RestfulLink[]
         {
             RestfulLink.Create("self", "/debug/{0}".F(service.Name), "self"),
             RestfulLink.Create("parent", "/debug", "parent"),
             RestfulLink.Create("meta", "/meta/{0}".F(service.Name)),
             RestfulLink.Create("errors", "/errors/{0}".F(service.Name))
         },
         meta: new
         {
             configHash = main.HashHexString,
             service = new ServiceDebug(service)
         }
     );
 }
 IHttpResponseAction debugMethod(SHA1Hashed<ServicesOffering> main, Service service, Method method)
 {
     return new JsonRootResponse(
         links: new RestfulLink[]
         {
             RestfulLink.Create("self", "/debug/{0}/{1}".F(service.Name, method.Name), "self"),
             RestfulLink.Create("parent", "/debug/{0}".F(service.Name), "parent"),
             RestfulLink.Create("meta", "/meta/{0}/{1}".F(service.Name, method.Name)),
             RestfulLink.Create("errors", "/errors/{0}/{1}".F(service.Name, method.Name)),
             RestfulLink.Create("data", "/data/{0}/{1}".F(service.Name, method.Name))
         },
         meta: new
         {
             configHash = main.HashHexString,
             method = new MethodDebug(method)
         }
     );
 }
        async Task<IHttpResponseAction> dataMethod(SHA1Hashed<ServicesOffering> main, Method method, System.Collections.Specialized.NameValueCollection queryString)
        {
            // Check for descriptor errors:
            if (method.Errors.Count > 0)
            {
                return new JsonRootResponse(
                    statusCode: 500,
                    statusDescription: "Bad method descriptor",
                    message: "Bad method descriptor",
                    meta: new
                    {
                        configHash = main.HashHexString,
                        serviceName = method.Service.Name,
                        methodName = method.Name,
                        errors = method.Errors.ToArray()
                    }
                );
            }

            // Check required parameters:
            if (method.Parameters != null)
            {
                // Create a hash set of the query-string parameter names:
                var q = new HashSet<string>(queryString.AllKeys, StringComparer.OrdinalIgnoreCase);

                // Create a list of missing required parameter names:
                var missingParams = new List<string>(method.Parameters.Count(p => !p.Value.IsOptional));
                missingParams.AddRange(
                    from p in method.Parameters
                    where !p.Value.IsOptional && !q.Contains(p.Key)
                    select p.Key
                );

                if (missingParams.Count > 0)
                    return new JsonRootResponse(
                        statusCode: 400,
                        statusDescription: "Missing required parameters",
                        message: "Missing required parameters",
                        meta: new
                        {
                            configHash = main.HashHexString,
                            serviceName = method.Service.Name,
                            methodName = method.Name
                        },
                        errors: new[]
                        {
                            new
                            {
                                missingParams = missingParams.ToDictionary(
                                    p => p,
                                    p => new ParameterSerialized(method.Parameters[p]),
                                    StringComparer.OrdinalIgnoreCase
                                )
                            }
                        }
                    );

                missingParams = null;
            }

            // Open a connection and execute the command:
            using (var conn = new SqlConnection(method.ConnectionString))
            using (var cmd = conn.CreateCommand())
            {
                var parameterValues = new Dictionary<string, ParameterValue>(method.Parameters == null ? 0 : method.Parameters.Count);

                // Add parameters:
                if (method.Parameters != null)
                {
                    foreach (var param in method.Parameters)
                    {
                        bool isValid = true;
                        string message = null;
                        object sqlValue, clrValue;
                        var paramType = (param.Value.SqlType ?? param.Value.Type);
                        string rawValue = queryString[param.Key];

                        if (param.Value.IsOptional & (rawValue == null))
                        {
                            // Use the default value if the parameter is optional and is not specified on the query-string:
                            sqlValue = param.Value.DefaultSQLValue;
                            clrValue = param.Value.DefaultCLRValue;
                        }
                        else
                        {
                            try
                            {
                                sqlValue = getSqlValue(paramType.SqlDbType, rawValue);
                                if (sqlValue == null)
                                {
                                    isValid = false;
                                    message = "Unsupported SQL type '{0}'".F(paramType.SqlDbType);
                                }
                            }
                            catch (Exception ex)
                            {
                                isValid = false;
                                sqlValue = DBNull.Value;
                                message = ex.Message;
                            }

                            try
                            {
                                clrValue = getCLRValue(paramType.SqlDbType, rawValue);
                            }
                            catch { clrValue = null; }
                        }

                        parameterValues.Add(param.Key, isValid ? new ParameterValue(clrValue) : new ParameterValue(message, rawValue));

                        // Add the SQL parameter:
                        var sqlprm = cmd.Parameters.Add(param.Value.Name, paramType.SqlDbType);
                        sqlprm.IsNullable = param.Value.IsOptional;
                        if (paramType.Length != null) sqlprm.Precision = (byte)paramType.Length.Value;
                        if (paramType.Scale != null) sqlprm.Scale = (byte)paramType.Scale.Value;
                        sqlprm.SqlValue = sqlValue;
                    }
                }

                // Abort if we have invalid parameters:
                var invalidParameters = parameterValues.Where(p => !p.Value.isValid);
                if (invalidParameters.Any())
                {
                    return new JsonRootResponse(
                        statusCode: 400,
                        statusDescription: "Invalid parameter value(s)",
                        message: "Invalid parameter value(s)",
                        meta: new
                        {
                            configHash = main.HashHexString,
                            serviceName = method.Service.Name,
                            methodName = method.Name
                        },
                        errors: invalidParameters.Select(p => (object)new { name = p.Key, attemptedValue = p.Value.attemptedValue, message = p.Value.message }).ToArray()
                    );
                }

                //cmd.CommandTimeout = 360;   // seconds
                cmd.CommandType = CommandType.Text;
                // Set TRANSACTION ISOLATION LEVEL and optionally ROWCOUNT before the query:
                const string setIsoLevel = "SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;\r\n";
                var sbCmd = new StringBuilder(setIsoLevel, setIsoLevel.Length + method.Query.SQL.Length);
                //if (rowLimit > 0)
                //    sbCmd.Append("SET ROWCOUNT {0};\r\n".F(rowLimit));
                sbCmd.Append(method.Query.SQL);
                cmd.CommandText = sbCmd.ToString();

                // Stopwatches used for precise timing:
                Stopwatch swOpenTime, swExecTime, swReadTime;

                swOpenTime = Stopwatch.StartNew();
                try
                {
                    // Open the connection asynchronously:
                    await conn.OpenAsync();
                    swOpenTime.Stop();
                }
                catch (Exception ex)
                {
                    swOpenTime.Stop();
                    return getErrorResponse(ex);
                }

                // Execute the query:
                SqlDataReader dr;
                swExecTime = Stopwatch.StartNew();
                try
                {
                    // Execute the query asynchronously:
                    dr = await cmd.ExecuteReaderAsync(CommandBehavior.SequentialAccess | CommandBehavior.CloseConnection);
                    swExecTime.Stop();
                }
                catch (ArgumentException aex)
                {
                    swExecTime.Stop();
                    // SQL Parameter validation only gives `null` for `aex.ParamName`.
                    return new JsonRootResponse(400, aex.Message);
                }
                catch (Exception ex)
                {
                    swExecTime.Stop();
                    return getErrorResponse(ex);
                }

                swReadTime = Stopwatch.StartNew();
                try
                {
                    var results = await ReadResult(method, dr, RowMapperUseMapping);
                    swReadTime.Stop();

                    var meta = new MetadataSerialized
                    {
                        configHash = main.HashHexString,
                        serviceName = method.Service.Name,
                        methodName = method.Name,
                        deprecated = method.DeprecatedMessage,
                        parameters = parameterValues,
                        // Timings are in msec:
                        timings = new MetadataTimingsSerialized
                        {
                            open = Math.Round(swOpenTime.ElapsedTicks * 1000m / (decimal)Stopwatch.Frequency, 2),
                            exec = Math.Round(swExecTime.ElapsedTicks * 1000m / (decimal)Stopwatch.Frequency, 2),
                            read = Math.Round(swReadTime.ElapsedTicks * 1000m / (decimal)Stopwatch.Frequency, 2),
                            total = Math.Round((swOpenTime.ElapsedTicks + swExecTime.ElapsedTicks + swReadTime.ElapsedTicks) * 1000m / (decimal)Stopwatch.Frequency, 2),
                        }
                    };

                    return new JsonRootResponse(
                        links: new RestfulLink[]
                        {
                        },
                        meta: meta,
                        results: results
                    );
                }
                catch (JsonResultException jex)
                {
                    swReadTime.Stop();
                    return new JsonRootResponse(statusCode: jex.StatusCode, message: jex.Message);
                }
                catch (Exception ex)
                {
                    swReadTime.Stop();
                    return getErrorResponse(ex);
                }
            }
        }
        SHA1Hashed<JObject> ReadJSONStream(TextReader input)
        {
#if TRACE
            // Send the JSON to Console.Out while it's being read:
            using (var tee = new TeeTextReader(input, (line) => Console.Write(line)))
            using (var sha1 = new SHA1TextReader(tee, UTF8.WithoutBOM))
#else
            using (var sha1 = new SHA1TextReader(input, UTF8.WithoutBOM))
#endif
            using (var jr = new JsonTextReader(sha1))
            {
                // NOTE(jsd): Relying on parameter evaluation order for `sha1.GetHash()` to be correct.
                var result = new SHA1Hashed<JObject>(Json.Serializer.Deserialize<JObject>(jr), sha1.GetHash());
#if TRACE
                Console.WriteLine();
                Console.WriteLine();
#endif
                return result;
            }
        }