Don't worry about single responsibility principle. It's not going to help you make a good decision here because you can subjectively choose a particular concept as a "responsibility." You could say the class' responsibility is managing data persistence to the database, or you could say its responsibility is to perform all the work related to creating a user. These are just different levels of the application's behavior, and they're both valid conceptual expressions of a "single responsibility." So this principle is unhelpful for solving your problem.
The most useful principle to apply in this case is the principle of least surprise. So let's ask the question: is it surprising that a repository with the primary role of persisting data to a database also sends e-mails?
Yes, it very much is surprising. These are two completely separate external systems, and the name SaveChanges
does not imply also sending notifications. The fact you delegate this out to an event makes the behavior even more surprising, since someone reading the code can no longer easily see what additional behaviors are invoked. Indirection harms readability. Sometimes, the benefits are worth the readability costs, but not when you're automatically invoking an additional external system that has effects observable to end users. (Logging can be excluded here since its effect is essentially record keeping for debugging purposes. End users do not consume the log, so there is no harm in always logging.) Even worse, this reduces flexibility in the timing of sending the e-mail, making it impossible to interleave other operations between the save and the notification.
If your code typically needs to send a notification when a user is successfully created, you could create a method that does so:
public void AddUserAndNotify(IUserRepository repo, IEmailNotification notifier, MyUser user)
{
repo.Add(user);
repo.SaveChanges();
notifier.SendUserCreatedNotification(user);
}
But whether this adds value depends on your application's specifics.
I'd actually discourage the existence of the SaveChanges
method at all. This method will presumably commit a database transaction, but other repositories might have modified the database in the same transaction. The fact it commits all of them is again surprising, since SaveChanges
is specifically tied to this instance of the user repository.
The most straightforward pattern for managing a database transaction is an outer using
block:
using (DataContext context = new DataContext())
{
_userRepository.Add(context, user);
context.SaveChanges();
notifier.SendUserCreatedNotification(user);
}
This gives the programmer explicit control over when changes for all repositories are saved, forces the code to explicitly document the sequence of events that must occur before a commit, ensures a rollback is issued on error (assuming that DataContext.Dispose
issues a rollback), and avoids hidden connections between stateful classes.
I'd also prefer not to send the e-mail directly in the request. It would be more robust to record the need for a notification in a queue. This would allow for better failure handling. In particular, if an error occurs sending the e-mail, it can be tried again later without interrupting saving the user, and it avoids the case where the user is created but an error is returned by the site.
using (DataContext context = new DataContext())
{
_userRepository.Add(context, user);
_emailNotificationQueue.AddUserCreateNotification(user);
_emailNotificationQueue.Commit();
context.SaveChanges();
}
It's better to commit the notification queue first since the queue's consumer can verify that the user exists before sending the e-mail, in the event that the context.SaveChanges()
call fails. (Otherwise, you'll need a full-blown two-phase commit strategy to avoid heisenbugs.)
The bottom line is to be practical. Actually think through the consequences (both in terms of risk and benefit) of writing code a particular way. I find that "single responsibility principle" doesn't very often help me do that, while "principle of least surprise" often helps me get into another developer's head (so to speak) and think about what might happen.