Where should EF Migrations go, my Class Library pr

2019-03-27 21:00发布

问题:

My solution contains:

  • FooBarAsp, an asp project (provides a UI for the application)
  • FooBar, a class library (the application)
  • FooBar.Tests, a test project (tests the application)

FooBar uses EF 6 Code First, and contains a number of models, and a DataContext. FooBarAsp uses Microsoft's Identity framework for user authentication, and has an ApplicationDbContext. Both contexts play nice and work as expected. Global.asax.cs should execute MigrateDatabaseToLatestVersion (right?). FooBar.Tests should execute DropCreateDatabaseAlways and won't care about migrations (right?).

Should I EnableMigrations in FooBarAsp, or FooBar? For both contexts?

After running EnableMigrations in FooBar (just for kicks), my __MigrationHistory table contains two InitialCreate rows, one with ContextKey=FooBar.Models.DataContext and the other with ContextKey=FooBarAsp.Models.ApplicationDbContext. So they're both being tracked already? If so, (and since I did not enable automatic migrations), do I need to explicitly run both MigrateDatabaseToLatestVersion<DataContext> and MigrateDatabaseToLatestVersion<ApplicationDbContext> in Global.asax.cs?


Edit

Why don't I want to move ApplicationUser into FooBar and merge ApplicationDbContext into DataContext?

The stock ApplicationUser that comes with ASP is a separate entity (and separate ApplicationDbContext, which happens to point to the same database). ASP's ApplicationUser and the Identity framework take care of authentication, registration, email verification, login, logout, passwords, password strength, passwords resetting, two factor authentication, cookies, sessions, external login from sources like facebook/google, etc. FooBar doesn't know or care about any of that. FooBar's Users have a UserName, and ASP's ApplicationUsers have a UserName. When a user logs in, I just look up Foobar's User by UserName, and that's who is logged in. So when I create a new Blog (where Blog is a FooBar entity), the author is FooBar's User (not ApplicationUser). The end result is that FooBar is not a Web/Desktop/Console/iPhone/Android application. It's a library that any of those things can reference, interact with, and provide a user interface for. The point is that FooBar is not polluted or biased by any of those user interfaces.

So I don't want to move ApplicationUser into FooBar because it's from the Microsoft.AspNet.Identity namespace, and FooBar doesn't know or care about ASP.NET (or WPF or whatever the interface happens to be).

回答1:

Like John said in his comment, you might be served well by moving all of your EF code into a separate class library. I typically create Web, Domain, DataAccess, and Test projects for a new solutions, and split them up even more down the road if it's necessary. This gives you some clear, basic separation of concerns up front. The Visual Studio Web project templates don't provide that, as it's designed to be self-contained in a single project.

Visual Studio's Web template is a good starting point for small projects but it pretty quickly becomes a mess as the project grows. Having the website (which is more or less your presentation layer), domain models & business logic, and data access all in the same project isn't particularly good practice. Partially separating them by leaving your user models and a separate DbContext behind in the web application can be even more confusing IMO, especially if your ApplicationUsers are going to participate in relationships with models from your domain class library.

I would suggest moving your ApplicationUser into the class library that contains your domain models, and take your ApplicationDbContext and move it into a DataAccess class library. You can take that one step further and combine your two DbContexts into one without much effort, unless you suspect that the project will grow large enough to require multiple `DbContexts. At that point, it becomes very apparent where your Migrations should live. Unless you're trying to abstract it away, your Initializer will always go in the application, which in your case is FooBarAsp, so you're correct that Global.asax is the place to set that up.

However, if this sounds unappealing to you and you'd like to keep your project structure as it currently is, it's my opinion that you would be best off configuring migrations for both DbContexts for consistency's sake. At that point, You'd have a set of Migrations in your web project (FooBarAsp) for the ApplicationDbContext that inherits from IdentityDbContext and another set of migrations that resided in your class library (FooBar). Both would need initializers in the Global.asax.cs of your web project.

Edit based on OP's update: I can see where you're coming from in your desire to keep ASP.NET related references out of your domain layer. It's basically impossible to keep this from happening without splitting up your user models in two as you've done. This is something I asked about when VS2013 was still pre-RTM and never got a satisfactory answer because it's currently not possible to have an User model that works with Identity that doesn't pull in some ASP.NET related assemblies. However, in a somewhat analogous situation, the DbGeography type can be a very useful type to include in your domain layer, but it's impossible to use it without pulling in Entity Framework related assemblies. It really comes down to weighing the pros and cons and picking the lesser of two evils- be pragmatic in when and where you apply design patterns.

That said, if you do want to keep the DbContexts separate and the ApplicationUser in your web application, I'd suggest pulling out all the extraneous fields from the application user- in fact, just use the IdentityUser class itself instead of subclassing it since you'll be using it purely for authentication and authorization. However, I believe you'll still need to enable Migrations for both contexts and look into how to use multiple contexts per database with EF6, which wasn't possible in EF5.