0

I am building an ASP.NET MVC application. My database has many-to-many relationship with a Intermediate table.

In my application, I am using repository pattern without Unit-Of-Work. I have a generic repository with CRUD operations defined in it.

Because I am using Entity Framework with a database-first approach, I have created my models from EDMX.

The intermediate table which I have is not showing up in .edmx file, but it is indicated by diamond sign so apparently that defines many to many.

This is an background to what I have. Now the issues

I have 2 tables Student & Books. In my view, I want a to display a form which has fields from both Student and Books table. Idea is each student will fill in their details and they will choose the books they are interested to read and then they will hit "Submit" upon submit their record should be save in my database and for admin I want all that record stored to be display for which I will use accordion to show the data. Because I am using generic repo at a time, I am injecting only one Student repo into my controller and upon form creation I only get data from Student table (i.e. their details) - I don't get fields from books where they can select the books.

Can someone please suggest me a solution?

For backup I am thinking if this doesn't work, I will get all the details in one single table in my database and use that. But I want to avoid that approach.

Any ideas, suggestions will be really helpful.

1
  • 1
    Don't change the database design because you want to use repositories. You should think of why you use the repository pattern if it clearly doesn't help you. Commented Jul 18, 2022 at 6:39

1 Answer 1

2

If you are using a Generic Repository pattern, then consider replacing it with a more purpose-built Repository class to serve the controller or service that wants to interact with the data domain. Generic Repositories, while extremely common out there in examples and such are an anti-pattern especially when it comes to Entity Framework. The reason is because they are poorly suited to the task and don't follow the intent of a Generic pattern. Generic classes are classes optimized where you can treat all instances entirely equally. This means if I have a Repository<Student> and a Repository<Book> then every operation between a Student and Book should be identical.

With EF, working on such assumptions is either crippling the capabilities that EF can bring, or adding a lot of unnecessary complexity to your solution to enable features like eager loading, filtering, projection, sorting, pagination, etc. While a Generic Repository can still serve as a base class for a repository, even then, the common capability that it can really provide doesn't really make it very worthwhile.

The other problem with Generic Repositories, or more specifically a Repository tied to a single Domain object is that it violates the Single Responsibility Principle. SRP is part of the S.0.L.I.D. design principles and states that a class should have one, and only one reason to change. While on the surface, using a Repository per domain object might seem like you're giving a repository one reason to change, this isn't really the case. Take something like a StudentRepository. How many controllers or services will need to interact with Students? Will they all be expecting to perform the exact same operations and have the exact same requirements of the Repository fetching and updating Students? Each consumer of a StudentRepository is a reason for that repository to change. One technique to get the most out of EF is to leverage Projection where-by we use Select or ProjectTo to significantly reduce the data size coming back and can leverage indexes for commonly used queries. If we are using a Generic Repository it's even worse because now the code in the repository has every reason to change as it needs to apply to all domain classes.

By all means you can write Generic Repositories or Repository-per-Domain Class to satisfy SRP, however the resulting Repository will either be extremely inefficient or extremely complex.

Instead, I recommend thinking of a Repository like you would a Controller in MVC, where a Repository has a single purpose: To Serve that Controller/Service.

For example, if I have a StudentController, I would create a StudentRepository. However, the purpose of StudentRepository is to serve the StudentController as opposed to the Student domain object. If the StudentController needs a list of Books, the StudentRepository will expose a method to retrieve them. A better example might be where I have a SearchStudentController and EditStudentController. Each of these would have respective SearchStudentRepository and EditStudentRepository. In this way the StudentRepository can expose methods specific to the needs of the Controller or Service that needs access to the domain. It has one, and only one reason to change.

The other advantage of this pattern is it makes dependency management a lot cleaner. Rather than a StudentController needing a StudentRepository, and a BookRepository, and a CourseRepository, and a ... It needs just one Repository to serve the domain. There may be a legitimate case to have more common Repository available for things like lookup values or such that pretty much all similar Controllers or Services might consume where that consumption is identical across all controllers.

The counter-argument to this approach is that code can be duplicated. For instance if you have a BooksRepository for listing/adding/managing books and a StudentsRepository that also needs to list books, then you can end up with duplicate or similar code for something like:

IEnumerable<Book> GetBooks();

However, these methods are often "similar" rather than "identical". When you want a list of Books for a particular Student, chances are you are filtering out books that are applicable to their courses, or the current revision, etc. When you are listing Books on a book search and management screen you might want to see/filter books by completely different criteria.

So if in your case we have a StudentController and a non-Generic, Controller-serving StudentRepository class, when we want to get a list of students we can explore options that don't impact anything else. At a start we can consider something like:

public async Task<IEnumerable<Student>> GetStudents()
{
     var students = await _context.Students
          .Include(s => s.Books)
          .ToListAsync();
     return students;
}

Doing this with a Generic Repository isn't really viable, but with a Repository designed to serve our specific needs we can write queries that meet those needs.

For something like search results where we don't need every detail, the repository could return a simplified DTO with the details that need to be displayed. For instance if we just wanted the student's ID #, Name, and # of books:

[Serializable]
public class StudentSummaryDTO
{
     public int StudentId { get; set; }
     public string FirstName { get; set; }
     public string LastName { get; set; }
     public int BookCount { get; set; }
}

Then in the repository:

public async Task<IEnumerable<StudentSummaryDTO>> GetStudents()
{
     var students = await _context.Students
          .Select(s => new StudentSummaryDTO
          {
              StudentId = s.StudentId,
              FirstName = s.FirstName,
              LastName = s.LastName,
              BookCount = s.Books.Count
          }).ToListAsync();
     return students;
}

This can generate a much faster and lighter weight query to run to return just enough data for the consumer. A more advanced variant is just to design the repository to return IQueryable<Student> to allow the consuming Controller to perform its own projection, pagination, etc.

Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.