Blazor dependency injection and EF/DbContext with private NuGet packages - entity-framework

I am trying to set up a Blazor project to be reusable through numerous other projects, and I am having issues understanding how Blazor's DI system works with NuGet packages.
In short, I built a simplistic Blazor app as a ticketing system for bug reports/requests. It collects simple information through input forms (built as components) and uploads tickets to a SQL Server database via Entity Framework and a connection string (stored in AppSettings.JSON). The DbContext is injected into the ticketing app's DI system via AddDbContextFactory. It all works perfectly within the solution.
My goal is: I want to package this ticketing system and reuse it throughout my other apps with minimal setup. In the other apps, I want to reuse the "AddTicket" components, which will simply accept some simple report data and update the ticketing databse.
As a test, I packaged the ticketing app as a NuGet package locally on my machine. I imported it into a separate host app. However, I had to recreate the connection string and the Dependency Injection (with the Context Factory and everything) in the host app to make the package work.
My problem is that I don't understand how Blazor's DI system works when I am building my own NuGet packages and I don't understand the flow of the dependency injections that are in Startup.cs versus what's in the NuGet package.
I just was a simple, reusable form and submittal component, with all of the EF and database logic built into it in the simplest way that I can drop into a dozen external projects.
What is the best way to build a NuGet package so that I don't need to do additional dependency injections in this situation? If I must do an additional entry into the host app's DI system, how do I make it as simple as possible?

When I create a library that has Injection. In my library I create a static class and create and extension method for IServiceCollection
public static class ServiceCollectionExtensions
{
public static void AddBlazorSyncServer(this IServiceCollection services)
{
services.AddScoped<ILogger, Logger<LoggingBroker>>();
services.AddScoped<ILoggingBroker, LoggingBroker>();
services.AddScoped<IDateTimeBroker, DateTimeBroker>();
services.AddTransient<ISyncDatabaseBroker, SyncDatabaseBroker>();
...
}
}
NOTE: In my example SyncDatabaseBroker is a DbContext :
public partial class SyncDatabaseBroker : DbContext, ISyncDatabaseBroker
This makes it a lot easier for the user to setup in their code base.
Databases have to be handled a little differently. When I want to manage migrations or share a connection strings I use this approach:
Program.cs (Library consuming server)
...
var migrationsAssembly = typeof(Program).Assembly.FullName;
builder.Services.AddDbContext<ApplicationDbContext>(
options => options.UseSqlServer(
connectionString,
dbOpts => dbOpts.MigrationsAssembly(migrationsAssembly)));
builder.Services.AddDbContext<SyncDatabaseBroker>(
options => options.UseSqlServer(
connectionString,
dbOpts => dbOpts.MigrationsAssembly(migrationsAssembly)));
...
builder.Services.AddBlazorSyncServer();
Console commands I can then use for the migrations:
Add-Migration InitialApplicationSchema -Context ApplicationDbContext -OutputDir Data/Migrations/Application
Add-Migration InitialSyncSchema -Context SyncDatabaseBroker -OutputDir Data/Migrations/Sync
Update-Database -Context ApplicationDbContext
Update-Database -Context SyncDatabaseBroker

Related

Entity Framework Core with split migrations

I am in the process of migrating from EF6 and .Net Framework to EF Core and .Net.
Previously I had a solution for the data model (Data project below) that built and deployed to a nuget package
Solution
Data
SchemaX
...
SchemaY
...
...
Data.Migrations.SqlServer
Migrations
SchemaX
20220701110223_Migration1.cs
...
The data model can then be consumed by any number of separate jobs/apps/etc using the package. This worked fine using EF6. However, if I start again for EF Core I'm seeing the odd issue.
Using the package manager console with default project Data, I can type
add-migration -StartupProject "Data.Migrations.SqlServer" -Context "Data.SchemaXContext" -OutputDir "Migrations\\SchemaX" -Name:CoreInitial
and it will scaffold a migration. The migration ends up in the Data project due to this default. If I use the Data.Migrations.SqlServer as the target/default it cannot find the context - I assume there is something that I could put in this project (other than a reference) to point it in the right direction?
I'm basically trying to migrate frameworks and use the suggestion here to set a start point for core.
NB The Data.Migrations... project is used as the startup as the Data project is .Net Standard 2.0 targetted to allow for use in framework and .Net projects whereas the migrations project has been set to target Framework 4.8.
Edit
Removed the following as it has been fixed by use of the modelBuilder.Ignore<> directive to handle where contexts crossover.
However, what it scaffolds is not limited to the context provided - it basically scaffolds the entire database, all schemas/context items - and using this mechanism as it stands I cannot get it to reference a database for comparison like I could EF6 with it's connection string parameter. I'm not sure whether the scaffolding of the entire model is a configuration issue or the lack of start point database.

Error when adding migration to EntityFramework project

Working on a .NETCore MVC back end that's using EntityFramework.
The Solution as a Service project, EntityFramework project, and an API (MVC) project. The API project is the envy point/startup.
It's an existing project, and there was an initial migration file which I used to setup the database from the command line using: dotnet ef database update.
Now, I've added a new model to the EntityFramework project, and added the appropriate public DbSet to the Context class. Now I'm trying to generate the update migration file for it so I can update the database.
Using dotnet ef migrations add migration_002 gave an error of:
Unable to create an object of type 'StartNetContext'. Add an
implementation of 'IDesignTimeDbContextFactory' to
the project, or see https://go.microsoft.com/fwlink/?linkid=851728 for
additional patterns supported at design time.
Searching for that turned up posts and blogs related to upgrades from EF 1 -> 2 ... not appropriate in this case. Or pointing to issues when creating the initial migration when there's also seeding happening... again, also not appropriate in this case. When I add the -v option, I get a bit more info:
Microsoft.EntityFrameworkCore.Design.OperationException: Unable to
create an object of type 'StartNetContext'. Add an implementation of
'IDesignTimeDbContextFactory' to the project, or see
https://go.microsoft.com/fwlink/?linkid=851728 for additional patterns
supported at design time. ---> System.MissingMethodException: No
parameterless constructor defined for this object.
Ugh. What? Why do I need that? Searching that points to re-arranging how the startup is configured... specifically making sure that the. AddDbContext is being called - which it is...
I feel like I'm going in circles... it should work... someone created the initial migration, so it had to have worked, right?
NOTE: I'm on a Mac, so there's no using the Package Manager console, so selecting target projects, etc isn't a solution either. This all needs to be done right from the CLI.
Add the following class in your main project.
public class DesignTimeDbContextFactory : IDesignTimeDbContextFactory<YourDbContext>
{
public YourDbContext CreateDbContext(string[] args)
{
IConfigurationRoot configuration = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json")
.Build();
DbContextOptionsBuilder<YourDbContext> builder = new DbContextOptionsBuilder<YourDbContext>();
string connectionString = configuration.GetConnectionString("YourDbContextConnectionName");
builder.UseSqlServer(connectionString);
return new YourDbContext(builder.Options);
}
}
Hope your error will go away!
So it turns out in my case it was a simple flag on the CLI... Despite having the web api project marked as the startup project in the solution, I needed to specify it on the command line as well.
dotnet ef migrations add migration_002 -s ../MyWebApp.Api
I got that from this post.
Adding the -s flag worked, even without the code and adding the class (which I did try first, but ran into problems with the return statement).
It's the simple things in life.

Any way to detect if EntityFramework Core is run inside a tool?

Is there any way to detect if my application is being run through dotnet ef * or the PowerShell alternatives?
I'm creating a multi-tenant ASP.NET Core app with Entity Framework Core and I have logic to decide which connection string should be used, but should not be run if I'm creating migrations or updating a database through the command line. I cannot rely on checking for the existance of an HttpContext because I use my DbContext during configuration as well, nor can I use additional command line arguments or environment variables.
EDIT: Found a way to see if a connection string has already been configured, it's not ideal but I can use it to help with my particular use case:
optionsBuilder.Options.Extensions
.OfType<RelationalOptionsExtension>()
.FirstOrDefault()?.ConnectionString
If you create a type that implements IDbContextFactory<T> in your DbContext (or startup) project, the tools will call this instead of trying get your DbContext from services or instantiate it directly.

Create Tables on runtime in EF Core

I know I can use the following command to add a new migration and create the database :
dotnet ef migrations add MigrationName -c DbContextName
dotnet ef database update -c DbContextName
but I need to create my database/tables at runtime.
First I tried to do that by overriding OnModelCreating but I failed. It returned me an error pointing out that the tables have not been created
Here is my dbcontext class
public class AimeLoggerContext : DbContext
{
public AimeLoggerContext(DbContextOptions<AimeLoggerContext> options)
: base(options)
{
Database.Migrate();
}
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
base.OnConfiguring(optionsBuilder);
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity("Aime.Logger.Loggers.Database.Model.Log", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<DateTime>("Datetime");
b.HasKey("Id");
b.ToTable("Logs");
});
base.OnModelCreating(modelBuilder);
}
public DbSet<Log> Logs { get; set; }
}
If you need to create you model tables also if in your DB there are some other tables you can use
RelationalDatabaseCreator databaseCreator =
(RelationalDatabaseCreator) context.Database.GetService<IDatabaseCreator>();
databaseCreator.CreateTables();
Before I dive in
If you really want to do things properly, then you should probably use Migrate() rather than EnsureCreated() as the latter means that if you want to move towards Migrations then you will have problems.
The fact that you want the database and tables created when you run the app, that means your probably more in favour of migration scripts and a code first approach to your workflow rather than a database script with a database first type approach.
So I will assume that
You want to use a code first approach
You want to use EF Core Migrations
You want these migrations to happen automatically when you start your app.
You want to do this the proper way!
I have just managed to acheive this the first time (mine was with Postgres, but everything should be pretty close as I saw examples for MS Sql and they were very similar, with the only difference if any being useSqlServer or UseNpgsql or similarly trivial differences.
After struggling to follow various articles on StackOverflow which were just tiny snippets of the biggest picture I finally managed to get it working nicely, properly and maybe elegantly. I followed this article here
There's a few things that I did where I'm not completely sure which thing exactly was the thing that got it working, but believe this list to be minimal enough for a satisfactory answer.
Here's What I did
I made sure that I had the powershell Cli tools installed by following this article here by MicroSoft
I ran this in cmd.exe dotnet tool install --global dotnet-ef
Open the VS Package manager console, which you can access by typing Package Manager Console in the search box at the top in VS 2019 (
Type the following commands into it
Install-Package Microsoft.EntityFrameworkCore.Tools
Update-Package Microsoft.EntityFrameworkCore.Tools
I have my repository code in a different project to my Web App project so for me I got some errors. I had to do the following things
I tried running the EF Migrate command on my repository project as it wasn't working from the WebAPI project
dotnet ef migrations add InitialCreate --project .\DilaRepository\DilaRepository.csproj
The console complained that
Your startup project 'DilaRepository' doesn't reference Microsoft.EntityFrameworkCore.Design
So where I got these messages I added the nuget package, that got rid of these errors. I don't know if this was required or not, because in the end I got the EF Migrate command working on the WebApi project which contains the startup.cs and program.cs
Here's what I needed to do to get it to work though
in my startup.cs I had this (which is fairly boiler plate, you'll see this on all articles)
public void ConfigureServices(IServiceCollection services)
{
...
services.AddEntityFrameworkNpgsql().AddDbContext<DilaDbContext>(opt =>
opt.UseNpgsql(
Configuration.GetConnectionString("DefaultConnection"),
b => b.MigrationsAssembly("DilaAPI")
));
The MigrationAssembly wasn't shown in any of the articles I read and in here the one I've used is the name of the Assembly its executed in, so it seems a bit strange that it can't figure it out itself, so perhaps I did something wrong to require that. I believe one of the error messages I got from running add-migration initial told me I needed that and after this it worked.
** Its super important you add this connection string to your appsettings.json (and within there the correct one that gets read at run time because there can be multiple (such as a appsettings.Development.json) **
This will be required when you run your add-migration command from the command prompt. This created a migration folder in my WebAPI app with some things in it for the migration, however this was not executed on my database. I'm not sure whether it actually should or not, but I will explain how I did finally get it to execute that on my database.
Execute the following command to generate the migration stuff
add-migration initial
If it doesn't work and you get some errors here's somet things you can try
our target project 'DilaAPI' doesn't match your migrations assembly 'DilaRepository'. Either change your target project or change your migrations assembly.
Change your migrations assembly by using DbContextOptionsBuilder. E.g. options.UseSqlServer(connection, b => b.MigrationsAssembly("DilaAPI"))
That's what originally instructed me to add the assembly in the startup.cs This is actually a really good error message as it tells you exactly what you need to do to fix it and it works
error MSB4057: The target "GetEFProjectMetadata" does not exist in the project.
This happened when I either
Executed this in the wrong directory
Exectued this on the solution file
In the end I needed to run this on the csproj of the WebApi project to work, you might have to cd to the directory from the package mangager console or you can specify the path to it with the --project option e.g. --project .\DilaApi\DilaApi.csproj in my case
You may also get some errors if you haven't installed the EF Tools properly from the steps above
Here's a picture of the stuff it generated
One Last thing
in your startup.cs add this to your configure method
public void Configure(IApplicationBuilder app, IWebHostEnvironment env, DilaDbContext context)
...
context.Database.Migrate();
If you've gotten everything else right when this executes on startup, this will actually execute the migration that was pre-generated by the add-migration initial command.
Now start your app and it should look something like this
Mine complained about something I did wrong, however I followed articles according to Microsoft, EF Core and Postgres, So I'm not sure why, But it worked. If anyone knows why that is and how I can rid of that warning I would be grateful. Perhaps we should tell them to update their documentation so their examples don't lead to this warning also.
What did this do to my Database
Created the Database
Created the Tables
MAGIC!!! 🧝‍♀️
This is quite remarkable, because it uses the infamous Convention over Configuration to know that my table should be called Word and my database Dila by the name of my Context and the name of the Entities.
Sorry for the long answer but it took me a few hours to get this working so thought I would share my war stories in the hope that everyone else gets it done a lot quicker than I did
Instead of DbContext.Database.Migrate() I should use DbContext.Database.EnsureCreated() method.

Code First Migrations for a custom NuGet package can't be executed from main application

I have created a module to be included in a main MVC3 web application. The module is packaged into a NuGet package and it can be installed and uninstalled via NuGet. Both, the main site and the module use Code First, EF >= 4.3
To create the package I have another MVC3 site and all the functionality is inside an area, so to create the package I just pack the libraries, the views and all the needed files. Database Migrations work fine in the project and the package is created nicely.
Now I install the package in the main site via NuGet. This site is in another solution, and the solution has two projects:
MyProject.Web.UI: this is an Mvc3 project
MyProject.EntityFramework: this is a class library with all the models, dbContext for MyProject...
The package is installed correctly and the Area, the Area views and libraries are correctly installed.
The problem now is how I update the database? I've tried first to run "Update-Database" but I get the message:
"No migrations configuration type was found in the assembly
'MyProject.Web.UI'. (In Visual Studio you can use the
Enable-Migrations command from Package Manager Console to add a
migrations configuration)."
I've tried then to enable the migrations with "Enable-Migrations" but I got this other message:
"No context type was found in the assembly 'MyProject.Web.UI'."
I tried also just to run the site and see if the changes are automatically applied but I get the exception page with the typical message:
"The model backing the 'NugetPackageDbContext' context has changed
since the database was created. Consider using Code First Migrations
to update the database"
I don't know what to do to update the database with the required changes in migrations that come in the NuGet package. Any one could put some light here in this matter? I'm quite new to Migrations, maybe there are some configs to update the database if there is a change instead of running the commands in the console, I'm a bit lost.
Thanks in advance :)
Good news! It seems that I got it. I was looking for a way to make the NuGet package to update the database to the latest version.
Well, this package comes with an Admin controller, so I added a new action called Update:
public ActionResult Update()
{
System.Data.Entity.Database.SetInitializer(new System.Data.Entity.MigrateDatabaseToLatestVersion<MyPackageDbContext, MyPackage.Migrations.Configuration>());
return View();
}
In my Configuration class for the migrations I have:
public Configuration()
{
AutomaticMigrationsEnabled = true;
}
I have to say that in my way to do all of this I've found a few strange behaviors. One thing that surprises me is this, I don't know if this is normal, read the secuence:
Package installed with new migrations but database not up to date. So if I access the EF this affected by this I get the exception about this. Ok up to this.
I go to my action /MyPackage/Admin/Update and run it. Apparently it runs. I go to the database and I don't see changes. Even the migrations table does not have a new row.
I access again the EF part that displayed the exception before (point number 1) and then everything goes through, database is updated and the migrations table shows the new line.
One thing that you have to notice is that the Configuration class is internal, but because this is a module I needed to be accessible from another assembly. I tried to make it public but I got some strange warnings/errors that I don't know if they are related. So in the end I kept it internal but used
[assembly: System.Runtime.CompilerServices.InternalsVisibleTo("OtherAssembly")]
I've found a few of strange behaviors in Visual Studio with all this stuff of the NuGet packages, CF, migrations ... I don't know if these things are bugs, but all this thing took me two full working days.
I hope it is useful for any other that wants to create a CF NuGet package that is updateable.
In package manager console you will need to the Default project to MyProject.EntityFramework
You may also need to make sure MyProject.Web.UI is set as the start up project (in case there are multiple), then you can pass in the connection string into update command:
Update-Database -ConnectionStringName MyConnStringName
This should update the database correctly, unless there is data that will be lost.
If your DbContext is in MyProject.EntityFramework then the Default Project in the Package Manager Console needs to be set to MyProject.EntityFramework before you can use Update-Database.
I realize that this question is very old, but since I don't see this answer I'll throw it in anyway.
To perform migrations and such on projects or external references you can still use the same three command:
Enable-Migrations
Add-Migration
Update-Database
but you wil need to supply some additional parameters. For the Enable-Migrations command you will need to add the -ContextTypeName and optionally the -ContextAssemblyName commands like so:
Enable-Migrations -ContextTypeName MyProject.EntityFramework.NugetPackageDbContext -ContextAssemblyName MyProject
This will give you a migration configuration class in your current project. The other two commands will require you to specify this configuration class:
Update-Database -ConfigurationTypeName MyProject.Web.UI.Migrations.Configuration
Hope that helps