this post was submitted on 14 Aug 2024
11 points (92.3% liked)

Learn Programming

1638 readers
1 users here now

Posting Etiquette

  1. Ask the main part of your question in the title. This should be concise but informative.

  2. Provide everything up front. Don't make people fish for more details in the comments. Provide background information and examples.

  3. Be present for follow up questions. Don't ask for help and run away. Stick around to answer questions and provide more details.

  4. Ask about the problem you're trying to solve. Don't focus too much on debugging your exact solution, as you may be going down the wrong path. Include as much information as you can about what you ultimately are trying to achieve. See more on this here: https://xyproblem.info/

Icon base by Delapouite under CC BY 3.0 with modifications to add a gradient

founded 1 year ago
MODERATORS
 

I understand the basic principle but I have trouble determining what is the hard line separating responsibilities of a Repository or a Service. I'm mostly thinking in terms of c# .NET in the following example but I think the design pattern is kinda universal.

Let's say I have tables "Movie" and "Genre". A movie might have multiple genres associated with it. I have a MovieController with the usual CRUD operations. The controller talks to a MovieService and calls the CreateMovie method for example.

The MovieService should do the basic business checks like verifying that the movie doesn't already exist in the database before creating, if all the mandatory fields are properly filled in and create it with the given Genres associated to it. The Repository should provide access to the database to the service.

It all sounds simple so far, but I am not sure about the following:

  • which layer should be responsible for column filtering? if my Dto return object only returns 3 out of 10 Movie fields, should the mapping into the return Dto be done on the repository or service layer?

  • if I need to create a new Genre entity while creating a new movie, and I want it to all happen in a single transaction, how do I do that if I have to go through MovieRepository and GenreRepository instead of doing it in the MovieService in which i don't have direct access to the dbcontext (and therefore can't make a transaction)?

  • let's say I want to filter entries specifically to the currently logged in user (every user makes his own movie and genre lists) - should I filter by user ID in the MovieService or should I implement this condition in the repository itself?

  • is the EF DbContext a repository already and maybe i shouldn't make wrappers around it in the first place?

Any help is appreciated. I know I can get it working one way or another but I'd like to improve my understanding of modern coding practices and use these patterns properly and efficiently rather than feeling like I'm just creating arbitrary abstraction layers for no purpose.

Alternatively if you can point me to a good open source projects that's easy to read and has examples of a complex app with these layers that are well organized, I can take a look at it too.

top 10 comments
sorted by: hot top controversial new old
[–] GetOffMyLan 4 points 2 months ago* (last edited 2 months ago)
  1. The general wisdom is that the dbcontext is already a repository and you should use Select statements to grab only the data you require from the database. So in your service you can do something like
    await context.Movies.Where(...)
    .Select(m => new { Property1,Propert2, ... })
    .AsAsyncEnumerable()
    .SelectAsync(m => new Dto(...))

this way you only pull the exact fields you need.

  1. Just new up genre, add your movie to it and add it to the dbcontext and call save changes.

  2. Again just do a where against the context.

  3. It is and there's no need to add another layer that limits what you can do with it.

This argument for a repo layer was all the rage about 10 years ago. It is absolutely not required and as you've identified makes working with the dbcontext a pain.

For testing either mock the dbcontext or use the in memory db provider.

One thing to look at to formalize your queries is compiled queries. These give you strongly typed queries and removed the SQL generation step each time you run it.

[–] [email protected] 4 points 2 months ago (1 children)

I'm not too familiar with EF specifically, but I'll give you my general advice:

  1. Column filtering can be done in either the repository or service layer.
  2. Your transaction should be scoped to the service method. MovieService should call both repositories.
  3. Prefer filtering in the repository
  4. I don't know this one
[–] Cyno 1 points 2 months ago* (last edited 2 months ago) (1 children)
  1. but if I do it on the repository layer I have to have a separate method for every possible filter combination, right? if i want to do it on the service layer i have to return an IQueryable which is also allegedly a bad practice (and i might as well return the entire dbset at that point)? also, should the repository be aware of my application (or even view) layer dto models?
  2. this means the service has direct access to the database (dbcontext in this case)? or do you expose opening a transaction through some repository too?
[–] [email protected] 2 points 2 months ago
  1. See GetOffMyLan's comment. An application will have a finite set of queries it needs to make. Depending on the complexity you may have to enumerate them, but sometimes you can build them up dynamically, or even come up with SQL that will cover multiple cases depending on parameter values.
  2. I think GetOffMyLan's advice applies here too.

When you're creating a layered architecture you want to limit a lower layer from knowing what's above it. If you tightly couple your data layer to the view then you lose flexibility. Imagine having to implement some batch processing features that don't involve the web.

A lot of this is overkill for a small app so if some it seems pointless it may be because there isn't a team of devs working on the same codebase.

[–] ericjmorey 3 points 2 months ago (1 children)

It think you get it. Repositories are superfluous abstractions that are focused on mocking databases for unit testing. They are entirely unnecessary, unless you're being micromanaged over unit tests.

[–] Cyno 2 points 2 months ago (1 children)

Well mocking a repository is pretty much the same process as mocking the dbcontext too, right? If that's the only purpose then I can see why they would seem unnecessary

[–] FunctionalOpossum 1 points 2 months ago

The typical way to test a repository is to create an in memory database and manually load data into it.

This sucks.

If you directly use the dbcontext in your service layer, then you have to populate data for every edge case to test it.

Make a super simple repository with no logic, and you only need 1 test case per method. Then in your service layer you mock the repository and tell it to return any random object you need.

[–] FunctionalOpossum 1 points 2 months ago* (last edited 2 months ago)

How I think about repositories:
These are simple wrappers around database calls that have few or no dependencies. If I'm adding dependencies here, then they'll either be a cache or a logger.

Make repositories as simple as possible, because texting them sucks. You make a repository because it makes unit testing your service easier.

How I think about services:
Services are just groups of related methods. In a simple CRUD API, I would typically do a service per controller and include all business logic at the service level.

How I think about controllers:
These should be treated like repositories and only contain logic for determining which response code to send.

[–] Cyno 1 points 2 months ago (1 children)

Additional question - I said at first that the "Service" should be doing the mandatory checks like uniqueness validation or whether the fields are filled in properly with good values, but is even that a good approach?

Instead of implementing this in every service that might create a new Movie (and it could be from different sources - import from file, different APIs, background worker, etc), wouldn't it make more sense to add these checks to the repository itself so they always gets called?

Alternatively, do we have to handle a constraint violation in every service or could we just have the repository return a result with failure if it happens?

In short, once I start thinking in this way I start to wonder why even have a separation between repository and service.

[–] [email protected] 2 points 2 months ago

The repository is responsible for talking to the db. Services are where your business logic lives (and ofc there's always some leakage of logic into the db).

It's ok for a service to call other services and multiple repositories, but a repository should only talk to the db. It should not call other repositories or anything else.

It's ok to rely on db constraints for validation checking.