Mac, DotNetCore, Postgres
July 29, 2017
I'm having fun trying to get an application that was originally developed on Windows, against SQL Server, running on a Mac against Postgres. It's written in Microsoft's dotnet core, using C# and Entity Framework Core.
The solution has multiple projects inside of it:
- Salon - contains the web site
- SalonServices - contains the services the web site makes use of, and accesses the database
- Salon.Web.Tests.Unit - unit tests for the web application controllers
- SalonServices.Tests.Integration - integration tests for the database access repositories
- SalonServices.Tests.Unit - unit tests for the Salon Services functionality.
Entity Framework Postgres Packages
In order to get Postgres support, I installed the following three packages into my project, using nuget:
<PackageReference Include="Npgsql" Version="3.2.5" /> <PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="1.1.1" /> <PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL.Design" Version="1.1.1" />
Creating the database
I created the database on a separate Linux server, called baal
postgres=# create user salon with password 'Secret'; postgres=# create database salon with owner salon;
Database Connection
I configured the database connection in appsettings.json (in my Salon project) to have the following entry:
"ConnectionStrings": { // "Salon": "data source = .;initial catalog = Salon; integrated security = True; MultipleActiveResultSets=True;App=EntityFramework" "Salon": "User ID=salon;Password=Secret;Host=baal;Port=5432;Database=salon;Pooling=true;" },
Missing dotnet-ef
The internet told me that I needed to run dotnet ef database update
in order to populate my database. This needs to be run inside the project that has a DbContext, so in my case this is the SalonServices folder. This didn't work, failing with:
$ dotnet ef database update No executable found matching command "dotnet-ef"
In order to get this to work, I had to manually edit the SalonServices.csproj
file.
I added another ItemGroup
containing
<ItemGroup> <DotNetCliToolReference Include="Microsoft.EntityFrameworkCore.Tools.DotNet" Version="1.0.0" /> </ItemGroup>
and then ran
$ dotnet restore
Missing appsettings.json
When I next ran my dotnet
command to run my database migrations, I got the following error:
The configuration file 'appsettings.json' was not found and is not optional. The physical path is '/Users/drumcoder/web/Salon/SalonServices/bin/Debug/netcoreapp1.1/appsettings.json'.
I fixed this with a manual copy of the appsettings.json file out of the Salon project into the specified directory. There's probably a much better solution to this, and I'll get to it when I finally have this all working.
Could not load System.Diagnostics.DiagnosticSource
My next error was this:
$ dotnet ef database update Build succeeded. 0 Warning(s) 0 Error(s) Time Elapsed 00:00:01.62 System.IO.FileLoadException: Could not load file or assembly 'System.Diagnostics.DiagnosticSource, Version=4.0.1.1, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51'. The located assembly's manifest definition does not match the assembly reference. (Exception from HRESULT: 0x80131040) File name: 'System.Diagnostics.DiagnosticSource, Version=4.0.1.1, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51' at Microsoft.EntityFrameworkCore.Infrastructure.RelationalServiceCollectionExtensions.AddRelational(IServiceCollection services) at Microsoft.Extensions.DependencyInjection.SqlServerServiceCollectionExtensions.AddEntityFrameworkSqlServer(IServiceCollection services) at Microsoft.EntityFrameworkCore.Internal.ServiceProviderCache.<>c__DisplayClass4_1.<GetOrAdd>b__2(Int64 k) at System.Collections.Concurrent.ConcurrentDictionary`2.GetOrAdd(TKey key, Func`2 valueFactory) at Microsoft.EntityFrameworkCore.DbContext.InitializeServices() at Microsoft.EntityFrameworkCore.DbContext.get_InternalServiceProvider() at Microsoft.EntityFrameworkCore.Infrastructure.AccessorExtensions.GetService[TService](IInfrastructure`1 accessor) at Microsoft.EntityFrameworkCore.Design.Internal.DbContextOperations.CreateContext(Func`1 factory) at Microsoft.EntityFrameworkCore.Design.Internal.DbContextOperations.CreateContext(String contextType) at Microsoft.EntityFrameworkCore.Design.Internal.MigrationsOperations.UpdateDatabase(String targetMigration, String contextType) at Microsoft.EntityFrameworkCore.Design.OperationExecutor.UpdateDatabase.<>c__DisplayClass0_1.<.ctor>b__0() at Microsoft.EntityFrameworkCore.Design.OperationExecutor.OperationBase.Execute(Action action) Could not load file or assembly 'System.Diagnostics.DiagnosticSource, Version=4.0.1.1, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51'. The located assembly's manifest definition does not match the assembly reference. (Exception from HRESULT: 0x80131040)
In turned out that I needed to point the database update
command at my startup project, so this was solved by changing the command line to being:
$ dotnet ef database update --startup-project ../Salon
This is running inside the SalonService project, but is also pointing at the Salon project.
Keyword not supported: 'host'
My next problem was that the dotnet ef database update --startup-project ../Salon
command was still giving an error:
$ dotnet ef database update --startup-project ../Salon Build succeeded. 0 Warning(s) 0 Error(s) Time Elapsed 00:00:04.83 An error occurred while calling method 'ConfigureServices' on startup class 'Startup'. Consider using IDbContextFactory to override the initialization of the DbContext at design-time. Error: Keyword not supported: 'host'. System.ArgumentException: Keyword not supported: 'host'. at System.Data.Common.DbConnectionOptions.ParseInternal(Dictionary`2 parsetable, String connectionString, Boolean buildChain, Dictionary`2 synonyms) at System.Data.Common.DbConnectionOptions..ctor(String connectionString, Dictionary`2 synonyms) at System.Data.SqlClient.SqlConnectionString..ctor(String connectionString) at System.Data.SqlClient.SqlConnectionFactory.CreateConnectionOptions(String connectionString, DbConnectionOptions previous) at System.Data.ProviderBase.DbConnectionFactory.GetConnectionPoolGroup(DbConnectionPoolKey key, DbConnectionPoolGroupOptions poolOptions, DbConnectionOptions& userConnectionOptions) at System.Data.SqlClient.SqlConnection.ConnectionString_Set(DbConnectionPoolKey key) at System.Data.SqlClient.SqlConnection.set_ConnectionString(String value) at Microsoft.EntityFrameworkCore.Storage.Internal.SqlServerConnection.CreateDbConnection() at Microsoft.EntityFrameworkCore.Internal.LazyRef`1.get_Value() at Microsoft.EntityFrameworkCore.Migrations.Internal.Migrator.Migrate(String targetMigration) at Microsoft.EntityFrameworkCore.Design.Internal.MigrationsOperations.UpdateDatabase(String targetMigration, String contextType) at Microsoft.EntityFrameworkCore.Design.OperationExecutor.UpdateDatabase.<>c__DisplayClass0_1.<.ctor>b__0() at Microsoft.EntityFrameworkCore.Design.OperationExecutor.OperationBase.Execute(Action action) Keyword not supported: 'host'.
This was because my code was still referring to the SqlServer settings in my DbContext. I commented out the UseSqlServer
line and replaced it with a UseNpgsql
line.
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { // optionsBuilder.UseSqlServer(GetAppSettingsConnectionString()); optionsBuilder.UseNpgsql(GetAppSettingsConnectionString()); }
Once this was done, and I built all my projects, the command then ran successfully and ran the migrations to create my database.
Auto Increment Serials
When I ran my application and tried to insert data into my Postgres tables, I hit a problem. The database engine wasn't auto creating the primary key column, it was expecting it to be passed in from the software. In Postgres, it needed to auto populate the column from a sequence, and this wasn't happening.
After some digging, I found that I needed to change my migrations to add another annotation. I need to investigate how to do this in the source, instead of hacking around with migrations, but this gets us going for the time being.
Each of the migrations that created a primary key on a table looked like this:
Id = table.Column<int>(nullable: false) .Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn), Name = table.Column<string>(maxLength: 50, nullable: false)
I needed to add a second annotation so that it looked like this:
Id = table.Column<int>(nullable: false) .Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn) .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.SerialColumn), Name = table.Column<string>(maxLength: 50, nullable: false)
I did this all the serial columns, deleted the tables in the database, and re-ran the migrations. The database now auto assigns Id values on INSERT.