EntityFramework Core 2.1 in .NET Standard 2.0 NuGet package

Sharing Entity Framework repositories across different frameworks was harder than I thought, so I created a recipe. I wanted to put my EF code inside a NuGet package, and from a consumer, I didn’t want to think about database stuff.

If I had clients stored in a database, I wanted to do simple stuff, like this:

...
services.AddSingleton<IRepositoryFactory, RepositoryFactory>();
...
IClientRepository _clientRepository = repositoryFactory.GetClientRepo();
...
var clients = await _clientRepository.GetAsync();
...

So I created

  • A “data layer” project, containing all the EF code. This is a .NET Standard 2.0 class library.
  • And a “repository” project, containing public classes (the NuGet packet)

To follow along, you need access to an SQL Server database. At the time of writing this post, my Visual Studio 2017 version was 15.9.2.

Create the solution

Start by creating a new solution


Create a new solution

I called mine TestSharedEf. 

Add a new .NET Standard library


Add a new .NET Standard library project for the data layer

Add the following NuGet packages to this project:

  • Microsoft.EntityFrameworkCore
  • Microsoft.EntityFrameworkCore.SqlServer

DbContext and Entities

Lets create the DbContext and an table/entity

Add a folder to your project called Entities and remove the Class1.cs file.

Create a new class called Client with some properties, in the Entities folder:

public class Client
{
public string ClientId { get; set; }

[StringLength(255)]
public string ClientName { get; set; }
}

Add the necessary using statement when prompted.

Create a new class for the DbContext:

public class EfDbContext : DbContext
{
public EfDbContext(DbContextOptions<EfDbContext> options)
: base(options)
{
}

public DbSet<Client> Clients { get; set; }
}

I don’t like injecting the DbContext directly, so we’ll also create a ContextFactory:

namespace EfDataLayer
{
public interface IContextFactory
{
EfDbContext GetContext();
}
}
using Microsoft.EntityFrameworkCore;

namespace EfDataLayer
{
public class ContextFactory : IContextFactory
{
private readonly DbContextOptions<EfDbContext> _options;

public ContextFactory(DbContextOptions<EfDbContext> options)
{
_options = options;
}

public EfDbContext GetContext()
{
return new EfDbContext(_options);
}
}
}

The project now looks like this:

Dummy Startup project

Now we want to create a migration. To do this, we use the dotnet ef tool. However, the tool only work with .NET Core and .NET Framework projects. We just created a .NET Standard project.

To get around this, we create a “dummy” .NET Core console app. Call this EfDataLayer.Startup:


Add EfDataLayer.Startup project

This projects only purpose is to act as a startup project for the dotnet ef tool. From the Microsoft documentation:

Why is a dummy project required? As mentioned earlier, the tools have to execute application code at design time. To do that, they need to use the .NET Core runtime. When the EF Core model is in a project that targets .NET Core or .NET Framework, the EF Core tools borrow the runtime from the project. They can’t do that if the EF Core model is in a .NET Standard class library. The .NET Standard is not an actual .NET implementation; it’s a specification of a set of APIs that .NET implementations must support. Therefore .NET Standard is not sufficient for the EF Core tools to execute application code. The dummy project you create to use as startup project provides a concrete target platform into which the tools can load the .NET Standard class library.

Entity Framework Core tools reference – .NET CLI

Add the following NuGet packages to this project:

  • Microsoft.EntityFrameworkCore.Design
  • Microsoft.EntityFrameworkCore.SqlServer

Let Main (in Program.cs) be empty and add DesignTimeContext.cs:

public class DesignTimeContext : IDesignTimeDbContextFactory<EfDbContext>
{
public EfDbContext CreateDbContext(string[] args)
{
var connectionString = "Server=<server>;Database=TestSharedEf;Trusted_Connection=True;MultipleActiveResultSets=true";
var builder = new DbContextOptionsBuilder<EfDbContext>();
builder.UseSqlServer(
connectionString,
b => b.MigrationsAssembly("EfDataLayer"));

return new EfDbContext(builder.Options);
}
}

Notice the connection string. In a real application, you want to put it into a parameter file or key vault.

Add using statements and reference the EfDatalayer project.

Add migration

You are now ready to run the migration command. First make sure you have the latest .NET Core SDK installed. This have to be installed even if you have the latest version of Visual Studio.

Open a command prompt or PowerShell. Move to your solution folder and run the following command:

dotnet ef

You should see the EF Unicorn:

EF Unicorn

To create the initial migration, build your solution and run the following commands:

dotnet ef migrations add Initial -s EfDataLayer.Startup -p EfDataLayer
dotnet ef database update -s EfDataLayer.Startup -p EfDataLayer

You can also run the command from the Packet Manager Console

You can ignore warnings like this:

“The EF Core tools version ‘2.1.3-rtm-32065’ is older than that of the runtime ‘2.1.4-rtm-31024’. Update the tools for the latest features and bug fixes.”

See https://github.com/aspnet/Home/releases/tag/2.1.3#known-issues

If your connection string is setup correctly, the first command will also a Migrations folder to your project with three files:


Solution Explorer

The second command will create the database (if it doesn’t exist) and then add the Clients table.

Notice the parameters

-s: The startup project is the one that the tools build and run. The tools have to execute application code at design time to get information about the project, such as the database connection string and the configuration of the model. By default, the project in the current directory is the startup project. You can specify a different project as startup project by using the -s (or –startup-project) option.

-p: The project is also known as the target project because it’s where the commands add or remove files. By default, the project in the current directory is the target project. You can specify a different project as target project by using the -p (or –project) option.

EF Repositories project

Add a new .NET Standard class library project called EfRepositories to your solution.

Add the following NuGet package:

  • AutoMapper

In the solution folder, right-click the projects Dependencies and add a reference to the EfDataLayer project.

Remove the Class1.cs file.

Add two folders

  • Mapping
  • Dtos (Data Transfer Objects)

In the Dtos folder, add Client.cs

public class Client
{
public string ClientId { get; set; }
public string ClientName { get; set; }
}

In the Mapping folder, add EntityMappingProfile.cs

using AutoMapper;
using DataClient = EfDataLayer.Entities.Client;

namespace EfRepositories.Mapping
{
public class EntityMappingProfile : Profile
{
public EntityMappingProfile()
{
CreateMap<DataClient, Dtos.Client>()
.ReverseMap();
}
}
}

Vi then add files for the Client repository:

namespace EfRepositories
{
public interface IRepositoryFactory
{
IClientRepository GetClientRepo();
}
}
using AutoMapper;
using EfDataLayer;
using EfRepositories.Mapping;
using Microsoft.EntityFrameworkCore;

namespace EfRepositories
{
public class RepositoryFactory : IRepositoryFactory
{
private readonly ContextFactory _contextFactory;
private readonly IMapper _mapper;

public RepositoryFactory()
{
var connectionString = "Server=<server_name>;Database=TestSharingEf;Trusted_Connection=True;MultipleActiveResultSets=true";
var builder = new DbContextOptionsBuilder<EfDbContext>();
builder.UseSqlServer(
connectionString,
b => b.MigrationsAssembly("EfDataLayer"));
_contextFactory = new ContextFactory(builder.Options);

var config = new MapperConfiguration(cfg => { cfg.AddProfile<EntityMappingProfile>(); });
_mapper = config.CreateMapper();
}

public IClientRepository GetClientRepo()
{
return new ClientRepository(_contextFactory, _mapper);
}
}
}

You should get the connection string from a parameter file or key vault. We hard code it here for simplicity.

using System.Collections.Generic;
using System.Threading.Tasks;
using EfRepositories.Dtos;

namespace EfRepositories
{
public interface IClientRepository
{
Task<IEnumerable<Client>> GetAsync();
Task<Client> GetAsync(string clientId);
Task<Client> InsertAsync(Client client);
Task<bool> DeleteAsync(string clientId);
}
}
using System.Collections.Generic;
using System.Threading.Tasks;
using AutoMapper;
using EfDataLayer;
using EfRepositories.Dtos;
using Microsoft.EntityFrameworkCore;
using DataClient = EfDataLayer.Entities.Client;

namespace EfRepositories
{
public class ClientRepository : IClientRepository
{
private readonly IContextFactory _contextFactory;
private readonly IMapper _mapper;

public ClientRepository(
IContextFactory contextFactory,
IMapper mapper)
{
_contextFactory = contextFactory;
_mapper = mapper;
}

public async Task<IEnumerable<Client>> GetAsync()
{
using (var context = _contextFactory.GetContext())
{
var clients = await context
.Clients.AsTracking()
.ToListAsync();

return _mapper.Map<IEnumerable<DataClient>, IEnumerable<Client>>(clients);
}
}

public async Task<Client> GetAsync(string clientId)
{
using (var context = _contextFactory.GetContext())
{
var client = await context
.Clients.AsTracking()
.FirstOrDefaultAsync(c => c.ClientId == clientId);
return client == null ? null : _mapper.Map<DataClient, Client>(client);
}
}

public async Task<Client> InsertAsync(Client client)
{
using (var context = _contextFactory.GetContext())
{
var dataClient = _mapper.Map<Client, DataClient>(client);
var trackedClient = context.Clients.Add(dataClient);
await context.SaveChangesAsync();

return _mapper.Map<DataClient, Client>(trackedClient.Entity);
}
}

public async Task<bool> DeleteAsync(string clientId)
{
using (var context = _contextFactory.GetContext())
{
var client = await context
.Clients.AsTracking()
.FirstOrDefaultAsync(c => c.ClientId == clientId);
if (client == null)
return false;

context.Clients.Remove(client);
await context.SaveChangesAsync();
return true;
}
}
}
}

Creating the NuGet package

NuGet feed

I have my own NuGet feed in Azure. You can find out how to create your own feed here:

Get started with NuGet
or you can use NuGet.org.

Publish the package

When you right-click your project, you might have see a Pack command:

Pack menu command

We can’t use this, since it will not add the EntityFramework binaries to the package.

As of VS2017 version 15.9.0, the pack command will not get the packages a dependent project (EfDataLayer in our case) depends on. Read more about creating and publishing NuGet packages here.

You will have to create a .nuspec file, and do it the “old fashioned” way.

You can use this .nuspec file as a template:

<?xml version="1.0" encoding="utf-8"?>
<package xmlns="http://schemas.microsoft.com/packaging/2012/06/nuspec.xsd">
<metadata>
<id>AH.ForTesting.EfRepositories</id>
<version>1.0.0</version>
<authors>Arve Hansen</authors>
<owners>Arve Hansen</owners>
<requireLicenseAcceptance>false</requireLicenseAcceptance>
<description>Package Description</description>
<tags>Test Demo Arve Hansen</tags>
<dependencies>
<group targetFramework=".NETStandard2.0">
<dependency id="Microsoft.EntityFrameworkCore" version="2.1.4" exclude="Build,Analyzers" />
<dependency id="Microsoft.EntityFrameworkCore.Design" version="2.1.4" exclude="Build,Analyzers" />
<dependency id="Microsoft.EntityFrameworkCore.SqlServer" version="2.1.4" exclude="Build,Analyzers" />
<dependency id="AutoMapper" version="8.0.0" exclude="Build,Analyzers" />
</group>
</dependencies>
</metadata>
<files>
<file src="netstandard2.0\EfDataLayer.dll" target="lib\netstandard2.0\EfDataLayer.dll" />
<file src="netstandard2.0\EfRepositories.dll" target="lib\netstandard2.0\EfRepositories.dll" />
</files>
</package>

What you should change:

  • The Id tag. This is the name of your NuGet package
  • The version is ok, we’ll overwrite it from the command line
  • Author and Owner
  • Give a description
  • Some appropriate tags

Make sure the versions under dependencies corresponds with the packages that you used in the EfRepositories project. E.g. if you used AutoMapper version 8.0.0, and the version under dependencies is 7.0.1, you will have a problem when you run the consumer later.

Save the .nuspec file in the bin directory. You can use debug for now.

Open a command prompt and go to EfRepositories/bin/debug (where you saved the .nuspec file).

Run the command:

nuget pack <nuspec_file> -Version 1.0.5

or whatever your version is.

If you haven’t added your feed to your NuGet Source, do it now. Adding my feed looks like this:

nuget.exe sources Add -Name "ForTesting_Nuget_Feed" -Source "https://arvehansen.pkgs.visualstudio.com/_packaging/ForTesting_Nuget_Feed/nuget/v3/index.json"

You can now push your package to your feed:

nuget.exe push -Source "ForTesting_Nuget_Feed" -ApiKey VSTS .\AH.ForTesting.EfRepositories.1.0.5.nupkg

The NuGet client’s push command requires an API key. You can use any non-empty string you want.

Credentials Provider

NuGet 3 and later supports the Credential Provider, which automatically acquires feed credentials when needed. I downloaded mine from my NuGet feed in Azure. Go to your feed, and click “Connect to feed”:

Download credentials manager

Put the CredentialProvider.VSS.exe in the same folder as your nuget.exe.

Consuming the NuGet package

We now have everything wrapped up in a NuGet packet, so lets try and use it.

Add a new ASP.NET Core Web Application project to your solution (a consumer of your NuGet will not be in the same solution as you NuGet project, but we’ll do it here for simplicity):

Add EfConsumer project


Add EFConsumer project settings

Add the NuGet packet you created earlier (AH.ForTesting.EfRepositories) to this project.

If the NuGet package manager doesn’t have your feed, add it. See above.

In Startup.cs, add the following service:

using EfRepositories;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;

namespace EfConsumer
{
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}

public IConfiguration Configuration { get; }

// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
services.AddSingleton<IRepositoryFactory, RepositoryFactory>();
}

// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseHsts();
}

app.UseHttpsRedirection();
app.UseMvc();
}
}
}

Add a ClientsController:

using System.Threading.Tasks;
using EfRepositories;
using Microsoft.AspNetCore.Mvc;

namespace EfConsumer.Controllers
{
[Produces("application/json")]
[Route("api/Clients")]
public class ClientsController : Controller
{
private readonly IClientRepository _clientRepository;

public ClientsController(IRepositoryFactory repositoryFactory)
{
_clientRepository = repositoryFactory.GetClientRepo();
}

[HttpGet]
public async Task<IActionResult> GetAsync()
{
var clients = await _clientRepository.GetAsync();
return Ok(clients);
}

[HttpGet("{id}")]
public async Task<IActionResult> Get(string id)
{
var client = await _clientRepository.GetAsync(id);
return Ok(client);
}

}
}

Set your Startup project to be EfConsumer, run your project and see that it works:

Browser showing no clients

(Since the database is empty, there are no clients)

The two main takeaways:

  1. When you put your EF code inside a .NET Standard class library, you need a “dummy” project for the migration tool
  2. You can’t use the Pack command from the Solution Explorer menu, you have to create a .nuspec file.

Isn’t it funny how simple tasks can be so hard the first time you do them 🙂