3

Suppose I have the following entities:

class Employee
{
    public string Id { get; set; }
    public ICollection<EmployeeBadge> Badges { get; set; }
}

class Badge
{
    public string Id { get; set; }
    public Employee Owner { get; set; }
}

The employee and badge may or may not be stored in different ways, which is an implementation detail and is not to be exposed. For this reason I decide to use a repository pattern, with the repositories being provided using dependency injection.

Naturally, I would create a repository for employees with CRUD operations, which would reference the storage mechanism employees are stored in.

I might also create a repository for employee badges, but because badges only make sense in the context of an employee, many methods in the repository would have to take the employee as a parameter. It would make more sense that each employee gets their own badge repository.

However, the badge repository would need to have access to the storage mechanism for employee badges. Since that is an implementation detail, and also because I'm using dependency injection, I can't require the repository to be instantiated using an employee.

Therefore, I decide to implement the factory pattern with a factory that has access to the badge storage mechanism, which would be called with an employee and create a repository specific to the employee, passing it the employee and storage mechanism during instantiation.

Is this an established/common pattern? If so, what is it called?

8
  • "It would make more sense that each employee gets their own badge repository." Is one employee going to have that many badges that it makes sense to address each employee's badges as a separate data set? I get the feeling that there's a mismatch here between the scope of your approach and the scope of your actual use case.
    – Flater
    Jul 7, 2021 at 11:12
  • "I decide to implement the builder pattern with a builder that has access to the badge storage mechanism, which would be called with an employee and create a repository specific to the employee" Am I understanding this correctly in that you consider "a repository" and "the storage mechanism" to be two distinct concepts? Because generally speaking, a repository is considered to be the storage mechanism (or the encapsulation thereof).
    – Flater
    Jul 7, 2021 at 11:14
  • @Flater Yes, this is an example to demonstrate the idea but in my actual scenario each entity has many 'sub-entities' attached to it and in most cases they only make sense in the context of that entity.
    – Sara E
    Jul 7, 2021 at 11:15
  • @Flater The repository would be what's exposed in order to perform CRUD on employee badges, but internally it would use a storage mechanism like PostgreSQL to actually store the badge details. As your edit references, the repository encapsulates the PostgreSQL database because it's an implementation detail.
    – Sara E
    Jul 7, 2021 at 11:16
  • I suspect we may be subtly talking next to each other. To clarify what I intended to say: a repository usually represents the access mechanism to a unified storage mechanism, e.g. the Badges database table. Not "Tom's badges". The separation you're trying to implement sounds much more like a DDD aggregate concept, rather than something that should be implemented on the repository level. I'm trying to establish if you (a) know about aggregates (b) have possibly already dismissed aggregates and (c) if your understanding of what a repository is matches with the general definition.
    – Flater
    Jul 7, 2021 at 11:20

1 Answer 1

1

Based on the comment exchange on the question, I suspect that the source of the confusion here is about the role a repository plays in this setup.

You mentioned:

I decide to implement the builder pattern with a builder that has access to the badge storage mechanism, which would be called with an employee and create a repository specific to the employee

This implies that you consider the repository to not be the storage mechanism. While not impossible, "repository" tends to refer to the storage mechanism. Concretely, if all badges are stored in a single database table, then there is one repository that represents that table.

I might also create a repository for employee badges, but because badges only make sense in the context of an employee,

Be very careful to separate your BLL (or Domain) logic from your DAL logic here. While from a business perspective, it makes sense that badges are in scope of a person, that does not mean that the storage structure has to follow suit!

The simplest example here is that you could be storing all badges in the same table. Even though the domain logic never works with a set of "all badges" (without a person's context), the database actually does do so.

many methods in the repository would have to take the employee as a parameter.

In and of itself, that is IMO not enough justification.

I do agree that software development means automation, and automation means tackling repetition and needless effort Developers are "lazy", as they say. But it's very easy to go a bridge too far with this, often leading to shortcuts and starting to tread in the territory of bad practice.

If "I don't want to repeat the person's ID" is your main concern, then I am of the opinion that you've gone a bridge too far. That's too lazy. Because then you could also argue that almost every repository often requires a PK value (for all operations on a singular resource, other than the Create logic), which would similarly lead you to pre-configure the PK value so you don't need to put it in the method parameters. ​

It's technically viably, but conceptually undesirable.

Note however that there are ways to mitigate this and find a happy middle ground in cases where you'd be passing both the badge and person ID to the same method. You could create compound keys, which are a combination of your specific resource's PK and the aggregate root PK (= the resource's FK).

public struct BadgeReference
{
    public int PersonId { get; }
    public int BadgeId { get; }

    public BadgeReference(int personId, int badgeId) : this()
    {
        PersonId = personId;
        BadgeId = badgeId;
    }
}

This way, you don't have to add an extra method parameter for the person's ID (you just substitute the int badgeId for BadgeReference badgeReference, and your repository can use the reference object to filter on both PK and FK. It also helps keeping things scoped neatly, making it harder to accidentally mismatch a person and a badge object.


You also mentioned:

I've read about aggregates, and from what I understand the employee would be the aggregate root and badges would be children of employees. That is what I'm trying to implement here, and I want to see how it would work together with the repository pattern.

There are a few ways to skin this cat.

Aggregates should be fetched in full. Therefore, if Badge falls under the Person aggregate root, any fetched person would have their badges loaded.
Note: Some devs deviate from this for performance's sake, but that is off-label usage.

Depending on how your data is stored, and how complex its retrieval is, you can take different approaches here.

  • You could have a separate PersonRepository and BadgeRepository, and the Domain logic composes a person from these two sources.
  • You could have a separate PersonRepository and BadgeRepository, but only the PersonRepository is made public (outside of the assembly) and the BadgeRepository is used as a dependency of the PersonRepository.
  • PersonRepository could just act as the repository for the full aggregate root.

These choices are contextual. It depends on the complexity of your data structure. Maybe a BadgeRepository is overkill, e.g. if badges are really just an integer (albeit wrapped in a neat Badge class, of course). Maybe BadgeRepository is not useful since the data is somehow stored with the person data and not in a separate source (i.e. database table, file, ...). Maybe badges are very complex data and BadgeRepository helps divide the workload away from the Person logic.

Technically, you could pick any of these options and match them with any of the storage methods (separate table, with person data, ...), and it will always be workable. But depending on what your reasonable expectations are about the way this data is structured, some choices make more sense than others.


Therefore, I decide to implement the builder pattern with a builder that has access to the badge storage mechanism, which would be called with an employee and create a repository specific to the employee, passing it the employee and storage mechanism during instantiation.

As an aside, I'm more inclined to call that a factory pattern than a builder pattern. You're not really leveraging the "many optional configurations" part of what distinguishes a builder from a factory. This seems to be a much more straightforward "you must provide X and I'll give you Y back".

2
  • "Concretely, if all badges are stored in a single database table, then there is one repository that represents that table" - I understand where this view is coming from, but I'm inclined to question it. While this is what people usually do, I think it comes from database-centric thinking. If we agree that a repository represents an abstraction of some storage, and provides an abstract interface to it, why should it be identified with a table? E.g., the data could come from a query. You can create an employee-specific badge repo that queries the single underlying Badges table just fine. Jul 8, 2021 at 10:29
  • 1
    @FilipMilovanović: The main point I wanted to address, which I maybe did not touch on enough in the first part, is that the person-scoped nature of badges in the business logic does not necessitate the same person-scoping in the DAL. I would argue that a repository is the conceptually "last stop" on the entity-oriented train towards the datastore, any further stops are either the database itself or the underlying connection/query mechanism. But maybe that's subjective :)
    – Flater
    Jul 8, 2021 at 11:07

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.