Design Patterns for Webdev

Dependency Injection

  • Overcome coupled classes
  • highly coupled
  • difficult to maintain
  • not testable

NEW == GLUE

Dependency Injection

  • Loosly coupled

Dependency Injection

  • Loosely coupled
  • Objects are created by DI container
    • Configured in a central place
  • Interface instead of concrete classes
    • implementation can be changed
    • testability
  • DI in three ways
    • constructor injection
    • method injection
    • property injection

Dependency Injection

  • ASP.NET Core Dependency Injection
public class MyDependency : IMyDependency
{
    private readonly ILogger<MyDependency> _logger;

    public MyDependency(ILogger<MyDependency> logger)
    {
        _logger = logger;
    }

    public Task WriteMessage(string message)
    {
        _logger.LogInformation(
            "MyDependency.WriteMessage called. Message: {MESSAGE}", 
            message);

        return Task.FromResult(0);
    }
}
//startup.cs
public void ConfigureServices(IServiceCollection services)
{
	//Demo (show with an example)
    ...	
}

Dependency Injection

  • Register additional services (groups of dependencies) with extension methods (Add...)

public void ConfigureServices(IServiceCollection services)
{
    services.Add(new ServiceDescriptor(typeof(IX), typeof(X), ServiceLifetime.Scoped));
    services.AddScoped<IX, X>();

    services.AddDbContext<ApplicationDbContext>(options =>
        options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));

    services.AddIdentity<ApplicationUser, IdentityRole>()
        .AddEntityFrameworkStores<ApplicationDbContext>()
        .AddDefaultTokenProviders();

    ...
}

Dependency Injection

//https://docs.microsoft.com/en-us/aspnet/core/fundamentals/dependency-injection?view=aspnetcore-3.0
public void ConfigureServices(IServiceCollection services)
{
    ...

        
        services.AddScoped<IAuthorRepository, AuthorRepository>();
        services.AddScoped<IMessageRepository, MessageRepository>();
        services.AddScoped<IBlackListRepository, BlackListRepository>();
            
        services.AddTransient<IUnitOfWork, UnitOfWork>();
    ...
}
  • Lifetime of Dependencies
    • Per (HTTP) Request
      • Scoped
    • Transient (tijdelijk)
      • Created every time when requested
    • Singelton (shared by all users & all request)
      • Created once, shared everywhere

Trick to Check if object are the same

  • In most debuggers, it's possible to "Make Object ID"
    • Check if life-time is correct and object reference variable use the same object

 

Object Marker

N - Layer architecture

  • Different Models for different "layers"
    • ViewModel
    • DataTransferModel (DTO)
    • Conversion between models, with Mapper (Assambler Pattern)

Repository Pattern

Repository Pattern

  • Separate business code from data retrieval and update logic
    • Shielded from Low-level Details
    • Any Data Source can be used (combination)
      • SQL Code (ADO.NET), ORM Frameworks, File, Webservice, NoSQL database, etc
  • Testability
    • Mock with stubs
    • Use a simple (in memory) database for testing
  • Data access is not the responsibility of Business Logic
    • SoC = Separation of Concerns
  • ORM/Data-access details changes over time, new features other/frameworks

Repository Pattern

Repository behaves like a collection!

Repository Generic Interface

    public interface IRepository<T> where T : class
    {
        T GetById(int id);
        IEnumerable<T> Get();
        IEnumerable<T> Get(Expression<Func<T, bool>> predicate);
        void Insert(T entity);
        void Delete(T entity);
        void Update(T entity);
    }

Repository Generic Abstract base class

public abstract class Repository<T> : IRepository<T> where T : class
{
    protected readonly DbContext _dbContext;

    //Normally Repository works only with UnitOfWork!
    //this is only for demonstration purpose, such that this repository
    //also works without a UnitOfWork.  

    public Repository(DbContext dbContext)
    {
        _dbContext = dbContext;
    }

    //exposes IQueryable interface to outside world (with a public access modifier)
    //is not a good idea! 
    protected IQueryable<T> AsQueryable()
    {
        return _dbContext.Set<T>().AsNoTracking();
    }

    public virtual T GetById(int id)
    {
        return _dbContext.Set<T>().Find(id);
    }

    public virtual IEnumerable<T> Get()
    {
        return _dbContext.Set<T>().AsEnumerable();
    }

    public virtual IEnumerable<T> Get(Expression<Func<T, bool>> predicate)
    {
        return _dbContext.Set<T>()
            .Where(predicate)
            .AsEnumerable();
    }

    public void Insert(T entity)
    {
        _dbContext.Set<T>().Add(entity);
    }

    public void Update(T entity)
    {
        _dbContext.Entry(entity).State = EntityState.Modified;
    }

    public void Delete(T entity)
    {
        _dbContext.Set<T>().Remove(entity);
    }
}

Specific Repository Implementation

    public interface IAuthorRepository : IRepository<Author>
    {
        Author GetByIdWithOwner(int id);
    }

    public class AuthorRepository : Repository<Author>, IAuthorRepository
    {
        public AuthorRepository(BlogContext dbContext) : base(dbContext)
        {
        }

        public Author GetByIdWithOwner(int id)
        {
            return AsQueryable().Include(x => x.Owner)
                .FirstOrDefault(x => x.Id == id);
        }
    }

Usage of Repository in Controller

 

        [HttpGet(Name = nameof(GetAuthor))]
        public ActionResult<IEnumerable<AuthorVM>> GetAuthor()
        {
//            var authorDtos = _db.Authors
//                .Include(x => x.Owner)
//                .Select(x => AuthorVM.From(x)).ToList();

            var authors = _authorRepository.Get();
            var authorDtos = _mapper.Map<IEnumerable<AuthorVM>>(authors);
//                .Select(x => AuthorDto.From(x));
            
            return Ok(authorDtos);
        }
        
        [HttpGet("{id}")]
        public ActionResult<AuthorVM> GetAuthors(int id)
        {
//            var author = _db.Authors
//                .Include(x => x.Owner)
//                .SingleOrDefault(x => x.Id == id);

            var author = _authorRepository.GetByIdWithOwner(id);
            
            if (author == null)
            {
                return NotFound();
            }

            return Ok(AuthorVM.From(author));
        }

Mocking Repository to test controller

[Fact]
public void Get_ReturnsAuthorVm_VMModelHasCorrectAuthorValues()
{
    // Arrange
    var mockRepo = new Mock<IAuthorRepository>();
    var author = new Author()
    {
        Id = 1, 
        BirthDate = new DateTime(2000, 1, 1),
        Email = "test@test.com",
        FirstName = "test1 firstname",
        LastName = "test1 lastname",
        Owner = new IdentityUser("AdminTest")
    };
    mockRepo.Setup(repo => repo.GetByIdWithOwner(1))
        .Returns(author);
    
    var controller = new SimpleControllerForTest(mockRepo.Object);

    // Act
    var result = controller.Get(1);

    var okObjectResult = Assert.IsAssignableFrom<OkObjectResult>(result.Result);
    var authorVm = Assert.IsAssignableFrom<AuthorVM>(okObjectResult.Value); 

    // Assert
    Assert.Equal(author.Id, authorVm.Id);
    Assert.Equal(author.FirstName + " " +author.LastName, authorVm.Name);
    Assert.Equal(author.Owner.UserName, authorVm.OwnerName);
}

Repository Result

  • Complexity reduces because of abstractions
    • more classes, interfaces
  • Benefits
    • keeps business logic free from data access code
    • central class for data access
    • maintainability, flexibility
    • not tied to a data access implementation 
    • testability (mocks)
  • Related Design Patterns
    • Unit of Work (atomic work over multiple Repo's) 
    • Decorating a Repository (caching, logging, etc)
    • Mappers (to and from View Models Models)
    • The repository can be used in services (complex business logic)

Unit of Work

  • Unit of Work = track changes to be made
    • efficiency
    • transaction management (atomic operation)
      • commit or rollback
  • Complex interactions with all or nothing behavior with multiple repositories
  • Complex interaction = update, delete, insert
  • all or nothing = transaction (concurrency issues)
    • For example
      • Subtract the amount x from account 1
      • Add the amount x to account 2
  • Saves database roundtrips
    • when doing multiple CRUD operations
      • updates/inserts/delete
 

Unit of Work

Unit of Work

  • An example without Unit of Work
    • A lot of database interaction, for each Delete(...)
  • What happens when code crashes?
    • Author not Blacklisted, some message "f**k" deleted others not

 

 

 
        [HttpGet("{id}")]
        public ActionResult<bool> BlackListAuthorWithoutUnitOfWork(int id)
        {
            var author = _authorRepository.GetById(id);

            bool blackList = false;
            foreach (var message in _messageRepository.GetMessageForAuthor(author))
            {
                if (message.Description.Contains("f**k"))
                {
                    _messageRepository.Delete(message);
                    //suppose something goes wrong with deleting or program crashes (Exception)!
                    //some messages are delete and others not
                    blackList = true;
                }
            }

            if (blackList)
            {
                _blackListRepository.AddAuthorToBlacklist(author);
            }

            return Ok(blackList);
        }

Unit of Work

  • An example with Unit of Work
    • all database calls are sent once

 

 

 
        [HttpGet("{id}")]
        public ActionResult<bool> BlackListAuthorWithUnitOfWork(int id)
        {
            bool blackList = false;

            var author = _authorRepository.GetById(id);
            
            foreach (var message in _messageRepository.GetMessageForAuthor(author))
            {
                if (message.Description.Contains("f**k"))
                {
                    _messageRepository.Delete(message);
                    //suppose something goes wrong with deleting or program crashes (Exception)!
                    //some message are delete and others not
                    blackList = true;
                }
            }

            if (blackList)
            {
             	_blackListRepository.AddAuthorToBlacklist(author);
                _db.SaveChanges(); //Commit() the Unit Of Work
            }

            return Ok(blackList); 
        }

Unit of Work

  • Implementation
    • ​In Entity Framework Core 
      • The DbContext is a Unit Of Work!!!
    • Unit Of Work for free by Dependency Injection container
      • Why are there so many crappy implementations on the internet?
 

Unit of Work

public void ConfigureServices(IServiceCollection services)
{
    services.AddDbContext<BlogContext>(options => 
        options.UseSqlite("Data Source=Blog.db"));
    
    services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);

    services.AddIdentity<IdentityUser, IdentityRole>()
        .AddEntityFrameworkStores<BlogContext>();

    services.AddScoped<IAuthorRepository, AuthorRepository>();
    services.AddScoped<IMessageRepository, MessageRepository>();

    services.AddTransient<IBlacklistService, BlacklistService>();
  • Unit Of Work == BlogContext == DbContext
  • AddDbContext registers BlogContext as Scoped
    • Lifetype is Scoped!
    • The same BlogContext instance is injected into the Repo's by dependency injection

Unit of Work

  • For each HTTP Request
    • For each HTTP Request Create a Unit of Work
      • AddDbContext<T> (lifetime scoped)

 

  • Atomicity & Saves database calls!
    • SaveChanges()
    • Logical transactions
    • Tracks changes

 

  • Related to Repository Design Pattern
    • they work together

Design Patterns for Webdev

By Joris Lops

Design Patterns for Webdev

  • 567