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.