Reputation: 629
I am developing an app with C# .NET in a DDD manner. I've also checked eshopcontainers, but it does not explain what I would like to know, so let me give a question here. My questions is
Detail
I am devloping a RSVP app. Now what I would like to do is sending reminder mail to people who has not voted yet. My domain model and Infrastructure layer is like this.
//Domain Model
class RSVP //RootAggreagate
{
public long Id {get; private set;}
public List<TimeSlot> TimeSlots {get; private set;}
public AutoRemindRule AutoRemindRule {get; private set;}
}
class AutoRemindRule
{
public long Id {get; private set;}
public int IntervalHour { get; private set; }
public DateTimeOffset NextTriggerDate { get; private set; }
public DateTimeOffset RemindBeginDate { get; private set; }
//Foreign Key for Plan
public long RSVPId
void SetNextTriggerDate()
{
//Compute NewNextTriggerDate based on IntervalHour and RemindBeginDate field.
NextTriggerDate = NewNextTriggerDate;
}
}
//Infrastructure Layer (EF Core)
public class MyDbContext : DbContext
{
//Plan Aggregate
public DbSet<RSVP> RSVPs { get; set; }
public DbSet<TimeSlot> TimeSlots { get; set; }
public DbSet<AutoRemindRule> AutoRemindRules { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
...
}
}
In this model, I would like to run a batch that checks NextTriggerDate
in AutoRemindRules
periodically, and if NextTriggerDate
is older than now, the batch sends mails to users who has not voted the RSVP. Eventually, the batch updates NextTriggerDate
by calling SetNextTriggerDate
. If I write a code like follows at Usecase(Application) layer in the batch, I can realize what I want. However, I do not think these codes follow the DDD rule because it updates the aggregate root partially. Is it ok to write a code like this in application layer, or if not, could anyone tell me a better way of coding?
//Usecase(Application) layer in a batch
using (var context = new MyDbContext)
{
var rules = context.AutoRemindRules.Where(i => i.NextTiggerdate < DateTimeOffset.Now);
foreach (var rule in rule)
{
SendRemindMail();
rule.SetNextTriggerDate();
context.SaveChanges();
}
}
Update
Another approach would be creating a method to update AutoRemindRule
in the aggregate root, and the batch utilizes the method. However, my concern is performance and the load on DB. in that case, the batch has to read a bunch of unnecessary RSVP records in addition to AutoRemindRule
records. I am wondering if there is another approach which is less load on DB while keeping the DDD manner.
Upvotes: 2
Views: 2448
Reputation: 2847
Is it OK to partially update an root aggregate directly from batch operation?
The short answer is: No.
The point of aggregates is encapsulating data and exposing business operations so that the validations and invariants are checked and enforced. If you let external processes modify the data of the aggregates, then it completely defeats their purpose.
Potential solutions:
Don't use Aggregates. If what you have is just a table with dates and some data and a batch job, but no or almost no business logic to enforce, then just do that: a table and a batch job. At first glance, it seems that the only thing you need to enforce is that the NextTriggerDate is sometime in the future and you can code that into the batch job. But I can imagine that you can have more rules, like don't trigger more than X times, don't trigger earlier than 1 day, etc. Or even let the aggregate decide the next trigger date, based on some internal logic (first trigger after 1 day, second after 2 days, etc). Aggregates are very handy for these things as each aggregate will store the state they'll need to calculate the next state change.
Evaluate if it really is a problem to load the full RSVP aggregate or not. Loading some extra columns shouldn't be a big performance hit for most applications and a necessary price to pay in order to get the benefits of having the aggregate encapsulating its business logic in case you need to do things like the ones I mentioned on the previous point. If instead, you are building a feature with massive scale and little business logic, then consider a different approach.
If the problem is that loading the full aggregate implies loading a big collection like the TimeSlots, that happens to be not necessary for that business operation, you could work around it by providing a specific method in your repository to load the aggregate without loading that collection. You should code your aggregate root operations that require that collection to validate if the collection was loaded and fail otherwise, to avoid bugs.
Upvotes: 3