C# Architecture – Purpose of Domain/Business Logic in Classes with Repositories

Architectureasp.net-mvccrepository

From my experience I think that having classes/models without behaviour only in my application, next to their repositories is not good OOP. But, this was the way I implemented the repository pattern. I just make everywhere I need an repository instance, to perform some actions. The result of this approach was that all my domain classes didn't have behaviour.

They were just objects holding data with no methods. My teacher said to me that I was using thin models and that I should strive to make fat models. In response to that feedback, I implemented some business logic in the classes, but I ran into some problems:

Example:

public class Movie
{
    private MovieRepository movieRepo = new MovieRepository(new MovieDbContext());
    private PostRepository postRepo = new PostRepository(new PostDbContext());

    public decimal Rating { get; set; }
    public List<Post> Posts { get; set; }

    public void Rate(User user, Movie movie)
    {
        if(movie.Rating < 0 || movie.Rating > 10) 
        {
            throw new Exception("The rating must be a digit between 0 and 10");
        }
        this.Rating = movie.Rating;
        movieRepo.RateMovie(user.Id, movie.Id, (int)movie.Rating);
    }

    public void AddPost(User user, Movie movie, string text)
    {
        int maxId = 0;
        foreach (Post p in Posts)
        {
            if (p.Id > maxId)
            {
                maxId = p.Id;
            }
        }

        Post post = new Post(maxId + 1, user, text, DateTime.Now);
        this.Posts.Add(post);

        postRepo.AddPost(user.Id, movie.Id, text, DateTime.Now);
    }


}

As you can see in the example above, I first handle some domain logic, to perform actions on the class itself and then I persist it to the database using a repository. But why am I even adding the posts to the class itself in the AddPost-method, when I handle it with a repository right after?

Is this just because you can now actually see the changes you made directly on the screen, in-memory? Or is business logic's purpose only to validate parameter's input, as shown in the movie Rate method? But these kind of exceptions can also be thrown in the repository if the repository method also checks if a digit is between 0 and 10. But i think a repository shouldn't be concerned about that. The repository only needs to translate input to database information, am I right?

But having said that, I don't understand exactly the need of performing changes on the object itself, when you handle it with a repository. Instead of that you could to this(anywhere in your application, to display the user object):

postRepo.AddPost(2, 1, "Nice movie!", DateTime.Now);
User user = userRepo.GetById(2);

What are the pros and cons of this difference?

Best Answer

What is the purpose of domain/business logic in classes when having repositories?

This is kind of like asking:

What is the purpose of cars when we have garages?

The reason is that Business Classes and Repositories solve different problems, and therefore are different Concerns in the application. As such, they need to be in separate classes.

A Repository's main purpose is to provide a layer of abstraction between persistence and your code. Switching database vendors, or even storage mediums (database, flat file, web service, etc) shouldn't matter outside of your Repository classes.

The purpose of a Business Class is to enforce business logic.

The purpose of separating business logic from persistence logic is so you can apply business logic without worrying about persistence. Maybe you've got a data import. Unit tests then don't need a database just to validate business rules.

Think of the requirements you have now:

  1. A Movie Rating must have a User
  2. A Movie Rating must have a Movie
  3. A Movie Rating must be between 0 and 10
  4. If a User has previously rated a Movie, the rating will be changed
  5. If a User has not rated a Movie, the rating will be added
  6. A User must have a username
  7. A User has zero or more movie ratings
  8. A User can rate movies

None of these have anything to do with inserting, updating, selecting or deleting data in the database. In fact, these same rules could be applied if you switch persistence to an XML file.

Now, consider these Business Classes:

First, a silly stub for the Movie:

public class Movie
{
    public int Id { get; set; }
    public string Title { get; set; }
}

Now we know a Movie Rating is composed of three things: A user; a movie, and a number rating.

The User class:

public class User
{
    public User(string username)
    {
        // Requirement #6
        if (string.IsNullOrEmpty(username))
            throw new ArgumentNullException("username");

        Username = username;

        // Requirement #7
        movieRatings = new Collection<MovieRating>();
    }

    // Requirement #6
    public string Username { get; private set; }

    // Requirement #7
    private ICollection<MovieRating> movieRatings;

    // Requirement #6
    public IEnumerable<MovieRating> MovieRatings
    {
        get { return movieRatings; }
    }

    // Requirement #4
    public MovieRating GetRating(Movie movie)
    {
        return MovieRatings.FirstOrDefault(rating => rating.Movie.Id == movie.Id);
    }

    // Requirement #8 and #1
    public MovieRating RateMovie(Movie movie, int rating)
    {
        // Requirement #2
        if (movie == null)
            throw new ArgumentNullException("movie");

        var movieRating = GetRating(movie);

        if (movieRating == null)
        {
            // Requirement #5
            movieRating = new MovieRating(this, movie, rating);
            movieRatings.Add(movieRating);
        }
        else
        {
            // Requirement #4
            movieRating.ChangeRating(rating);
        }

        return movieRating;
    }
}

The MovieRating class:

public class MovieRating
{
    // Requirement #8 and #1
    internal MovieRating(User user, Movie movie, int rating)
    {
        // Requirement #1
        if (user == null)
            throw new ArgumentNullException("user");

        // Requirement #2
        if (movie == null)
            throw new ArgumentNullException("movie");

        // Requirement #3
        if (IsValidRating(rating))
            throw new ArgumentOutOfRangeException("rating", "Rating must be between " + MIN_RATING + " and " + MAX_RATING);

        User = user;
        Movie = moview;
        Rating = rating;
    }

    public User User { get; private set; }
    public User Movie { get; private set; }
    public int Rating { get; private set; }

    // Requirement #3
    public const int MIN_RATING = 0;
    public const int MAX_RATING = 10;

    // Requirement #3
    public static bool IsValidRating(int rating)
    {
        return rating >= MIN_RATING && rating <= MAX_RATING;
    }

    // Requirement #4
    public void ChangeRating(int newRating)
    {
        // Requirement #3
        if (IsValidRating(newRating))
            throw new ArgumentOutOfRangeException("newRating", "Rating must be between " + MIN_RATING + " and " + MAX_RATING);

        Rating = newRating;
    }
}

I've put comments in the C# code to illustrate how the Business classes (User, Movie and MovieRating) enforce business logic.

Noteworthy features of this code:

  • The constructor for the MovieRating class is marked internal restricting who can create instances of this class to code inside the same Assembly as the class.

  • The RateMovie method on the User class is public and is the only thing that creates the MovieRating objects. This ensures that you have correctly linked the right User with the movie when adding it to the private movie ratings collection

  • The User.movieRatings field is private so the User class has full control over how MovieRating's are created

  • The User.MovieRatings property is an IEnumerable<MovieRating> so that client code must call the RateMovie method on the User class in order to rate a movie for that user.

  • The minimum and maximum ratings are codified as constants on the MovieRating class

  • A static IsValidRating method is public so any code, regardless of whether or not a MovieRating object is available, has one central place to know if a rating is valid or not. Think form field validators in the presentation/web layer of your application.

  • The RateMovie method finds an existing rating and changes it, or creates a new MovieRating object if one doesn't exist (requirement #4)

  • None of these features have anything to do with how data is inserted or updated