If you are like me and design your classes following Domain-Driven Design principals , you may find yourself with code like this for controlling how objects get added to collections in the root entity.
public class Samurai { public Samurai (string name) : this() { Name = name; } private Samurai () { _quotes=new List<Quote>(); } public int Id { get; private set; } public string Name { get; private set; } private readonly List _quotes = new List () private IEnumerable Quotes => _quotes.ToList (); public void AddQuote (string quoteText) { var newQuote=new Quote(quoteText,Id); _quotes.Add (newQuote); }
I have a fully encapsulated collection of Quotes. The only way to add a new quote is through the AddQuote method. You can’t just call Samura.Quotes.Add(myquote).
Additionally, because I want to control how developers interact with my API, there is no DbSet for Quotes. You have to do all of your queries and updates via context.Samurais.
A big downside to this is that if I have a new quote and I know the ID of the samurai, I have to first query for the samurai and then use the AddQuote. That really bugs me. I just want to create a new quote, push in the Samurai’s ID value and save it. And that requires either raw SQL or a DbSet<Quote>. I don’t like either option. Raw SQL is a hack in this case and DbSet<Quote> will open my API up to potential misuse.
I was thinking about this problem while laying in bed this morning (admit it, that’s the first thing you do when you wake up, too, right?) and had an idea.
In EF Core, we can now add objects directly to the context without going through the DbSet. The context can figure out what DbSet the entity belongs to and apply the right info to the change tracker. I thought this was handy for being able to call
myContext.AddRange(personobjectA, accountobjectB, productObjectC);
Although I haven’t run into a good use case for leveraging that yet.
What occurred to me is that if DbContext.Add is using reflection, maybe EF Core can find a private DbSet.
So I added a private DbSet to my DbContext class:
private DbSet<Quote> Quotes { get; set; }
static void AddQuoteToSamurai () { using (var context =newSamuraiContext ()) { var quote=newQuote("Voila",1); context.Add(quote); context.SaveChanges(); } }
public void AddQuote (string quoteText) { Utilities.RemoveBadWords(quoteText); var newQuote=new Quote(quoteText,Id); _quotes.Add (newQuote); }
public static Quote AddQuote(string quoteText,int samuraiId) { Utilities.RemoveBadWords(quoteText); var newQuote=newQuote(quoteText,samuraiId); return newQuote; }
This works and now I don’t have to have an instance of Samurai to use it:
staticvoid AddQuoteToSamurai () { using (var context =newSamuraiContext ()) { context.Add(Samurai.AddQuote("static voila",1)); context.SaveChanges(); }
One thing I was worried about was if I had an instance of Samurai and tried to use this to add a quote to a different samurai. That would break the aggregate root…it’s job is to manage its own quotes only. It shouldn’t know about other Samurais.
But .NET protects me from that. I can’t call the static method from an instance of Samurai.
I still think that there’s a little bit of code smell from a DDD perspective about having this static, pass-through method in an aggregate root so will have to investigate that (or wait for any unhappy DDDers in my comments). But for now I am happy that I can avoid having to query for an instance of Samurai just to do this one task.
Sign up for my newsletter so you don't miss my conference & Pluralsight course announcements!
Nice article!
I think “taht” should probably be “that”, “u pdates” should be “updates”, and “jsut” should be “just”
haha thanks! That’s what I get for trying to write a long blog post before my coffee! I even found a few more that you missed. 🙂
You can put that method in a service contract, then implement as you want in data access layer
Agreed. Where it is now (a console app) is just for proof of concept.
Don’t you want to make the IEnumerable Public?
From a DDD perspective, is a DbSet a Domain thing or a Repository thing?
If I get your example you are using it “all the way”.
DbSet is part of the ORM API (Entity Framework). Not part of the domain.
I believe that the inability to call a static method from the instance is a C# restriction, not a .NET restriction.
After all, the compiler knows the type of the variable (in statically typed systems) and can easily resolve it as a static method if instance methods fail to resolve.
That said, I believe this is what VB does (and some other .NET languages may as well).
That is really good to know! Thank you.
Nice article, great stuff to think about.
I agree from a DDD perspecive there is a little bit of a code smell. You have context about adding quotes via a Sumari aggregaat, but the client is responsible for it’s input now. (by providing a correct SumariaId)
Again, great stuff to think about.
And thank you for some extra spelling errors. 🙂
“I shave to first query for the samurai…” 🙂