16

I have an issue getting a DbContext to correctly pull my connection string from my local.settings.json

Context:

  • This is an Azure function project
  • The main problem code is in System.Data.Entity.Internal.AppConfig
  • Although I have a local.settings.json file this is not dotnet core. It's .net 4.6.1

Error message:

'The connection string 'ShipBob_DevEntities' in the application's configuration file does not contain the required providerName attribute."'

Json configuration:

{
  "IsEncrypted": false,
  "Values": {
    "AzureWebJobsStorage": "",
    "AzureWebJobsDashboard": ""
},

"ConnectionStrings": {
"ShipBob_DevEntities": {
  "ConnectionString": "metadata=res://*/Model1.csdl|res://*/Model1.ssdl|res://*/Model1.msl;provider=System.Data.SqlClient;provider connection string='data source=***;initial catalog=***;persist security info=True;User Id=***;Password=***;;multipleactiveresultsets=True;application name=EntityFramework'",
  "providerName": "System.Data.EntityClient"
    }
  }
}  

Configuration versions tested:

  • Moving the provider name into the actual ConnectionString token value : same error ocurrs
  • Setting the provider attribute inside the ConnectionString attribute to EntityClient: this did nothing
  • Making ShipBob_DevEntities a string value = to the value of ConnectionString : this throws new errors the likes of which are

    keyword metadata is not supported

  • I tried using an ADO connection string which throws a code first exception which seems to occur when your connection string is incorrect in a database first approach.

I've taken the liberty to decompile EntityFramework.dll using dotPeek and have traced the problem down to System.Data.Entity.Internal.LazyInternalConnection.TryInitializeFromAppConfig. Inside this method there is a call to LazyInternalConnection.FindConnectionInConfig which spits out a ConnectionStringSettings object that has it's ProviderName value set to null. Unfortunately I am unable to debug the AppConfig.cs class which it seems to use to generate this value so I am stuck.

enter image description here

So far I have consulted these two articles. One of which states to put the provider name as it's own token; however, this is not working.

https://github.com/Azure/azure-functions-cli/issues/193
https://github.com/Azure/azure-functions-cli/issues/46

Does anyone know the correct format to use in local.settings.json for an Entity Framework connection?

Adrian
  • 3,332
  • 5
  • 34
  • 52

4 Answers4

16

I went through several similar questions and answers here. Many of them are either misleading or assuming everybody is on the same level and understands how the azure functions are working. there is no answer for newbies like me. I would like to summarize here my solution step by step. I dont think that provided answer is the best option because it forces you to change the auto generated edmx files which can be overwritten by mistake or next update of your edmx from database. Also best option here is to use Connection strings instead of App settings in my opinion.

  1. most important thing is that we understand local.settings.json file IS NOT FOR AZURE. it is to run your app in the local as the name is clearly saying. So solution is nothing to do with this file.

  2. App.Config or Web.Config doesnt work for Azure function connection strings. If you have Database Layer Library you cant overwrite connection string using any of these as you would do in Asp.Net applications.

  3. In order to work with, you need to define your connection string on the azure portal under the Application Settings in your Azure function. There is Connection strings. there you should copy your connection string of your DBContext. if it is edmx, it will look like as below. There is Connection type, I use it SQlAzure but I tested with Custom(somebody claimed only works with custom) works with both.

metadata=res:///Models.myDB.csdl|res:///Models.myDB.ssdl|res://*/Models.myDB.msl;provider=System.Data.SqlClient;provider connection string='data source=[yourdbURL];initial catalog=myDB;persist security info=True;user id=xxxx;password=xxx;MultipleActiveResultSets=True;App=EntityFramework

  1. After you set this up, You need to read the url in your application and provide the DBContext. DbContext implements a constructor with connection string parameter. By default constructor is without any parameter but you can extend this. if you are using POCO class, you can amend DbContext class simply. If you use Database generated Edmx classes like me, you dont want to touch the auto generated edmx class instead of you want to create partial class in the same namespace and extend this class as below.

This is auto generated DbContext

namespace myApp.Data.Models
{   

    public partial class myDBEntities : DbContext
    {
        public myDBEntities()
           : base("name=myDBEntities")
        {
        }

        protected override void OnModelCreating(DbModelBuilder modelBuilder)
        {
            throw new UnintentionalCodeFirstException();
        }

}

this is the new partial class, you create

namespace myApp.Data.Models
{
    [DbConfigurationType(typeof(myDBContextConfig))]
    partial class myDBEntities
    {

        public myDBEntities(string connectionString) : base(connectionString)
        {
        }
    }

      public  class myDBContextConfig : DbConfiguration
        {
            public myDBContextConfig()
            {
                SetProviderServices("System.Data.EntityClient", 
                SqlProviderServices.Instance);
                SetDefaultConnectionFactory(new SqlConnectionFactory());
            }
        }
    }
  1. After all you can get the connection string from azure settings, in your Azure Function project with the code below and provide to your DbContext myDBEntities is the name you gave in the azure portal for your connection string.
var connString = ConfigurationManager.ConnectionStrings["myDBEntities"].ConnectionString;


 using (var dbContext = new myDBEntities(connString))
{
        //TODO:
}
Emil
  • 6,411
  • 7
  • 62
  • 112
  • 1
    your approach is better than the accepted answer. Thanks a lot. However, for Step 5, I have added a base class for my repository method and extracted the connection string in there and passed that property to DBContext Ctor. – Jawand Singh Jan 25 '18 at 06:10
  • @JawandSingh indeed it is almost the same thing. If you are using POCO classes, it is easier to do that but using Database Generated Edmx file, i didnt want to change my DBContext as it might be overwritten later. therefore I did it using a partial class – Emil Jan 26 '18 at 12:56
  • 1
    Hey @batmaci I'm coming back after reviewing your answer. I just want to let others know, the difference between this answer and mine is that this answer assumes you have the luxury of newing up a new dbcontext in code. In my situation I did not have the luxury of doing that as I was reusing work units and services already written. There for my solution provides a way to continue using parameterless dbcontexts without having to specify a conection string in the constructors. Just to clarify for everyone. – Adrian Apr 07 '18 at 17:25
  • Unfortunately this doesn't work on my own project with a testdatalayer - I'm beginning to think it's not intended to work on the current version of azure functions! – Richard Griffiths May 01 '18 at 10:49
  • @RichardGriffiths do you mean in unit test project? maybe you can create another ticket for your issue with more details. – Emil May 01 '18 at 13:31
  • @batmaci I've setup another question as I've tried now publishing from VS and working from the portal (two separate functions of course) and nothing I try works. The detail on my question is regarding the first version - using the portal editor and largely following your approach. I think one problem is trying to stick with .net 4.6 and similar simply because of the EF datalayer project. However, my understanding is this SHOULD work lol. https://stackoverflow.com/questions/50115519/azure-functions-can-compile-but-cannot-run-with-custom-datalayer-library – Richard Griffiths May 01 '18 at 13:41
  • This works if it is the only EF Context used in your project. I have a context in class library shared between apps, but another context in a web app using the class library. Web app context is instantiated 1st, and when I create the class lib context I get "The default DbConfiguration instance was used by the Entity Framework before the 'AdminDBContextConfig' type was discovered. An instance of 'AdminDBContextConfig' must be set at application start before using any Entity Framework features or must be registered in the application's config file. (etc)" – jrichview Aug 21 '18 at 17:37
13

So the solution ended up being trivial. The ProviderName attribute specified in local.settings.json MUST be camel case.

From the original git hub discussions :
https://github.com/Azure/azure-functions-cli/issues/46
Shows the provider name as being pascal case

https://github.com/Azure/azure-functions-cli/issues/193
Shows the provider name being camel case in pseudo code It was very easy to miss but your config section must be exactly as follows

"ConnectionStrings": {
"ShipBob_DevEntities": {
  "ConnectionString": "metadata=res://*/Model1.csdl|res://*/Model1.ssdl|res://*/Model1.msl;provider=System.Data.SqlClient;provider connection string='data source=***;initial catalog=***;persist security info=True;User Id=***;Password=***;;multipleactiveresultsets=True;application name=EntityFramework'",
  "ProviderName":  "System.Data.EntityClient"
  }
}  

These points are important:

  • Make sure your connection string has metadata information
  • If copying your string from an xml config, make sure you unescape apostrophes
  • Make sure the ProviderName attribute is camel case
  • Make sure the provider name is System.Data.EntityClient

Fix for missing providername in deployment

Note, this answer assumes you are trying to use the parameterless constructor of a DbContext. If you are creating new code you can easily follow the second upvoted answer

I figured out a way to circumvent the provider name issue while still retaining the use of the portal config and thus deployment slots. It involves setting the default connection string of db context using static properties

private static string _connectionString = "name=ShipBob_DevEntities";

    static ShipBob_DevEntities()
    {
        if(!string.IsNullOrEmpty(System.Environment.GetEnvironmentVariable("AzureFunction")))
        {
            var connectionString = System.Environment.GetEnvironmentVariable("EntityFrameworkConnectionString");

            if (!string.IsNullOrEmpty(connectionString))
            {
                _connectionString = connectionString;
            }
        }
    }

    public ShipBob_DevEntities()
        : base(_connectionString)
    {
        this.Configuration.LazyLoadingEnabled = false;
    }  

This involves the developer to create an app setting in the azure portal as a flag. In my case it is AzureFunction. This makes sure that our code is only run in an azure function and all other clients of this DbContext, whether they be web apps, windows apps, etc, can still continue behaving as expected. This also involves adding your connection string to the azure portal as an AppSetting and not an actual connection string. Please use the full connection string including them metadata information but without the provider name!

EDIT

You will need to edit your auto generated .tt file t4 template to make sure this code does not get overridden if you are using db first.

Here is a link on the T4 syntax: https://learn.microsoft.com/en-us/visualstudio/modeling/writing-a-t4-text-template

And here is an explanation on EF T4 templates: https://msdn.microsoft.com/en-us/library/jj613116(v=vs.113).aspx#1159a805-1bcf-4700-9e99-86d182f143fe

Community
  • 1
  • 1
Adrian
  • 3,332
  • 5
  • 34
  • 52
  • Exactly how does this bypasses the providerName issue in production? I understand in local you're passing the ProviderName but in prod you're still missing that. Do you have any special stuff in the entityframeworkconnectionstring or is it identical to what you have in the local settings file (connectionstring) ? – Mavi Domates Oct 18 '17 at 23:16
  • Actually if you have an app.config or web.config can you please share that as well - or at least the relevant bits – Mavi Domates Oct 18 '17 at 23:17
  • Hey Mavi, so the connection string that is being read by the constructor is the same exact connection string that is found in the local.settings.json. The only difference is the `ProviderName` is left out of it. There is no need to specify that in the constructor of a db context. I'm assuming because it's already implied that the provider is entity framework if you are using a dbcontext. I do not have any `app.config` or `web.config`'s at all. I simply add the 2 keys mentioned above to my app settings in the actual portal. – Adrian Oct 19 '17 at 04:36
  • Example: place this as an `appsetting` in your azure portal: `metadata=res://*/Model1.csdl|res://*/Model1.ssdl|res://*/Model1.msl;provider=System.Data.SqlClient;provider connection string='data source=***;initial catalog=***;persist security info=True;User Id=***;Password=***;;multipleactiveresultsets=True;application name=EntityFramework'` Then follow my code sample above to retrieve it. Make sure your property and constructors are static so they run before the instance constructors – Adrian Oct 19 '17 at 04:40
  • I've hardcoded the string and I'm still getting this error – Mavi Domates Oct 19 '17 at 19:20
  • I started a chat with you so we can talk there – Adrian Oct 19 '17 at 21:10
  • 1
    Oh! found what I was doing wrong! It was another method which initialized the entities without the hardcoded string (so still looking for the config values in a file). This method works. Thanks a lot Adrian – Mavi Domates Oct 23 '17 at 13:05
  • why do you use the AppSettings instead of using adding a new ConnectionString in the Portal? does this not work? – Emil Jan 04 '18 at 18:52
  • It has been a while since I've messed with this code but I believe the issue is that you cannot access the connection strings via `Environment.GetEnvironmentVariable` as only the app settings are loaded as environment variables. Also there are differences in the way you access these variables in local dev and in prod. For example, I also believe you cannot access app settings in prod by using ConfigurationManager but in local you can. It should be easy to test. – Adrian Jan 04 '18 at 22:20
  • Thank you for your answer. Beside that I am using EF edmx from database generated. Is it a good approach to amend Auto generated Context file? it will be overwritten anyway with the next update. there should be a safer way – Emil Jan 08 '18 at 11:31
  • 2
    Also you can use ConfigurationManager to get Connection string from Azure Function settings. – Emil Jan 08 '18 at 14:54
  • Hello batmaci, I saw your answer. If it works for others awesome! In response to your question above, yes the edmx files get overwritten unless you edit the t4 templates. I mentioned that in the edit. – Adrian Jan 09 '18 at 02:32
  • Batmaci, can you use ConfigurationManager to get the connection strings on a deployed environment? – Adrian Jan 09 '18 at 20:22
  • I am confused. Your example uses Pascal case, but you say you **MUST** use camel case? – SvenAelterman Oct 18 '19 at 20:19
  • Pro tip: if you copy and paste the Entity Framework connection string from an app.config file, replace the " by a single quote (`'`). – SvenAelterman Oct 18 '19 at 20:31
0

I encountered the similar issue before, I would use the following approach for achieving my purpose, you could refer to it:

local.settings.json

{
  "IsEncrypted": false,
  "Values": {
    "AzureWebJobsStorage": "DefaultEndpointsProtocol=https;AccountName=brucchstorage;AccountKey=<AccountKey>",
    "AzureWebJobsDashboard": "DefaultEndpointsProtocol=https;AccountName=brucchstorage;AccountKey=<AccountKey>",
    "sqldb-connectionstring": "Data Source=.\\sqlexpress;Initial Catalog=DefaultConnection;Integrated Security=True;Connect Timeout=15;Encrypt=False;TrustServerCertificate=True;ApplicationIntent=ReadWrite;MultiSubnetFailover=False"
  },
  "ConnectionStrings": {
    "Bruce_SQLConnectionString": "Data Source=.\\sqlexpress;Initial Catalog=DefaultConnection;Integrated Security=True;Connect Timeout=15;Encrypt=False;TrustServerCertificate=True;ApplicationIntent=ReadWrite;MultiSubnetFailover=False"
  }
} 

For retrieving the connection string:

var connString = ConfigurationManager.AppSettings["sqldb-connectionstring"];
//or var connString = ConfigurationManager.ConnectionStrings["Bruce_SQLConnectionString"].ConnectionString;
using (var dbContext = new BruceDbContext(connString))
{
    //TODO:
}

Or you could init your no-argument constructor for your DbContext as follows:

public class BruceDbContext:DbContext
{
    public BruceDbContext()
        : base("Bruce_SQLConnectionString")
    {
    }

    public BruceDbContext(string connectionString) : base(connectionString)
    {
    }
}

Then, you could create the instance for your DbContext as follows:

using (var dbContext = new BruceDbContext(connString))
{
    //TODO:
}

Moreover, you could refer to Local settings file for Azure Functions.

Bruce Chen
  • 18,207
  • 2
  • 21
  • 35
  • Thank you for the response. This is something I would gladly do; however, I'm trying to use a bunch of existing data access code which has the dbcontext being created with the parameterless constructor. I'm trying to avoid having to edit all these other services. I just took a quick look at the link you provided. This may help. – Adrian Oct 12 '17 at 15:53
  • I found the issue. I will add an answer – Adrian Oct 12 '17 at 17:28
0

Here are two approaches that work for me:

Approach 1

  • Add the connection string to the App Settings (respectively local.settings.json) in the following format:

metadata=res:///xxx.csdl|res:///xxx.ssdl|res://*/xxx.msl;provider=System.Data.SqlClient;provider connection string='data source=xxx.database.windows.net;initial catalog=xxx;user id=xxx;password=xxx;MultipleActiveResultSets=True;App=EntityFramework'`

  • Go to the class that extends DbContext ("TestEntities") and extend the constructor to take the connection string as argument
public partial class TestEntities: DbContext
{
    public TestEntities(string connectionString)
        : base(connectionString)
    {
    }
  • If you want then to interact with the database you need to retrieve the connection string from the app settings and then pass it over when initializing DbContext
string connectionString = Environment.GetEnvironmentVariable("connectionStringAppSettings");

using (var dbContext = new TestEntities(connectionString))
{
// Do Something
}
  • The problem with this approach is that every time you update the database you need to update the class "TestEntities" as it is overwritten

Approach 2

  • The goal here is to leave the class "TestEntities" as is to avoid the issue from Approach 1

  • Add the connection string to the App Settings (respectively local.settings.json) like in Approach 1

  • Leave TestEntities as is

public partial class TestEntities : DbContext
    {
        public TestEntities ()
            : base("name=TestEntities")
        {
        }
  • As TestEntities is partial you can extend that class by creating another one that is also partial with the same name in the same namespace. The goal of this class is to provide the constructor that takes the connection string as argument

public partial class TestEntities
{
    public TestEntities(string connectionString)
        : base(connectionString)
    {
    }
}
  • Then you can go on like with Approach 1
quervernetzt
  • 10,311
  • 6
  • 32
  • 51