How do you define DBContext properly in a Class Library Project?

asp.net-web-api c# entity-framework entity-framework-core

Question

I'm using entity framework 7 and created two projects. One project as a ASP.NET 5 Web API project and the other is a Class Library Project (Package) where I wanted to store all my Data Access Layer logic in. That way I can use this package for another reporting project down the road and other additional services I may make.

Basically I have a simple post in my controller of the web api project that calls a function in my data library project. When the function initiates the database it says database not defined even tho it's defined in both projects.


ERROR

{"No database providers are configured. Configure a database provider by overriding OnConfiguring in your DbContext class or in the AddDbContext method when setting up services."}

CODE

Data Library Project: InsertPerson

public int InsertPerson(tbl_Person person)
{
    using (var db = new AppContext())
    {
        try
        {
            db.tbl_Person.Add(person);
            db.SaveChanges();
            return person.PersonID;
        }

        catch
        {
            return 0;
        }
    }
}

CONFIGURATION FILES

Web API Project: Startup.cs

public void ConfigureServices(IServiceCollection services)
{
    services.AddEntityFramework()
        .AddSqlServer()
        .AddDbContext<AppContext>(options =>
            options.UseSqlServer(Configuration["Data:DefaultConnection:ConnectionString"]));

    services.AddIdentity<ApplicationUser, IdentityRole>()
        .AddEntityFrameworkStores<AppContext>();

    services.AddMvc();
}

Web API Project: appsettings.json

{
  "Data": {
    "DefaultConnection": {
      "ConnectionString": "Server=localhost;Database=testDB;Trusted_Connection=True;MultipleActiveResultSets=true"
    }
  },
  "Logging": {
    "IncludeScopes": false,
    "LogLevel": {
      "Default": "Verbose",
      "System": "Information",
      "Microsoft": "Information"
    }
  }
}

Web API Project: project.json

{
  "userSecretsId": "blah,
  "version": "1.0.0-rc1-final",
  "compilationOptions": {
    "emitEntryPoint": true
  },

  "dependencies": {
    "DataLibrary": "",
    "Microsoft.AspNet.Authentication.OAuthBearer": "1.0.0-beta7",
    "Microsoft.AspNet.Http.Abstractions": "1.0.0-rc1-final",
    "Microsoft.AspNet.Identity.EntityFramework": "3.0.0-rc1-final",
    "Microsoft.AspNet.IISPlatformHandler": "1.0.0-rc1-final",
    "Microsoft.AspNet.Mvc": "6.0.0-rc1-final",
    "Microsoft.AspNet.Server.Kestrel": "1.0.0-rc1-final",
    "Microsoft.AspNet.StaticFiles": "1.0.0-rc1-final",
    "Microsoft.Extensions.Configuration.FileProviderExtensions": "1.0.0-rc1-final",
    "Microsoft.Extensions.Configuration.Json": "1.0.0-rc1-final",
    "Microsoft.Extensions.Configuration.UserSecrets": "1.0.0-rc1-final",
    "Microsoft.Extensions.Logging": "1.0.0-rc1-final",
    "Microsoft.Extensions.Logging.Console": "1.0.0-rc1-final",
    "Microsoft.Extensions.Logging.Debug": "1.0.0-rc1-final",
    "Remotion.Linq": "2.0.1"
  },

  "commands": {
    "web": "Microsoft.AspNet.Server.Kestrel"
  },

  "frameworks": {
    "dnx451": { },
    "dnxcore50": { }
  },

  "exclude": [
    "wwwroot",
    "node_modules"
  ],
  "publishExclude": [
    "**.user",
    "**.vspscc"
  ]
}

Data Library Project: Startup.cs

public void ConfigureServices(IServiceCollection services)
{
    services.AddEntityFramework()
        .AddSqlServer()
        .AddDbContext<AppContext>(options =>
            options.UseSqlServer(Configuration["Data:DefaultConnection:ConnectionString"]));

    services.AddIdentity<ApplicationUser, IdentityRole>()
        .AddEntityFrameworkStores<AppContext>();

    services.AddMvc();
}

Data Library Project: appsettings.json

{
  "Data": {
    "DefaultConnection": {
      "ConnectionString": "Server=localhost;Database=testDB;Trusted_Connection=True;MultipleActiveResultSets=true"
    }
  },
  "Logging": {
    "IncludeScopes": false,
    "LogLevel": {
      "Default": "Verbose",
      "System": "Information",
      "Microsoft": "Information"
    }
  }
}

Data Library Project: AppContext.cs

namespace DataLibrary
{
    public class AppContext : IdentityDbContext<ApplicationUser>
    {
        public DbSet<tbl_Person> tbl_Person { get; set; }

        public static AppContext Create()
        {
            return new AppContext();
        }

        protected override void OnModelCreating(ModelBuilder builder)
        {
            new tbl_PersonMap(builder.Entity<tbl_Person>());
            base.OnModelCreating(builder);
        }
    }
}

Accepted Answer

It looks like your issue in the sample code is that you are newing up the AppContext instance instead of resolving it from the ServiceProvider. When you initialize a DbContext this way, a new ServiceProvider is implicitly created for the context and none of the configuration in Startup.cs is respected by that context. This is a pattern you would want to use if you were configuring your context in OnConfiguring and not interested in dependency injection in the rest of your application.

In this kind of situation, I would expect you to get the AppContext through constructor injection. For this kind of solution, you would also need to register your Data Access Layer classes in the ServiceCollection. The result should look a bit more like this:

Data Library Project: AppContext.cs

namespace DataLibrary
{
    public class AppContext : IdentityDbContext<ApplicationUser>
    {
        public DbSet<tbl_Person> tbl_Person { get; set; }

        protected override void OnModelCreating(ModelBuilder builder)
        {
            new tbl_PersonMap(builder.Entity<tbl_Person>());
            base.OnModelCreating(builder);
        }
    }
}

Data Library Project: PersonHelper.cs

namespace DataLibrary
{
    class PersonHelper
    {
        private readonly AppContext db;

        public PersonHelper(AppContext context)
        {
            db = context;
        }

        public int InsertPerson(tbl_Person person)
        {
            try
            {
                db.tbl_Person.Add(person);
                db.SaveChanges();
                return person.PersonID;
            }
            catch
            {
                return 0;
            }
        }
    }
}

Web API Project: Startup.cs

public void ConfigureServices(IServiceCollection services)
{
    services.AddEntityFramework()
        .AddSqlServer()
        .AddDbContext<AppContext>(options =>
            options.UseSqlServer(Configuration["Data:DefaultConnection:ConnectionString"]));

    services.AddIdentity<ApplicationUser, IdentityRole>()
        .AddEntityFrameworkStores<AppContext>();

    services.AddScoped<PersonHelper>();

    services.AddMvc();
}

Web API Project: MyController.cs

[Route("api/[controller]")]
public class MyController : Controller
{
    private readonly personHelper helper;

    public MyController(PersonHelper helper)
    {
        this.helper = helper;
    }

    // POST api/my
    [HttpPost]
    public void Post([FromBody]string value)
    {
        var person = new tbl_Person
        {
          // ...
        }

        return helper.InsertPerson(person);
    }
}

you may even consider adding an extension method on IServiceCollection for your Data Access Layer classes to help reduce duplication in your configuration, especially as you add more common services.


Popular Answer

I found a way to do this but I'm still not convinced this is the best way. In the class file that holds my insert function I added this so that when it's called from the web api project, the appcontext from the web api is passed to the data library project. With that said I'm not sure if there is a more elegant approach

public class fnCommon
{
    private readonly AppContext db;

    public fnCommon(AppContext context)
    {
        this.db = context;
    }

    public int InsertPerson(tbl_Person person)
    {

        try
        {
            db.tbl_Person.Add(person);
            db.SaveChanges();
            return person.PersonID;
        }

        catch
        {
            return 0;
        }
    }
}


Related

Licensed under: CC-BY-SA with attribution
Not affiliated with Stack Overflow
Is this KB legal? Yes, learn why
Licensed under: CC-BY-SA with attribution
Not affiliated with Stack Overflow
Is this KB legal? Yes, learn why