How to configure Entity Framework in a separate project? - c#

I have an ASP.NET MVC application EducationalCenter with the following structure of projects:
DbContext file is EducationalCenterContext.cs in the Data project and looks as follows:
public sealed class EducationalCenterContext: DbContext
{
public DbSet<Student> Students { get; set; }
public EducationalCenterContext( DbContextOptions<EducationalCenterContext> options)
: base(options)
{
Database.EnsureCreated();
}
}
And in Startup.cs file, the dbContext configured as follows in ConfigureService():
services.AddDbContext<EducationalCenterContext>
(options => options.UseSqlServer("Server=localhost;Database=EducationalCenterDb;Trusted_Connection=True;MultipleActiveResultSets=true"));
This is my working version to which I came by fixing errors when try to add-migration. However it seems awful to me that my web app has project reference to the Data project.
What was my first idea:
In appsettings.json I created this section :
"ConnectionStrings": {
"DefaultConnection": "Server=localhost;Database=EducationalCenterDb;Trusted_Connection=True;MultipleActiveResultSets=true"
}
Then I created AppSettings class in the Common project:
public class AppSettings
{
public string ConnectionString { get; set; }
}
Then I try to pass ConnectionString in DAL via DI:
services.Configure<AppSettings>(Configuration.GetSection("ConnectionStrings"));
And created EducationalDbContext.cs:
public sealed class EducationalCenterContext: DbContext
{
private readonly string _connectionString;
public DbSet<Student> Students { get; set; }
public EducationalCenterContext( IOptions<AppSettings>, DbContextOptions<EducationalCenterContext> options)
: base(options)
{
_connectionString = app.Value.ConnectionString;
Database.EnsureCreated();
}
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder.UseSqlServer(_connectionString);
}
}
But when I try to add-migration via PM Console, I ran into this error:
Could not load assembly 'EducationalCenter.Data'. Ensure it is referenced by the startup project 'EducationalCenter'
Then I added project reference and ran into the next error:
Unable to create an object of type 'EducationalCenterContext'. For the different patterns supported at design time, see https://go.microsoft.com/fwlink/?linkid=851728
Then I added services.AddDbContext<> in Startup.cs and came to the working version which I mentioned above.
So...
Is it normal that my web app has reference to the data access project?
Is it possible to configure EF in the Data project and ensure normal separation between DAL, BLL and web app?

Putting the context and configuration in a separate project is fine.
You got the first error because "Education Center" was set as start up project but did not have reference to data project.
The second error is because the migration builder needs some connection information in the data project to resolve the connection (to compare EF state and database state) to determine what changes are needed.
First add reference in your data project to:
Microsoft.EntityFrameworkCore.Design
Then add a context factory in your data project that migration console command will discover:
internal class MyContextFactory : IDesignTimeDbContextFactory<MyContext>
{
public MyContext CreateDbContext(string[] args)
{
var dbContextBuilder = new DbContextOptionsBuilder<MyContext>();
var connString = "myconnection string";
dbContextBuilder.UseSqlServer(connString);
return new MyContext(dbContextBuilder.Options);
}
}

Related

Can't create Entity Framework code-first migrations

I've been developing a .NET Core 6 console application (not ASP.NET) the last weeks and now I've tried to implement Entity Framework 6 migrations to it.
However, even though I reused some code from a working database model that used migrations, now I can't manage to make it work and I've also been struggling due to the lack of output from dotnet-ef.
For reasons I can't remember, the database project I reused code from used Design-Time DbContext creation. I don't know if that's my optimal way to make migrations but at least it managed to work on the previous project. I implemented the required IDesignTimeDbContextFactory<DbContext> interface the same way it was done previously:
public class MySqlContextFactory : IDesignTimeDbContextFactory<MySqlContext>
{
public MySqlContext CreateDbContext(string[] args)
{
DbContextOptionsBuilder optionsBuilder = new();
ServerVersion mariaDbVersion = new MariaDbServerVersion(new Version(10, 6, 5));
optionsBuilder.UseMySql(DatabaseCredentials.GetConnectionString(), mariaDbVersion);
return new MySqlContext();
}
}
public class MySqlContext : DbContext
{
public DbSet<Endpoint> EndpointsSet { get; set; }
private readonly string _connectionString;
public MySqlContext() : base()
=> _connectionString = DatabaseCredentials.GetConnectionString();
public MySqlContext(string connectionString) : base()
=> _connectionString = connectionString;
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
=> Configurator.Configure(optionsBuilder, _connectionString);
protected override void OnModelCreating(ModelBuilder modelBuilder)
=> Configurator.Create(modelBuilder);
}
public static void Configure(DbContextOptionsBuilder optionsBuilder, string connectionString)
{
ServerVersion mariaDbVersion = new MariaDbServerVersion(new Version(10, 6, 5));
optionsBuilder.UseMySql(connectionString, mariaDbVersion);
}
public static void Create(ModelBuilder modelBuilder)
{
IEnumerable<Type> types = ReflectionUtils.GetImplementedTypes(typeof(IEntityTypeConfiguration<>));
if (types.Any())
{
foreach (Type entityConfigurationType in types)
{
modelBuilder.ApplyConfigurationsFromAssembly(entityConfigurationType.Assembly);
}
}
else
{
Environment.Exit((int) EExitCodes.EF_MODEL_NOT_FOUND);
}
}
However, when I tried to create the first migration, I've been prompted with this absolutely non-descriptive output from the dotnet-ef tool:
PS> dotnet ef migrations add Init
Build started...
Build succeeded.
PS>
But no migrations were made nor anything changed in my project. So I decide to force dotnet ef to tell me more things by appending the --verbose flag on the PS command:
[...]
Build succeeded.
dotnet exec --depsfile F:\pablo\Documents\source\MyBot\bin\Debug\net6.0\MyBot.deps.json --additionalprobingpath C:\Users\pablo\.nuget\packages --runtimeconfig F:\pablo\Documents\source\MyBot\bin\Debug\net6.0\MyBot.runtimeconfig.json C:\Users\pablo\.dotnet\tools\.store\dotnet-ef\6.0.1\dotnet-ef\6.0.1\tools\netcoreapp3.1\any\tools\netcoreapp2.0\any\ef.dll migrations add Init -o Migrations\Init --assembly F:\pablo\Documents\source\MyBot\bin\Debug\net6.0\MyBot.dll --project F:\pablo\Documents\source\MyBot\MyBot.csproj --startup-assembly F:\pablo\Documents\source\MyBot\bin\Debug\net6.0\MyBot.dll --startup-project F:\pablo\Documents\source\MyBot\MyBot.csproj --project-dir F:\pablo\Documents\source\MyBot\ --root-namespace MyBot--language C# --framework net6.0 --nullable --working-dir F:\pablo\Documents\source\MyBot--verbose
Using assembly 'MyBot'.
Using startup assembly 'MyBot'.
Using application base 'F:\pablo\Documents\source\MyBot\bin\Debug\net6.0'.
Using working directory 'F:\pablo\Documents\source\MyBot'.
Using root namespace 'MyBot'.
Using project directory 'F:\pablo\Documents\source\MyBot\'.
Remaining arguments: .
Finding DbContext classes...
Finding IDesignTimeDbContextFactory implementations...
Found IDesignTimeDbContextFactory implementation 'MySqlContextFactory'.
Found DbContext 'MySqlContext'.
Finding application service provider in assembly 'MyBot'...
Finding Microsoft.Extensions.Hosting service provider...
No static method 'CreateHostBuilder(string[])' was found on class 'Program'.
No application service provider was found.
Finding DbContext classes in the project...
Using DbContext factory 'MySqlContextFactory'.
PS>
The first thing I thought I could search for was that CreateHostBuilder function the tool is searching but not retrieving. However, once again, all the documentation I could find was refer to ASP.NET applications, and programming patterns I'm not implementing in my bot application. My app does retrieve the services via Dependency Injection, custom made (maybe that's the reason of the line No application service provider was found. ?), but I didn't find a way to implement that CreateHostBuilder function without changing everything.
Just for adding the information, this is how I managed to create and configure the EF model with the non-migrations approach:
public static IServiceProvider GetServices(DiscordSocketClient client, CommandService commands)
{
ServiceCollection services = new();
services.AddSingleton(client);
services.AddSingleton(commands);
services.AddSingleton<HttpClient>();
services.AddDbContext<MySqlContext>(ServiceLifetime.Scoped);
return AddServices(services) // builds service provider;
}
private static async Task InitDatabaseModel(IServiceProvider provider)
{
MySqlContext? dbCtxt = provider.GetService<MySqlContext>();
if (dbCtxt == null)
{
Environment.Exit((int) EExitCodes.DB_SERVICE_UNAVAILABLE);
}
await dbContext.Database.EnsureDeletedAsync();
await dbContext.Database.EnsureCreatedAsync();
}
But unfortunately, my application is planned to interact with a database dynamically, so the Code-First configuring approach is not valid for me.
How can I solve this? Is an approach problem, or am I messing around with the custom non ASP.NET Dependency Injection provider? Thank you all
There is an issue with your IDesignTimeDbContextFactory. EF Core is trying to your this factory to create a MySqlContext.
public class MySqlContextFactory : IDesignTimeDbContextFactory<MySqlContext>
{
public MySqlContext CreateDbContext(string[] args)
{
// set up options
DbContextOptionsBuilder optionsBuilder = new();
ServerVersion mariaDbVersion = new MariaDbServerVersion(new Version(10, 6, 5));
optionsBuilder.UseMySql(DatabaseCredentials.GetConnectionString(), mariaDbVersion);
// *** this is the issue ***
// return default constructor W/O options (ie, UseMySql is never called)
return new MySqlContext();
}
}
You can add this constructor to your DbContext class:
public MySqlContext(DbContextOptions<MySqlContext> options)
: base(options)
{
}
and then return new MySqlContext(optionsBuilder.Options) from your factory.

How to specify an Entity Framework Connection String in API Project

Question: How do I specify the Entity Framework connection string within a .NET API?
I am accustomed to creating a DAL class and specifying the base connection string like I did here.
public class LocalContext : DbContext
{
public LocalContext() : base("LocalDBContext")
{
}
public DbSet<Weapons> Weapons { get; set;
}
Which in turn grabs the LocalDBContext connection string from the web.config or appsettings.json.
"ConnectionStrings": {
"LocalDBContext": "Server=.;Database=Weapons;Trusted_Connection=True;"
},
This is what I have done in the past in various web apps but not sure if I have to do something different with an API?
I would expect it to call and save into "Weapons" at "Server=." however it instead created a new Database called "LocalDBContext" at the connection of "(localdb)\mssqllocaldb". Any tips would be greatly appreciated!
In EF core you don't need to send a connection to the base class with the constructor, just follow this approach:
public partial class LocalContext : DbContext
{
public LocalContext ()
{
}
public LocalContext(DbContextOptions<LocalContext> options) :
base(options)
{
}
public virtual DbSet<Weapon> Weapons { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
if (!optionsBuilder.IsConfigured)
{
//warning You can move this code to protect potentially sensitive
information
//in connection string.
optionsBuilder.UseSqlServer("Data Source= .;Initial
Catalog=Weapons;Trusted_Connection=True;");
}
}
}
The given String "LocalDBContext" is interpreted as Connectionstring, see official Documentation on DbContext(String).
Do something like:
public class LocalContext : DbContext
{
public LocalContext (DbContextOptions<LocalContext> options)
: base(options)
{
}
....
I have some questions below:
Questions:
Did you add Entity data model in your API solution?
If yes, didn't you save connection string in config file while adding EDM?
While adding EDMX in solution, model window asks to connect the database. Once EDM connects with database, it asks to save connection string in configuration file. You can add tables, functions, SPs, views. This way EDM connects with actual database rather picking different database.

Could not perform EF Core for .NET Core Class Lib Project and .NET Core Web Api Project. Error: Value cannot be null. (Parameter 'connectionString')

I split out my solution into multiple projects / layers. The technology which I am using is C# in .NET Core and Entity Framework Core.
I have Infrastructure project which is a .NET Core class library project which I put the DataContext class. If I put my connection string in the DataContext class directly, it works. I can perform the EF migration when I set as startup project of my Web Api project .
public class DataContext : DbContext
{
const string connectionString = "Server=STEVENGAI\\SQLEXPRESS;Database=CinemaDbNETCore;Trusted_Connection=True;MultipleActiveResultSets=true";
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
AppConfiguration appconfig = new AppConfiguration();
optionsBuilder.UseSqlServer(appconfig.ConnectionString);
}
public DbSet<Movie> Movies { get; set; }
}
But, I need to put the connectionstring at appsettings.json instead. So, I installed these packages
Microsoft.Extensions.Configuration
Microsoft.Extensions.Configuration.Abstractions
Microsoft.Extensions.Configuration.Json
and have this class which read the connectionstring from appsettings.json.
namespace CinemaApp.NETCore.Infrastructure
{
public class AppConfiguration
{
public readonly string _connectionString = string.Empty;
public AppConfiguration()
{
var configurationBuilder = new ConfigurationBuilder();
var path = Path.Combine(Directory.GetCurrentDirectory(), "appsettings.json");
configurationBuilder.AddJsonFile(path, false);
var root = configurationBuilder.Build();
_connectionString = root.GetSection("ConnectionString").GetSection("CinemaDb").Value;
//var appSetting = root.GetSection("ApplicationSettings");
}
public string ConnectionString
{
get => _connectionString;
}
}
}
Then, I create another .NET Core console project to test if I could get the connection string which it did successfully. I can perform the EF migration when I Set as Startup Project of this Console Test project.
namespace ConsoleTest
{
class Program
{
static void Main(string[] args)
{
AppConfiguration appconfig = new AppConfiguration();
Console.WriteLine(appconfig.ConnectionString);
Console.WriteLine("Hello World!");
}
}
}
But, the problem is when I switch back Set as Startup Project to Web Api project, and perform EF migration, it gives me this error Value cannot be null. (Parameter 'connectionString')
I have copied the same connectringstring for both appsettings.json in Console Test project and Web Api project.
#viveknuna thanks for the pointer. I found out the issue already. In my web api project appsettings.json, I have "ConnectionStrings" but it actually detect connectionstring without "s", "ConnectionString"

Entity Framework Core migration - connection string

I'm having a problem to handle the DB connection string in conjunction with migrations.
I have 2 projects:
Domain
Application
The DbContext is in the Domain project, so this is the project I run migrations against.
The migrations concept enforces me to implement OnConfiguring in my DbContext and therein specify the database provider, eg:
protected override void OnConfiguring(DbContextOptionsBuilder builder)
{
builder.UseSqlServer("<connection string>");
}
My problem is that I don't want to use a hard coded connection string, for obvious reasons, and I cannot use ConfigurationManager to read it from the config file since the config file is in the application project.
All the examples I've seen involve either hard-coding the connection string or putting it in my ASP.NET Core application's settings files.
If you aren't using ASP.NET Core, or maybe, I don't know, don't want to have your local environment's database details committed to source control, you can try using a temporary environment variable.
First, implement IDesignTimeDbContextFactory like this (note that IDbContextFactory is now deprecated):
public class AppContextFactory: IDesignTimeDbContextFactory<AppContext>
{
public AppContextFactory()
{
// A parameter-less constructor is required by the EF Core CLI tools.
}
public AppContext CreateDbContext(string[] args)
{
var connectionString = Environment.GetEnvironmentVariable("EFCORETOOLSDB");
if (string.IsNullOrEmpty(connectionString))
throw new InvalidOperationException("The connection string was not set " +
"in the 'EFCORETOOLSDB' environment variable.");
var options = new DbContextOptionsBuilder<AppContext>()
.UseSqlServer(connectionString)
.Options;
return new AppContext(options);
}
}
Then, you can include the environment variable when you call Update-Database, or any of the other EF Core tools:
$env:EFCORETOOLSDB = "Data Source=(local);Initial Catalog=ApplicationDb;Integrated Security=True"; Update-Database
Here's how I do it, without a lot of extra code or craziness.
Project Structure:
AspNetCoreProject.Web
AspNetCoreProject.Data <-- DbContext here
My DbContext is set up with the constructor that allows you to inject the DbContextOptions
AspNetCoreProject.Data
public class MyContext : DbContext
{
public MyContext(DbContextOptions<MyContext> options) : base(options)
{
}
}
In your application or web application, you set up your ConfigureServices normally.
AspNetCoreProject.Web / Startup.cs / ConfigureServices()
services.AddDbContext<MyContext>(options =>
options.UseSqlServer(Configuration.GetConnectionString("connection"))
Now, what about migrations? Well, I "trick" the Visual Studio UI into working as expected.
First, make sure your application (AspNetCoreProject.Web project with Startup.cs) is the start up project.
Second, open up your Nuget Package Manager Console. At the top of the Nuget PM> Console, there's a dropdown for 'Set Default Project', point this to your AspNetCoreProject.Data or project with the DbContext class.
Run your migration commands normally. add-migration init then update-database
Assuming your DbContext class has a constructor that accepts a parameter of type DbContextOptions, the dotnet ef commands have native support for this scenario - requiring no code changes nor additional configuration. Just use the "--startup-project" and "--project" parameters when creating and running migrations.
For example, let's say you have a "Application" project with your configuration and a separate project called "Domain" where the DbContext is implemented.
Context:
public class MyContext : DbContext
{
public MyContext(DbContextOptions<MyContext> options) : base(options)
{
}
}
Startup:
services.AddDbContext<MyContext>(options =>
options.UseSqlServer(Configuration.GetConnectionString("connection"))
CLI command:
dotnet ef database update --startup-project Application --project Domain
We've had a same issue and there is a solution. :)
You have to implement IDbContextFactory<TContext>
When doing so you can read the connectionstrings from your appsettings.json. You can also use Add-Migration without errors, because overwriting OnConfigure() is obsolete then.
Sample implementation:
public class DomainContextFactory : IDbContextFactory<DomainContext>
{
public string BasePath { get; protected set; }
public DomainContext Create()
{
var environmentName = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT");
var basePath = AppContext.BaseDirectory;
return Create(basePath, environmentName);
}
public DomainContext Create(DbContextFactoryOptions options)
=> Create(options.ContentRootPath, options.EnvironmentName);
private DomainContext Create(string basePath, string environmentName)
{
BasePath = basePath;
var configuration = Configuration(basePath, environmentName);
var connectionString = ConnectionString(configuration.Build());
return Create(connectionString);
}
private DomainContext Create(string connectionString)
{
if (string.IsNullOrEmpty(connectionString))
{
throw new ArgumentException($"{nameof(connectionString)} is null or empty", nameof(connectionString));
}
var optionsBuilder = new DbContextOptionsBuilder<DomainContext>();
return Configure(connectionString, optionsBuilder);
}
protected virtual IConfigurationBuilder Configuration(string basePath, string environmentName)
{
var builder = new ConfigurationBuilder()
.SetBasePath(basePath)
.AddJsonFile("constr.json")
.AddJsonFile($"constr.{environmentName}.json", true)
.AddEnvironmentVariables();
return builder;
}
protected virtual string ConnectionString(IConfigurationRoot configuration)
{
string connectionString = configuration["ConnectionStrings:DefaultConnection"];
return connectionString;
}
protected virtual DomainContext Configure(string connectionString, DbContextOptionsBuilder<DomainContext> builder)
{
builder.UseSqlServer(connectionString, opt => opt.UseRowNumberForPaging());
DomainContext db = new DomainContext(builder.Options);
return db;
}
DomainContext IDbContextFactory<DomainContext>.Create(DbContextFactoryOptions options)
=> Create(options.ContentRootPath, options.EnvironmentName);
}
How we use it:
public override IServiceResult<IList<Datei>> LoadAllData()
{
using (var db = this.DomainContextFactory.Create())
{
var files = db.Datei
.ToListAsync<Datei>();
return new ServiceResult<IList<Datei>>(files.Result, files.Result.Count);
}
}
sample config
{
"ConnectionStrings": {
"DefaultConnection": "Put your connectionstring here"
}
}
I was using OnConfiguring below with configured in Windows environment variable MsSql.ConnectionString and command for initial ef migration creation started to work: dotnet ef migrations add InitialCreate
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
var connectionString = Environment.GetEnvironmentVariable("MsSql.ConnectionString");
if(string.IsNullOrEmpty(connectionString))
throw new ConfigurationErrorsException("Sql server connection string configuration required");
if (!optionsBuilder.IsConfigured)
{
optionsBuilder
.UseSqlServer(connectionString)
.UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking);
}
}
To configure environment variable:
use Win + R hotkeys kombination to open Run command window
Type systempropertiesadvanced and hit Enter
On Advanced tab click Environment Variables
Click New... button
In Variable name field type MsSql.ConnectionString
In Variable value field type your connection string value
Make sure console(and any program that starts console) is restarted after new variable addition and before running dotnet ef related commands
I have my DBContext in my console app and was using a ctor with few parameters (such as connection string etc), since EF Core Migrations was using the default parameter less ctor and hence the connection string wasn't being populated I had the migrations failing.
Just added code to get the connection string from ConfigurationBuilder within my default ctor to by pass this.
Was only playing around with console app and EF Core so this works for me for now.

Enabling Migrations causes namespace to stop working

I created a WCF / EF code first service. I created a folder called "Models" and there created my /POCOs. In there I created my context. Now in the Package Manager Console I run enable-migrations. This returns Success and adds the Migrations folder and /Configuration.cs. Now the problem shows itself.
The auto-generated code in Configuration.cs looks like this;
internal sealed class Configuration :
DbMigrationsConfiguration<MyService.**Models**.MyContext>
{
public Configuration()
{
AutomaticMigrationsEnabled = false;
}
}
protected override void Seed(MyService.**Models**.MyContext context)
Now under the word Models I get the red squiggle and the error message
The type name Models does not exist in the type MyService.MyService.
I have tried everything including renaming the Namespace and the folder. Anywhere the myContext file is placed will cause this error.
I've tried deleting all existence of the Entity Framework and starting over many times but still can't figure this one out! :/
namespace MyService.Models>
{
public class MyContext : DbContext
{
public DbSet<Chain> Chains { get; set; }
public DbSet<Shop> Shops { get; set; }
}
}

Categories

Resources