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; }
And tried out this code (notice I’m using context.Add, not context.Quotes.Add):
static void AddQuoteToSamurai ()
{
using (var context =newSamuraiContext ())
{
var quote=newQuote("Voila",1);
context.Add(quote);
context.SaveChanges();
}
}
And it worked! But this isn’t complete yet. I’m breaking my rule of ensuring that only my aggregate root can manage quotes. So this is “dangerous” code from my DDD perspective. However, I was happy to know that EF Core would support this capability.
Currently, Samurai.AddQuote does not have any additional logic to be performed on the quote. What if I were to add in a “RemoveBadWords” rule before a quote can get added?
public void AddQuote (string quoteText)
{
Utilities.RemoveBadWords(quoteText);
var newQuote=new Quote(quoteText,Id);
_quotes.Add (newQuote);
}
Now I have an important reason to use Samurai to do the deed. I can add a second, static AddQuote method that also takes an int. Because it’s static, it’s a pass through method.
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.