Why Your OnModelCreating Is a Hot Mess (And How to Fix It)
Entity Framework Core configurations become cleaner and testable when persistence rules move into `IEntityTypeConfiguration` classes loaded through assembly scanning.
Transcript 7 topics
Centralize EF Core rules
0:00If your DB context is looking like a Costco receipt and your entities have more attributes than there are crumbs in my beard, then today's for you. We're gonna clean up that in the framework configuration to add in separation of concerns, declutter it a lot, and give you a pattern by the end of this video that you can drop into any application, whether it's a minimal API, a full ASP dot NET Core application, or even a background service where you just need to create some data context without all the drama. Early on Entity Framework Core makes it really easy to add attributes onto properties of your entities, or even just load all that into an on model creating. That's great until you start adding your fifteenth entity and all of a sudden that method starting to grow personality. I've got a simple minimal API setup where there's nothing it really does except for run.
There's no end points to it. I've stripped out all the weather forecast stuff that we load in by default. All that's clean. Let's make this implement what we're talking about. And if we're familiar with this, if we were gonna create some classes, maybe we have some domain and then under that entities and then under that a subscriber subscriber.
If I'll remember how to spell subscriber and I've actually got a class I can paste in here. And that looks pretty familiar, right? Just like ID, first name, last name, that's just kind of standard stuff. And then we might wanna go and create a DB context and say infrastructure, infrastructure. Thank you.
And then do app db context dot c s. And if you thought your boy had this ready to copy and paste as well, you're right because I lazy and it's a lot easier that way. But here, here's our DB context. Pretty simple. We just say, hey, there's a table.
It's got some subscribers in it and you know, all is well with the world. And of course now we need to go to our web API and say, hey, I need to register that DB context so that it's available for dependency injection. We'll come over here and I'm just gonna replace that as well. And you can see, there we go. We create the builder.
We inject our DB context into that service layer. We build it out. We have one path that returns subscribers. And if we run this, it'll run. There's no subscribers in there.
So it's been empty array, but all is well with the world except we never added our rules. We never told them the framework, hey, we're gonna add more than this. Right? We're gonna have a create and an update and delete and all that and hey, email address needs to be unique.
First name needs to only be five, 50 characters. Let's say, maybe it needs to be required.
Use entity annotations
2:27So how would we go about doing that? Well, the classic way would be to go add annotations to our class. So we could go in and do that and just replace that class actually replace all that with just this and there we go. We've got key on our index, our ID up there. We've got required and max lengths on their appropriate fields.
We've got required on these other two properties. This all makes sense, right? This is code we've seen before, but now we've got persistence logic tied to our business logic and it's just a mess. I don't like the way that works. I wish it was separate.
Right? So of course then our option is on model creating. So let's strip all this out and say, no, no, no, no. Go back to the way it was because that's prettier.
We like it better. And then let's go to our on model creating over here and oops, do it inside there.
Move to OnModelCreating
3:14Do all that logic inside that method. And this makes sense, We're saying, hey, with that model that we told you you have of subscriber, hey, it has to be this table name this, and it's gonna have this side. This property is gonna be a key and this one's gonna be a max length of this enterprise. It's the same kind of thing. It's just in a more fluent way, which you can imagine how long is this method gonna get as we add new components and new entities?
Why did I say components? That doesn't make any sense. Entities, types, whatever classes, it's gonna get really long, right? Well, I mean, there are other options, right? I've seen people do like extension methods that say, hey, hey, I've got my subscriber class.
And in my same subscriber class, I've seen people go in and say like, hey, look, I'm gonna create an extension that's maybe static and it does all this work for me. And I just pass it in and over here I just come over here and say subscriber.registerme. But still I don't like that either because it's still keeping my persistence logic coupled with the data logic, the business logic, right? Not the data logic, the business logic. I gotta put my teeth in and use my words here.
Anyway, we can see where this would be lengthy and kinda unruly over time and there's a better way. So let me show you how to do it. But before we do, let's clean all this stuff up. Let's get rid of that on model creating. I think we cleaned it out of the class already.
Now we wanna use I entity type configuration. Why? One, once we get it set up for a class, we don't have to worry about it anymore. Entity framework is gonna be able to scan our assembly, see all those configurations, and load them for us, which means we don't have to come back to this DB context and register everything all over again every time we have a new, like, entity or something. And it makes it super testable.
I'll show you how. Let's check this out. We're gonna go over and go into our infrastructure. Let's add a new folder called configuration. And, you know, one of the thing I wanna point out is I've got these folders up here or up here, I guess.
But the thing that I should notice, this is a small project running locally In a larger application, this is likely gonna be broken out into other class libraries, right? So this infrastructure folder might be an infrastructure project that's built as a class library, whereas that domain might be its own project. And it probably should be. All these should probably be separated so we can just test individual things. But for this point of reference in this example, you get the point, you know, it's all in the same place.
Extract config classes
5:45Anyway, go in here and create subscriber config. Cs and let's paste in what I'm talking about here. And we're gonna need to tell it where subscriber is in this case. So include that reference and there we go. Now we've got something that implements I entity type configuration.
It's gonna set up that, configure method to implement that interface. And it's everything related to a subscriber's configuration. It looks very much like the on model creating, doesn't it? It's basically the same exact thing, only it's moved here outside of that on model creating. So every entity would have a corresponding entity config file like subscriber, subscriber config, address, address config, order, order config.
You get how this is going. Right? But what makes it really nice is it's separated. It's the configuration for the persistence logic for this class, for this dataset, this database in one place. So if we ever did have to come back and say, hey, unplug that SQL Server version to insert something else.
We have all this logic contained. We can replace it for the logic for that thing. And it's not touching our business logic at all. None of our entities and types, all that kind of garbage. And I mean garbage in a very nice way.
But if you just looked at this and said, well, bruh, how is any framework gonna know that that's the config for this other class? It's like two different namespaces and now there's nothing in the app, the DB context anymore. Right?
Scan configs by assembly
7:20This is the magic of the whole thing. We'll come into the on model creating and we'll add in this model builder, apply configurations from assembly method, and pass it this assembly. It needs to stay together, right? So your configurations need to stay in the same namespace that the whole class library that you're in, right? So don't put those over in domain because you're wanting this assembly here.
This DLL, if it's a class library, this XE, whatever you got, whatever you're got. Anywho, let's go on. So this is gonna scan the assembly when it's building and running and say, give me all the configurations. I'll load them all in. I see you all.
Yes, subscriber, you're there, blah, blah, blah, blah, loads them in. So that means when we have a new entity, all we have to do is come up here and say, you know, public DB set, whatever, and add it. And then it's gonna apply that. It's not even gonna care. It's not gonna compare that list unrelated, but it'll pick up the configurations.
All you have to do is add the DB set so that it references that domain type and you're good to go. Now you might say, bro, that is too legit. In fact, you might even say it's too legit to quit. Oh, wow. I just dated myself there.
But let's talk about some other niceties that we can build on top of this that make it really pay off in the long run. Now, one of these you can already do without it, but it's still kind of cool. So if we had like an enum and say subscription status and we wanted to say down here public subscription status, we'll call it status. Get set equals subscription status dot active, save that. We wanna be able to tell our DB context, hey, don't store that enum as an enum normally would be as like a number because that can get us into real trouble.
We would rather our database be more readable. So instead of saving zero for unknown, save unknown instead of active or one for active, choose active. That way somebody's looking through the database, can tell what it is. It also means that when we change databases, it's a lot easier to migrate that data. Thirdly, it means that you can't get messed up.
And if you've never had this happen to you, you are blessed. Let me know in the comments if this has ever happened to you. Has someone ever come in and added a new property to a new type or whatever to a status or whatever you want to call it to your enum right in the stinking middle and said, you know, green, who knows? All of a sudden that looks great. Code runs, app runs.
Wait, my reports are all wrong. This is all wrong. Things are going crazy because the index for green is two, whereas it used to be paused. If this data was stored in the database as strings, this wouldn't be as big of an issue.
But darn that guy, you know that guy, that guy don't do that. Anyway, let's show how to add that to your DB context or really your config to say, hey, DB context, when you register the subscriber, save this enum as a string, not as the editor, integer backing value.
Store enums as strings
10:23To do that, we just gotta go over to the config file. We'll come back in, add this. Let's tab that over a little bit. And the same deal. Property status says has conversion string.
So it'll convert that to a string with a max length of 20. We gotta remember that because we can't ever make a value in our enum longer than 20, which we probably shouldn't. We want it as small as possible, honestly. And then make it required in our case. But you can say well I can do this already in on model creating yeah but it looks a lot sexier over here right?
I mean come on this is a lot better a lot better. Anyway let's get on. I mean, even soft deletes, let's say, what if we came over to our entity and said something like public bool is deleted. I get set and we'll say it's false by default. You actually don't have to do that because it is false by default.
And then is deleted, we'll come over to our DB context. And then there we'll keep it the same. We'll add in that model builder entity subscriber has query filter. Now in this case, wanna keep it close to the the line above it or, know, kinda keep all these things that are like setting up our types and and configuring those persistence rules together so that they're not scattered amongst other things like caching or logging or other middleware or whatever. But in this case now, anytime we look for subscribers, it'll automatically only bring us back to ones where it is deleted is false.
So without having to do anything else, like any query we run against the database, this query is gonna get applied to that unless we opt out of it. But heck, even seeding data is really easy. We'll come back over to our config file and just add it in. Maybe down towards the bottom so that it doesn't conflict with our rules. But there we are.
That's the wrong name of that. That's called, is commenter. Yeah. Yeah. It's an old code.
And then, you know, we'd had to put in some more things like the first name and last name because we said those were required, but still it works. Right? And what's great is that's gonna get picked up in migrations and then it'll get applied to your database as you need it. Even things like different schemas, like if you've ever needed it, it's rare, but if you've ever needed to change the default schema for your app, come over to the model creating, add in model builder dot add default schema. In my case, it's BBB.
Of course it's BBB, come on. Otherwise, it's probably DBO if you're in SQL. But anyway, disregard it. It should be BBB. I petitioned Microsoft to change this as the default from now on.
I may be leaving the DBO. Anyway, you can see how though this whole process is a lot cleaner and it makes it more testable. More testable you say? Yes, I do say.
Here's why. Let's show off testing this whole thing.
Test config rules
13:24Now your boy don't have a testing project set up. So I'm just gonna come over here and say, test tests that we should probably be its own project. I'm not when I say probably should be, it should be. Not should be. Make it so.
Make it so number one. Now let's just call it subscriber config tests dot cs and then we'll just paste in some code that says basically using this we probably need to give this a namespace but knows namespace Not like that. Not like this. Can has subscribed our tests and I think we have everything we need here, but this actually is set this up to be written in a console. So it's not really built for being in a minimal API application, but you see the context here where we're just getting the context.
We're using an in memory database. So we're not even using SQL Server at this point, but we're checking not SQL Server. We're checking our rules and our patterns. Right? So we can come in and say, find that type of entity in your model, find that property on that entity and then console right line or in the test case, might wanna assert that prop get max length equals whatever it should be.
So you could have unit tests for your entity type configurations. All of that persistence logic testable so that you don't make mistakes down the road when someone changes a number inadvertently, or if someone does make a change, they have to think through, oh, wait, was this done for a reason? Why does this matter so much to have a test for it? Hopefully it slows your junior developer's role or copilot's role as it's trying to make changes to your code that you don't want changed. Or it flags you when you see those PRs where it is changed.
Now we have separation of concerns. We are testable. We have less clutter in our business and domain logic. We have our persistent and infrastructure code just right there together and ready to work together and just go team. And we could wrap all these kinds of checks with unit tests and then no database needed, no migrations to run, just a quick sanity check that all of our types are gonna be loaded to the database as we expect them.
But if your project has annotations sprinkled everywhere, or you are one of those people with a 500 plus line on model creating, carve out one entity and try this pattern and then just move into it as you go. You'll thank yourself later once everything's converted and your code will be much calmer place to write in until next time.
Sign in to join in. Reading needs nothing.