0

I've been experimenting with .config files in Azure-Functions.

If I write this function

using System;
using System.Configuration;
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;

using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.Http;
using Microsoft.Azure.WebJobs.Host;


namespace GranadaCoder.AzurePoc.AzureFunctionsOne
{
    public static class AppSettingsTestOne
    {
        [FunctionName("AppSettingsTestOneFunctionName")]
        public static async Task<HttpResponseMessage> Run([HttpTrigger(AuthorizationLevel.Function, "post", Route = null)]HttpRequestMessage req, TraceWriter log)
        {

            try
            {

                string rootDirectory = string.Empty;
                if (!string.IsNullOrEmpty(Environment.GetEnvironmentVariable("HOME")))
                {
                    /* running in azure */
                    rootDirectory = Environment.GetEnvironmentVariable("HOME") + "\\site\\wwwroot";
                }
                else
                {
                    /* in visual studio, local debugging */
                    rootDirectory = ".";
                }
                string path = rootDirectory + @"\CustomConfigFiles\CustomAppSettings.config";

                if (!System.IO.File.Exists(path))
                {
                    throw new System.IO.FileNotFoundException(string.Format("NOT FOUND!!! ('{0}')", path));
                }
                else
                {
                    log.Info(string.Format("File exists='{0}'", path));
                }

                ExeConfigurationFileMap map = new ExeConfigurationFileMap { ExeConfigFilename = path };
                Configuration config = ConfigurationManager.OpenMappedExeConfiguration(map, ConfigurationUserLevel.None);

                Configuration fileConfig = ConfigurationManager.OpenExeConfiguration(path); /* does NOT work */
                string val1 = config.AppSettings.Settings["KeyOne"].Value;
                string val2 = config.AppSettings.Settings["KeyTwo"].Value;
                string val3 = config.AppSettings.Settings["KeyThree"].Value;

                string msg = string.Join(",", val1, val2, val3);

                return req.CreateResponse(HttpStatusCode.OK, msg);
            }
            catch (Exception ex)
            {
                string errorMsg = ex.Message; //  ExceptionHelper.GenerateFullFlatMessage(ex);
                log.Error(errorMsg);
                return req.CreateResponse(HttpStatusCode.BadRequest, errorMsg);
            }
        }
    }
}

with this .config file (CustomAppSettings.config)

<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <appSettings>
    <add key="KeyOne" value="ValueOne" />
    <add key="KeyTwo" value="ValueTwo" />
    <add key="KeyThree" value="ValueThree" />
  </appSettings>
</configuration>

It works as anticipated.

If I use this function:

using System;
using System.Collections.Specialized;
using System.Configuration;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;
using System.Xml;

using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.Http;
using Microsoft.Azure.WebJobs.Host;


namespace GranadaCoder.AzurePoc.AzureFunctionsOne
{
    public static class NameValuePairAppSettingsTest
    {
        [FunctionName("NameValuePairAppSettingsTestFunctionName")]
        public static async Task<HttpResponseMessage> Run([HttpTrigger(AuthorizationLevel.Function, "post", Route = null)]HttpRequestMessage req, TraceWriter log)
        {

            try
            {
                string rootDirectory = string.Empty;
                if (!string.IsNullOrEmpty(Environment.GetEnvironmentVariable("HOME")))
                {
                    /* running in azure */
                    rootDirectory = Environment.GetEnvironmentVariable("HOME") + "\\site\\wwwroot";
                }
                else
                {
                    /* in visual studio, local debugging */
                    rootDirectory = ".";
                }
                string path = rootDirectory + @"\CustomConfigFiles\NameValuePairSettings.config";


                if (!System.IO.File.Exists(path))
                {
                    throw new System.IO.FileNotFoundException(string.Format("NOT FOUND!!! ('{0}')", path));
                }
                else
                {
                    log.Info(string.Format("file exists='{0}'", path));
                }

                ExeConfigurationFileMap map = new ExeConfigurationFileMap { ExeConfigFilename = path };
                Configuration config = ConfigurationManager.OpenMappedExeConfiguration(map, ConfigurationUserLevel.None);

                //NameValueCollection nvc = (NameValueCollection)config.GetSection("myLittleArea"); /* does not work */

                ConfigurationSection myParamsSection = config.GetSection("myLittleArea");
                /* see https://stackoverflow.com/questions/13825323/how-do-i-get-the-values-from-a-configsection-defined-as-namevaluesectionhandler */
                string myParamsSectionRawXml = myParamsSection.SectionInformation.GetRawXml();
                XmlDocument sectionXmlDoc = new XmlDocument();
                sectionXmlDoc.Load(new StringReader(myParamsSectionRawXml));
                NameValueSectionHandler handler = new NameValueSectionHandler();
                NameValueCollection nvc = handler.Create(null, null, sectionXmlDoc.DocumentElement) as NameValueCollection;

                var items = nvc.AllKeys.SelectMany(nvc.GetValues, (k, v) => new { key = k, value = v });
                ////////foreach (var item in items)
                ////////{
                ////////    Console.WriteLine("{0} {1}", item.key, item.value);
                ////////}

                string msg = string.Join(",", items.ToList());

                return req.CreateResponse(HttpStatusCode.OK, msg);
            }
            catch (Exception ex)
            {
                string errorMsg = ex.Message; //  ExceptionHelper.GenerateFullFlatMessage(ex);
                log.Error(errorMsg);
                return req.CreateResponse(HttpStatusCode.BadRequest, errorMsg);
            }
        }
    }
}

with this .config file (NameValuePairSettings.config)

<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <configSections>
    <section name="myLittleArea" type="System.Configuration.NameValueSectionHandler, System, Version=1.0.5000.0,Culture=neutral, PublicKeyToken=b77a5c561934e089" />
  </configSections>

  <myLittleArea>
    <add key="color" value="red"/>
    <add key="street" value="main"/>
    <add key="month" value="july"/>
    <add key="candy" value="snickers"/>
  </myLittleArea>

</configuration>

Everything works ok.

(Drum Roll).

If I create a custom configuration section.

using System.Configuration;

namespace GranadaCoder.AzurePoc.ConfigurationLibrary.MyCustomConfigurationSettings
{
    public static class MyCustomConfigurationSettingsConfigurationRetriever
    {
        public static readonly string ConfigurationSectionName = "MyCustomConfigurationSettingsConfigurationSectionName";

        /*
        public static MyCustomConfigurationSettingsConfigurationSection GetMyCustomConfigurationSettings()
        {
            MyCustomConfigurationSettingsConfigurationSection returnSection = (MyCustomConfigurationSettingsConfigurationSection)ConfigurationManager.GetSection(ConfigurationSectionName);
            if (returnSection != null)
            {
                return returnSection;
            }

            return null;
        }
        */

        public static MyCustomConfigurationSettingsConfigurationSection GetMyCustomConfigurationSettings(System.Configuration.Configuration cfg)
        {
            MyCustomConfigurationSettingsConfigurationSection returnSection = (MyCustomConfigurationSettingsConfigurationSection)cfg.GetSection(ConfigurationSectionName);
            if (returnSection != null)
            {
                return returnSection;
            }

            return null;
        }
    }
}

and

using System.Configuration;

namespace GranadaCoder.AzurePoc.ConfigurationLibrary.MyCustomConfigurationSettings
{
    public class MyCustomConfigurationSettingsConfigurationSection : ConfigurationSection
    {
        private const string FavoriteNumberPropertyName = "FavoriteNumber";
        private const string FavoriteColorPropertyName = "FavoriteColor";

        [ConfigurationProperty(FavoriteNumberPropertyName, IsRequired = true, DefaultValue = 100)]
        public int FavoriteNumber
        {
            get
            {
                return (int)this[FavoriteNumberPropertyName];
            }
        }

        [ConfigurationProperty(FavoriteColorPropertyName, IsRequired = true, DefaultValue = ",")]
        public string FavoriteColor
        {
            get
            {
                return (string)this[FavoriteColorPropertyName];
            }
        }
    }
}

and .config (MyCustomConfigurationSettings.config)

<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <configSections>
    <section name ="MyCustomConfigurationSettingsConfigurationSectionName" type="GranadaCoder.AzurePoc.ConfigurationLibrary.MyCustomConfigurationSettings.MyCustomConfigurationSettingsConfigurationSection, GranadaCoder.AzurePoc.ConfigurationLibrary" />
  </configSections>
  <MyCustomConfigurationSettingsConfigurationSectionName
    FavoriteNumber="333"
    FavoriteColor="Green"
  >
  </MyCustomConfigurationSettingsConfigurationSectionName>
</configuration>

and azure function code

using System;
using System.Configuration;
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;

using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.Http;
using Microsoft.Azure.WebJobs.Host;

using GranadaCoder.AzurePoc.ConfigurationLibrary.MyCustomConfigurationSettings;



namespace GranadaCoder.AzurePoc.AzureFunctionsOne
{
    public static class CustomConfigurationTest
    {
        [FunctionName("CustomConfigurationTestFunctionName")]
        public static async Task<HttpResponseMessage> Run([HttpTrigger(AuthorizationLevel.Function, "post", Route = null)]HttpRequestMessage req, TraceWriter log)
        {
            try
            {
                string rootDirectory = string.Empty;
                if (!string.IsNullOrEmpty(Environment.GetEnvironmentVariable("HOME")))
                {
                    /* running in azure */
                    rootDirectory = Environment.GetEnvironmentVariable("HOME") + "\\site\\wwwroot";
                }
                else
                {
                    /* in visual studio, local debugging */
                    rootDirectory = ".";
                }
                string path = rootDirectory + @"\CustomConfigFiles\MyCustomConfigurationSettings.config";

                log.Info(string.Format("CustomConfigurationTestFunctionName HostingEnvironment.ApplicationPhysicalPath='{0}'", System.Web.Hosting.HostingEnvironment.ApplicationPhysicalPath));

                if (!System.IO.File.Exists(path))
                {
                    throw new System.IO.FileNotFoundException(string.Format("NOT FOUND!!! ('{0}')", path));
                }
                else
                {
                    log.Info(string.Format("File exists='{0}'", path));
                }

                ExeConfigurationFileMap map = new ExeConfigurationFileMap { ExeConfigFilename = path };
                Configuration config = ConfigurationManager.OpenMappedExeConfiguration(map, ConfigurationUserLevel.None);

                MyCustomConfigurationSettingsConfigurationSection customSection = MyCustomConfigurationSettingsConfigurationRetriever.GetMyCustomConfigurationSettings(config);

                string msg = string.Join(",", customSection.FavoriteNumber.ToString(), customSection.FavoriteColor);

                return req.CreateResponse(HttpStatusCode.OK, msg);
            }
            catch (Exception ex)
            {
                string errorMsg = ex.Message; //  ExceptionHelper.GenerateFullFlatMessage(ex);
                log.Error(errorMsg);
                return req.CreateResponse(HttpStatusCode.BadRequest, errorMsg);
            }
        }
    }
}

The above does not work.

I get an error

"An error occurred creating the configuration section handler for MyCustomConfigurationSettingsConfigurationSectionName: Could not load file or assembly 'GranadaCoder.AzurePoc.ConfigurationLibrary' or one of its dependencies. The system cannot find the file specified. (C:\blah\blah\blah\bin\Debug\net461\CustomConfigFiles\MyCustomConfigurationSettings.config line 4)"

The file IS THERE (see image)

Any idea why the custom configuration does not work?

enter image description here

Janusz Nowak
  • 2,595
  • 1
  • 17
  • 36
granadaCoder
  • 26,328
  • 10
  • 113
  • 146
  • Why have you decided to use a custom configuration implementation and not the 'normal' approach of using the ConfigurationManager? When using the default approach you can leverage the functionality of Azure and Release Management (any other deployment system). Some more information on configuring your Functions can be found in this SO question: https://stackoverflow.com/a/45681977/352640 But if there's a valid reason for you not to use it, just ignore this comment. I am still curious though why you want to implement it like this. – Jan_V Sep 09 '17 at 11:39

2 Answers2

1

After printed out the BaseDirectory of current domain in the Azure Function, I found that the function is ran by fun.exe. It will look for the assembly in "AppData\Local\Azure.Functions.Cli\1.0.1\" folder. After copied the "GranadaCoder.AzurePoc.ConfigurationLibrary" to the folder, the function will work fine.

Code:

string friendlyName = AppDomain.CurrentDomain.FriendlyName;
string baseDirectory = AppDomain.CurrentDomain.BaseDirectory;

Output:

BaseDirectory = "C:\\Users\\myusername\\AppData\\Local\\Azure.Functions.Cli\\1.0.1\\"
FriendlyName = "func.exe"
Amor
  • 8,325
  • 2
  • 19
  • 21
  • You solved the riddle. I don't like that this is how it might work......(kills xcopy deployment scenarios)..........but it does work now. You have to keep the files very up to date......or you get this error. (next comment) – granadaCoder Sep 11 '17 at 14:46
  • [A]GranadaCoder.AzurePoc.ConfigurationLibrary.MyCustomConfigurationSettings.MyCustomConfigurationSettingsConfigurationSection cannot be cast to [B]GranadaCoder.AzurePoc.ConfigurationLibrary.MyCustomConfigurationSettings.MyCustomConfigurationSettingsConfigurationSection. – granadaCoder Sep 11 '17 at 15:39
  • Type A originates from 'GranadaCoder.AzurePoc.ConfigurationLibrary, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null' in the context 'Default' at location 'C:\Users\MyUserName\AppData\Local\Azure.Functions.Cli\1.0.0-beta.99\GranadaCoder.AzurePoc.ConfigurationLibrary.dll'. – granadaCoder Sep 11 '17 at 15:40
  • Type B originates from 'GranadaCoder.AzurePoc.ConfigurationLibrary, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null' in the context 'LoadFrom' at location 'C:\MyDeployment\GranadaCoder.AzurePoc.AzureFunctionsOne\bin\Debug\net461\GranadaCoder.AzurePoc.ConfigurationLibrary.dll'. – granadaCoder Sep 11 '17 at 15:40
  • Clarification on my previous statement. The .dll that contains the "logic" (of interpreting the custom configuration) will have to be in that directory where the func.exe is. BUT the settings themselves can go side-saddle with the function .dll. – granadaCoder Sep 11 '17 at 18:18
  • I still find it interesting that it does not look in the /bin/ directory of the deployment........like dotnet has been doing for 17+ years. – granadaCoder Sep 11 '17 at 18:19
1

Azure function supports only limited part of app.config. It allows to save app settings and connections in local.settings.json when running function from VS. It don't support WCF endpoint settings under system.serviceModel in this json file. I had a dll library reference in AzureFunction and that was internally calling WCF apis.

Strange thing I found is, when I run the Azure function, it converts back the json to xml config at the cli path (C:\Users\<< machine name >>\AppData\Local\AzureFunctionsTools\Releases\1.6.0\cli\func.exe.config). I added my xml configuration hierarchy (system.serviceModel) to this config file and it worked fine, picking my WCF endpoints to run the services. Though have struggles in using log4net configuration but am good to run the APIs.

Hope this helps.

Phantom
  • 101
  • 6