As far as I know, each Aggregate root has it's own repository and the repository should work only with Aggregate Entity and it's Primary Key.
For example, for the Aggregate Appointment:
public class Appointment
{
public int Id { get; set; }
public DateOnly ForDate { get; set; }
public string ForTimeInterval { get; set; }
public Person ForPerson { get; set; }
}
one version of Repository for Appointment could be the following:
public class AppointmentRepository
{
public Appointment FindById(int Id) { }
public void Appointment Add(Appointment appointment) { }
public void Remove (Appointment appointment) { }
public int Count() { }
}
My question is, are any of the following methods allowed in the Repository or should they be in some other place, like AppointmentDataAccess(DAO/DAL):
public class Repository
{
. . .
public IEnumerable<Appointment> FindAppointmentsForDate(DateOnly date) { }
public IEnumerable<Appointment> FindAppointmentsForPerson(Person person) { }
. . .
}
As a side note, I need to make a query that returns all free TimeIntervals for some Date. TimeInterval is also free if there are no more than X amount of reservations. All of this is business logic so it makes sense, to me, to have the two methods in the Repository and check logic in Application Service(or Domain Service maybe???). The data is used for populating Drop-downs or similar UI elements, which usually I do using more abstract Data Access logic like DAO/DAL and the sorts.
Question is tagged CQRS but is not necessary to post an answer for it.
FindAppointmentsForDate
Consider what would happen if you don't have that method in the repository. You would be forced to retrieve every entity into memory and filter them in the service. You might end up retrieving millions of entities every time. Something like this:
NB: This is a bad idea!
public class AppointmentService(AppointmentRepository repo)
{
public IEnumerable<Appointment> FindAppointmentsForDate(DateOnly date)
{
// Get all appointments for all time.
var all = await repo.GetAllAppointments();
// Filter in memory.
return all.Where(a => a.ForDate.Date == date).ToArray();
}
}
It's unlikely to be practical, and I imagine you'll instinctively (correctly!) feel that's a bad idea.
So, it's entirely appropriate to do simple filtering like this in a repository. Common examples are:
This starts to look like business logic, but that doesn't mean a hard "no" to having a repository method. The answer depends on several factors:
If you can confidently answer "yes" to both of the above, you might consider putting that logic in the repository implementation. Your first question meets these criteria. But for your second question: it's hard to give an answer without knowing more about the entities.
To give some idea of why the exact use-case is important, here are two examples:
Assumptions:
Appointments
Slots
Maybe we could say there's not much that would need unit testing here. So we could implement it with a simple SQL statement with an outer join, a GROUP BY
and a HAVING
. The exercise left to the reader! Your repository method might end up like:
IEnumerable<DateTimeOffset> FindFreeSlotsForDate(DateOnly date);
Let's say, perhaps, the maximum number of appointments per timeslot varies depending on other factors. For example:
Now, you'd probably want to unit test the logic. You want to try different test cases and validate it. Your repository should just have a FindAppointmentsForDate
and the business logic would be implemented in the service.
This is a completely fictional example, but hopefully it illustrates the level of complexity that belongs in a service rather than a repository:
public record Timeslot(
String TimeslotType, // E.g. Weekday
TimeOnly TimeOfDay, // E.g. 1100
int MaxNormalAppointmentsPermitted,
int MaxSpecialAppointmentsPermitted
);
public class AppointmentService(AppointmentRepository repo)
{
public async Task<IEnumerable<Timeslot>> FindFreeTimeslots(DateOnly date)
{
var appointments = await repo.FindAppointmentsForDate(date);
Timeslots[] allTimeslots = await repo.GetTimeslotsConfiguration();
var appointmentsByTime = appointments.ToLookup(a => a.ForDate);
var freeTimeslots = allTimeslots.Where(t =>
{
var appointmentsThisSlot = appointmentsByTime[t.TimeOfDay];
var specialAppointments = appointmentsThisSlot.Where(a => a.Type == "Special");
var normalAppointments = appointmentsThisSlot.Where(a => a.Type == "Normal");
if (appointmentsThisSlot.Count() > specialAppointments.Count() + normalAppointments.Count())
{
throw new MyException("Can't handle this appointment type");
}
if (normalAppointments.Count() > t.MaxNormaAppointmentsPermitted) return false;
if (specialAppointments.Count() > t.MaxSpecialAppointmentsPermitted) return false;
return true;
}
return freeTimeslots.ToArray();
}
}
Hope that helps!