0

I have a small app I am writing as an exercise to learn how to work with SQL databases in C# .NET. My app tries to open a connection to a database, and if it fails, it assumes that the database was not there and creates it. This database is used only by my app, and is always local, so it's a lot simpler than many other database situations. Here is the code, based on things I've read on SO and also in "Pro C# 7: With .NET and .NET Core", by Andrew Troelsen:

private static DbConnection connection = null;
private static MyAppModels context = null;
public static void SetupDatabase()
{
    string dataProvider = "System.Data.SqlClient";
    DbProviderFactory factory = DbProviderFactories.GetFactory(dataProvider);

    // get connection object (closes automatically at end of using scope)
    connection = factory.CreateConnection();
    if (connection == null) { /* throw Exception */ }

    string connectionString = @"Data Source=(localdb)\mssqllocaldb;Initial Catalog=MyAppDB;Integrated Security=True";
    connection.ConnectionString = connectionString;
    try { connection.Open(); }
    catch (Exception)
    {
        // database could not be found; try to create it...
        connection.ConnectionString = @"Data Source=(localdb)\mssqllocaldb;Integrated Security=True;database=master";
        connection.Open();
        string str = "CREATE DATABASE MyAppDB ON PRIMARY " +
                     "(NAME = MyApp_Data, FILENAME = 'C:\\Users\\Me\\Documents\\MyApp\\MyAppData.mdf', " +
                     "SIZE = 2MB, MAXSIZE = 50MB, FILEGROWTH = 10%) " +
                     "LOG ON (NAME = MyApp_Log, FILENAME = 'C:\\Users\\Me\\Documents\\MyApp\\MyAppLog.ldf', " +
                     "SIZE = 1MB, MAXSIZE = 10MB, FILEGROWTH = 10%)";
        DbCommand myCommand = connection.CreateCommand();
        myCommand.CommandText = str;
        try { myCommand.ExecuteNonQuery(); }
        catch (System.Exception ex) { /* throw Exception */ }
    }

    context = new MyAppModels(connection);
    return;
}

This code seems to execute without a problem until I try to add data to a table, at which point I get the following error:

"The model backing the 'MyAppModels' context has changed since the database was created. Consider using Code First Migrations to update the database (http://go.microsoft.com/fwlink/?LinkId=238269)."

If I exit my app and re-open it, it's able to connect to the now-existing database and add records without a problem. But I don't want users of my app to be required to exit and re-open the app after first installing it, just to get around this problem!

I looked as several other posts here, including:

The model backing the 'POSContext' context has changed since the database was created

The model backing the context has changed since the database was created

These both seemed to point to the same solution. Also, the second one had the following quote from a member of the Microsoft EF team:

When a model is first created, we run a DatabaseInitializer to do things like create the database if it's not there or add seed data. The default DatabaseInitializer tries to compare the database schema needed to use the model with a hash of the schema stored in an EdmMetadata table that is created with a database (when Code First is the one creating the database). Existing databases won’t have the EdmMetadata table and so won’t have the hash…and the implementation today will throw if that table is missing. We'll work on changing this behavior before we ship the fial version since it is the default. Until then, existing databases do not generally need any database initializer so it can be turned off for your context type by calling:

Database.SetInitializer<YourDbContext>(null);

I don't really understand all this. But I tried adding that code to my DbContext class anyway:

protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
    Database.SetInitializer<MyAppModels>(null);
    base.OnModelCreating(modelBuilder);
}

Now I get the following error when I try to add the first record to a table:

"An error occurred while updating the entries. See the inner exception for details."

Inner exception: "String or binary data would be truncated. The statement has been terminated."

And now, if I close and re-open the app, the same error occurs every time. So things are now worse. From looking at other SO posts, I see that this error is pointing to a problem where the data being entered into the database exceeds the field size. But I get the error on the first record I try to add and it definitely does NOT exceed any field size in that table.

I suspect that I should not be passing null to SetInitializer(). I noted the comment, "... we run a DatabaseInitializer to do things like create the database if it's not there or add seed data." My code is already trying to handle the database creation. Should my app not be doing that? How would SetInitializer() know the location to put the database or the MAXSIZE? And I don't think I need it to "add seed data", either.

I also noted this comment, "We'll work on changing this behavior before we ship the fial (sic) version since it is the default." That makes it sound like a bug. The comment was written ~9 years ago, so I would have hoped that it was already fixed.

In the meantime, does anyone have any tips for how to get this working right?

--Update--

I was able to get this working with a small addition to the creation code shown above:

myCommand.ExecuteNonQuery();
// close & try again to open the database
connection.Close();
connection.ConnectionString = @"Data Source=(localdb)\mssqllocaldb;Integrated Security=True;Initial Catalog=MyAppDB";
connection.Open();

Seems like it should be unnecessary, but it works...

Then I tried something else. I replaced all the code in SetupDatabase() with one line, as I had seen in several example tutorials:

context = new MyAppModels();

This allows/relies on EF to open the database, creating it, if necessary (in some undefined/default location). This worked when the database already existed, but when the database needed to be created, it failed:

System.Data.SqlClient.SqlException HResult=0x80131904
Message=CREATE FILE encountered operating system error 5(Access is denied.) while attempting to open or create the physical file 'C:\Users\MyAppDB.mdf'. CREATE DATABASE failed. Some file names listed could not be created. Check related errors. Source=.Net SqlClient Data Provider

Note the lack of a backslash between and MyAppDB.mdf. I don't know why this happens or how to fix it. I don't see anything obvious in App.config that would control this...

<connectionStrings>
  <add name="MyAppConnection" connectionString="data source=(localdb)\mssqllocaldb;initial catalog=MyAppDB;integrated security=True;MultipleActiveResultSets=True;App=EntityFramework" providerName="System.Data.SqlClient" />
</connectionStrings>

Can someone help me work this out, so I can use "pure" EF, or do I need to go back to my hack that works...?

Todd Hoatson
  • 123
  • 2
  • 18

2 Answers2

0

Well

is the code, based on things I've read on SO and also in "Pro C# 7: With .NET and .NET Core"

it seems that you have tried mixing both manual DB creation and code first migrations.

And it ended badly.

Can we solve this problem?

Yes...with some additional details/code and it may even require reworking a lot of things from the scratch, so I am not sure it would be worth the time, as you seem to be studying technologies, rather than trying to solve some technological mishmash in some business critical internal program suite.

So, I recommend you to decide what you are trying to learn and do it step by step using standard tutorials:

  1. You want to learn how to work with EF context and Code-First migrations? - https://learn.microsoft.com/en-us/ef/ef6/modeling/code-first/migrations/
  2. Want to learn how to do it with already existing DB? - https://learn.microsoft.com/en-us/ef/ef6/modeling/code-first/workflows/existing-database
  3. Want to learn how to work with basic ADO.NET classes (DbConnection, DbDataReader) - https://learn.microsoft.com/en-us/dotnet/framework/data/adonet/ado-net-code-examples
  4. Want to do something in between? - just be sure that you have used each thing separately before mixing them and that you really want to try it (academic interest/desire to experiment is all good, but applying non-standard approaches to real problems is usually a bad idea).

P.S.: You seem to be using EF6 (as you used DatabaseInitializer), but I'd actually advise you to start learning EF Core as it is the latest iteration of Entity Framework.

Eugene Podskal
  • 10,270
  • 5
  • 31
  • 53
  • Whelp, I knew from experience that SO can be brutal to learners, but I didn't see this coming... – Todd Hoatson Nov 30 '19 at 21:56
  • "it seems that you have tried mixing both manual DB creation and code first migrations." Let me explain - I couldn't find information about how to create a database in EF, so I felt like I had to mix methods. They provide a way to create a context based on a connection, so I thought it should work. – Todd Hoatson Nov 30 '19 at 22:00
  • "... it ended badly. Can we solve this problem? Yes...with some additional details/code and it may even require reworking a lot of things from the scratch, so I am not sure it would be worth the time" I think you're magnifying the problem. It's ~30 lines of code. Reworking/rewriting from scratch is trivial. Also, the rest of my code is all EF, different from what you see here, so no rework should be required there. I have successfully imported > 11,500 records into 7 tables already. It's just the initial creation part that's wonky. – Todd Hoatson Nov 30 '19 at 22:05
  • I appreciate you're taking the time to offer some links. I can see how migrations may become useful once I'm near release, but during heavy development it looks like total overkill. It's definitely beyond the question I was asking here. So I could use DbDataReader, but I thought it would be nice to do things in a more OO kind of way. – Todd Hoatson Nov 30 '19 at 22:18
  • I generally find Microsoft documentation to be pretty bad. Def not written for learners. It's also out-of-date - some pages are 3 years old, but EF has been changing, as you pointed me to EF Core. A video I saw there about using EF with a new database was for Visual Studio 2012!!! Don't really know how much of it still applies. Like SO, MS information addresses the how, without really explaining the big picture - the why & the when. Not learner-friendly. I'll search on YouTube (again). – Todd Hoatson Nov 30 '19 at 22:19
  • What I didn't see in the MS documentation - which I think would be really important to most developers - is how to find out / control where the database is created. This is a show-stopper. The user needs to have a way to back the thing up... – Todd Hoatson Nov 30 '19 at 22:21
  • "You seem to be using EF6 (as you used DatabaseInitializer)" Actually, neither the code I posted nor the rest of my project contains any occurrence of the string DatabaseInitializer. It was simply referenced in a comment that I found online... I took it to be something internal to EF. – Todd Hoatson Nov 30 '19 at 22:24
  • If you still want to have the current situation solved, then you should add [mcve] - otherwise it is not always that easy to guess what exactly is going wrong from incomplete information. – Eugene Podskal Dec 01 '19 at 10:08
  • `how to find out / control where the database is created` - perhaps you should just ask it as a separate standalone question. Most probably with pure EF it could be done just by providing a custom connection string, but that needs to be checked. – Eugene Podskal Dec 01 '19 at 10:23
0

The code you've posted is how you would "manually" create a database using "dynamic" SQL. You don't need to use that approach at all when using Entity Framework.

One of the easiest ways to start with Entity Framework is to use Model-First:

  • Add a new "ADO.NET Entity Data Model" to your project.
  • Choose "Empty EF Designer Model".
  • Edit the model to design your database.
  • Upon saving (or in VS2019 you have to "Run Custom Tool" on the .tt files) you will have all of your entity classes created.
  • In the model choose "Generate Database from Model" to generate an SQL file containing all of the SQL statements needed to create the new database with that model. This step also creates all of the necessary bindings between the database and the C# code. (All of which exists behind the scenes.)

Entities that you add to the model become tables in the database and classes in the C# code.

Attributes that you add become fields in the database and properties in the C# code.

Associations that you add become foreign key relationships in the database and collections ("navigation properties") in the C# code.

You can then start building queries in C# code using LINQ and IQueryable.

You can make changes to the model, and go through the "Generate Database from Model" process again to get a new SQL file that creates the database, but note that it will first nuke any existing database when you execute the script. This can be ok when testing, but it can also help to use a version control system so that you can see the changes to the SQL and then make manual changes to the database structure to make sure the database matches the model without having to delete it first.

Dave Cousineau
  • 12,154
  • 8
  • 64
  • 80