Clean Architecture

The architecture should be independent of frameworks. Meaning that a developer should be able to replace an out of date framework with a new framework without having to make changes to the architecture.

Repository Benefits

  • Minimises duplicate query logic:
    var products = repository.GetTopSellingProducts(category,
                                                   count)
  • Promotes test-ability.
  • Decouples an application from persistence frameworks, such as entity framework.

    This give developers options to replace old persistence frameworks as and when new persistence frameworks are released.

    New persistence frameworks are released about every few years:

    2002 ADO.NET • 2007 LINQ to SQL • 2007 NHibernate • 2008 Entity Framework v1 • 2010 Entity Framework v4 • 2011 Entity Framework v4.1 • 2012 Entity Framework v5& • 2013 Entity Framework v6 • 2016 Entity Framework Core 1.0 • 2017 Entity Framework Core 2.0 • 2019 Entity Framework Core 3.0.

What should the Repository Look Like?

A repository should consist of at least the following methods:

Add(obj)
Remove(obj)
Get(id)
GetAll()
Find(predicate)

Note that there is no update, remove or save. Because the repository acts as a collection of objects in memory. Instead the Unit of Work is used

Unit of Work

Maintains a list of objects affected by a business transaction and coordinates the writing out of changes.

Patterns of Enterprise Application Architecture by Martin Fowler

Simplicity is the ultimate sophistication

Patterns of Enterprise Application Architecture by Martin Fowler

Implementing the Repository Pattern

Unit of Work

Note that Complete() is the equivalent of Save(), but is semantically a better choice for a "Unit of work".

Implementation In Visual Studio

Note for simplicity the code examples contain only synchronous methods. In the real word asynchronous methods should also be implemented.

IRepository Interface

using System;
using System.Collections.Generic;
using System.Linq.Expressions;

namespace Example.Core.Repositories
{
    public interface IRepository<TEntity> where TEntity : class
    {
        #region Getter
        TEntity Get(int id);
        IEnumerable<TEntity> GetAll();
        IEnumerable<TEntity> Find(Expression<Func<TEntity, bool>> predicate);
        TEntity SingleOrDefault(Expression<Func<TEntity, bool>> predicate);
        #endregion

        #region Adder
        void Add(TEntity entity);
        void AddRange(IEnumerable<TEntity> entities);
        #endregion

        #region Removers
        void Remove(TEntity entity);
        void RemoveRange(IEnumerable<TEntity> entities);
        #endregion
    }
}

Implementing IRepository

using Example.Core.Repositories;
using System;
using System.Collections.Generic;
using System.Data.Entity;
using System.Linq;
using System.Linq.Expressions;


namespace Example.Persistence.Repositories
{
    public class Repository<TEntity> : IRepository<TEntity> where TEntity : class
    {
        private DbSet<TEntity> _entities;
        protected readonly DbContext Context;


        public Repository(DbContext context)
        {
            Context = context;
            // Here we are working with a DbContext,
            // not MyAppContext. So we don't have DbSets 
            // such as Courses or Authors,
            // and we need to use the generic Set() method to access them.
            _entities = Context.Set<TEntity>();
        }

        public TEntity Get(int id)
        {
            return _entities.Find(id);
        }

        public IEnumerable<TEntity> GetAll()
        {
            return _entities.ToList();
        }

        public IEnumerable<TEntity> Find(Expression<Func<TEntity, bool>> predicate)
        {
            return _entities.Where(predicate);
        }


        public TEntity SingleOrDefault(Expression<Func<TEntity, bool>> predicate)
        {
            return _entities.SingleOrDefault(predicate);
        }

        public void Add(TEntity entity)
        {
            _entities.Add(entity);
        }

        public void AddRange(IEnumerable<TEntity> entities)
        {
            _entities.AddRange(entities);
        }

        public void Remove(TEntity entity)
        {
            _entities.Remove(entity);
        }

        public void RemoveRange(IEnumerable<TEntity> entities)
        {
            _entities.RemoveRange(entities);
        }
    }
}

No Track Queries and DbContext in EF Core

var product = _context.Products.AsNoTracking().FirstOrDefault();

To make this universal add this line to the contractor of the class that inherits from DbContext:

ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;

Creating a Custom Repository

For this example lets imagine a Course class.

First create and interface

using Example.Core.Domain;
using System.Collections.Generic;

namespace Example.Core.Repositories
{
    public interface ICourseRepository : IRepository<Course>
    {
        IEnumerable<Course> GetTopSellingCourses(int count);
        IEnumerable<Course> GetCoursesWithAuthors(int pageIndex, int pageSize);
    }
}

Implement the interface

using Example.Core.Domain;
using Example.Core.Repositories;
using System.Collections.Generic;
using System.Data.Entity;
using System.Linq;

namespace Example.Persistence.Repositories
{
    public class CourseRepository : Repository<Course>, ICourseRepository
    {
        public CourseRepository(MyDbContext context) 
            : base(context)
        {
        }

        public IEnumerable<Course> GetTopSellingCourses(int count)
        {
            return MyDbContext.Courses.OrderByDescending(c => c.FullPrice).Take(count).ToList();
        }

        public IEnumerable<Course> GetCoursesWithAuthors(int pageIndex, int pageSize = 10)
        {
            return MyDbContext.Courses
                .Include(c => c.Author)
                .OrderBy(c => c.Name)
                .Skip((pageIndex - 1) * pageSize)
                .Take(pageSize)
                .ToList();
        }

        public MyDbContext MyDbContext
        {
            get { return Context as MyDbContext; }
        }
    }
}

Code for Unit of Work

Create the interface

using Example.Core.Repositories;
using System;

namespace Example.Core
{
    public interface IUnitOfWork : IDisposable
    {
        ICourseRepository Courses { get; }
        IAuthorRepository Authors { get; }
        int Complete();
    }
}

Implementation

using Example.Core;
using Example.Core.Repositories;
using Example.Persistence.Repositories;

namespace Example.Persistence
{
    public class UnitOfWork : IUnitOfWork
    {
        private readonly MyDbContext _context;

        public UnitOfWork(MyDbContext context)
        {
            _context = context;
            Courses = new CourseRepository(_context);
            Authors = new AuthorRepository(_context);
        }

        public ICourseRepository Courses { get; private set; }
        public IAuthorRepository Authors { get; private set; }

        public int Complete()
        {
            return _context.SaveChanges();
        }

        public void Dispose()
        {
            _context.Dispose();
        }
    }
}

Using the Code

An Example

using Example.Persistence;

namespace Example
{
    class Program
    {
        static void Main(string[] args)
        {
            using (var unitOfWork = new UnitOfWork(new MyDbContext()))
            {
                // Example1
                var course = unitOfWork.Courses.Get(1);

                // Example2
                var courses = unitOfWork.Courses.GetCoursesWithAuthors(1, 4);

                // Example3
                var author = unitOfWork.Authors.GetAuthorWithCourses(1);
                unitOfWork.Courses.RemoveRange(author.Courses);
                unitOfWork.Authors.Remove(author);
                unitOfWork.Complete();
            }
        }
    }
}
Copyright © 2024 delaney. All rights reserved.