W tym poście chcę Ci pokazać jak prawidłowo zaimplementować jeden z popularniejszych wzorców projektowych zwany Repository Pattern. Pomaga on bardzo w oddzieleniu warstwy odpowiedzialnej za przechowywanie danych w naszej aplikacji – i oddzieleniu logiki z tym związanej właśnie tam. Repozytoria są też częścią najpopularniejszej architektury aplikacji, czyli Domain Driven Design – więc poznanie go bardzo Ci się przyda. Po zastosowaniu tego wzorca w praktyce, kod staje się dużo czytelniejszy i komunikacja z bazą danych również staje się bardzo prosta i łatwa. Brzmi jak coś co chciałbyś poznać? Zaczynajmy!

Na sam początek wyjaśnimy sobie krótko trzy pojęcia z których będe korzystać. Będziemy się tu opierać na bazach danych relacyjnych – konkretny silnik jest obojętny, ważne jest to, żeby baza miała tabelki. Czyli mamy bazę danych, w której są tabelki, a w każda tabela przechowuje dany obiekt w postaci wierszy. Klasa i jej propertisy z C# jest zamieniana na pojedyńczy wiersz w tabeli (ten efekt zapewnia nam Entity Framework). Dla łatwiejszego zrozumienia, przyjmijmy sobie następujący scenariusz – mamy prostą bazę danych z dwoma tabelkami – użytkownicy i produkty. Użytkownicy mają imię i nazwisko, a produkty mają nazwę i cenę. Z takim obrazem bazy, przejdzmy do wytłumaczenia pojęć.

Entity – jest to właśnie ta klasa, która jest pojedyńczym wierszem w danej tabeli – po polsku można to nazywać “encją”, ale ja pozostaje przy angielskich nazwach. W naszej przykładowej bazie, mielibyśmy dwie entities – jedna klasa o nazwie User z dwoma propertisami – FirstName oraz LastName, oraz druga klasa o nazwie Product również z dwoma propertisami – Name oraz Price. Dodatkowo, do obu entities moglibyśmy jeszcze dodać trzeci propertis o nazwie ID, który byłby indentyfikatorem

Repository – jest to klasa, taki helper, który jest odpowiedzialny za interakcje z JEDNĄ konkretną tabelką – w naszej przykładowej bazie byłoby osobne repozytorium do tabelki użytkownicy oraz osobne repetytorium do tabelki produkty, i w każdym z nich byłyby operacje związane z przypisaną do nich tabelką. Nazwa repozytorium to zawsze nazwa entity + “Repository”, przykladowo UserRepository.

DbContext – klasa która reprezentuje naszą bazę danych w kodzie – tam się dzieje cała magia Entity Framework. Tam deklarujemy nasze tabelki oraz wszystko co z bazą danych związane. Tabelki w bazie danych są natomiast nazywane DbSet.

Pojęcia już znamy, więc przejdzmy do implementacji tego wszystkiego w praktyce – pokażę Ci przykładowy kod wraz z wszystkimi klasami, napisanymi generycznie, więc będziesz je mógł użyć od zaraz w swojej dowolnej aplikacji.

Zacznijmy od podstaw, czyli od klasy DbContext. Setup Entity Frameworka celowo pomijam, jeśli potrzebujesz przy tym pomocy to napisałem osobny post o tym jak prawidłowo go skonfigurować i wdrożyć do dowolnej aplikacji. Mając dodany Entity Framework do projektu, będziemy mieć pustą klasę DbContext. Dodajmy do niej dwie tabelki, będziemy się trzymać wcześniejszego przykładu, czyli użytkownicy z produktami. Robimy to w taki sposób:

using Microsoft.EntityFrameworkCore;

namespace ProtonSoftwareEntityFrameworkCore.Database
{
    public class ApplicationDbContext : DbContext
    {
        public DbSet<User> Users { get; set; }
        public DbSet<Product> Products { get; set; }
    }
}

I tym sposobem mamy bazę danych z dwiema tabelkami. Tylko klasy User oraz Product jeszcze nie istnieją – więc je stwórzmy. Będą one jako entities, więc polecam stworzyć folder o tej nazwie i tam trzymać wszystkie klasy tego typu. Entity User będzie wyglądać w naszym przypadku tak:

namespace ProtonSoftwareEntityFrameworkCore.Database
{
    public class User
    {
        public int Id { get; set; }

        public string FirstName { get; set; }

        public string LastName { get; set; }
    }
}

A entity Product będzie wyglądać tak:

namespace ProtonSoftwareEntityFrameworkCore.Database
{
    public class Product
    {
        public int Id { get; set; }

        public string Name { get; set; }

        public decimal Price { get; set; }
    }
}

I gotowe – tak naprawdę można by już przejść dalej – tylko tak jak obiecałem, chcę żeby ten poradnik był pomocny dla Ciebie w każdej sytuacji i w każdej aplikacji, i aby przedstawione klasy były gotowe do użycia – zrobimy więc tutaj małe usprawnienie. Jak widać w każdej entity powtarza się ID – musimy je przecież jakoś identyfikować, zresztą na ID (które jest kluczem podstawowym) opierają się bazy relacyjne. Możemy więc to id wyodrębnić do osobnej klasy – przykładowo BaseEntity – i dzięki temu będziemy też wiedzieć czy dana klasa jest zwykłym obiektem czy może bazo danowym entity – wystarczy zobaczyć czy dziedziczy po BaseEntity. Jedna uwaga – zauważ, że ID dałem jako int – ale nie zawsze musi tak być – czasem można identyfikować po czymś innym, np. GUID’zie, albo dowolnym stringu. Dlatego nasze ID zrobimy jako generyczne i przy każdej entity będziemy podawać typ ID. Daje nam to taką klasę BaseEntity:

namespace ProtonSoftwareEntityFrameworkCore.Database
{
    public abstract class BaseEntity<TypeForId>
    {
        public TypeForId Id { get; set; }
    }
}

A nasze entities finalnie wyglądają tak:

public class User : BaseEntity<int>
{
    public string FirstName { get; set; }

    public string LastName { get; set; }
}
public class Product : BaseEntity<int>
{
    public string Name { get; set; }

    public decimal Price { get; set; }
}

Bazę mamy, entities mamy, pora na tytułowe repozytoria. Zacznijmy od użytkowników – chcielibyśmy dodawać nowych, usuwać, zmieniać, wyświetlać, dosłownie wszystko co się da zrobić z danymi. Stwórzmy sobie folder Repositories i dodajmy tam nową klasę UserRepository. Tylko żeby cokolwiek działać z bazą danych, trzeba mieć do niej dostęp, więc ta klasa potrzebuje mieć DbContext w sobie żeby móc go używać. Przejmujemy go w kontruktorze, ponieważ tam go automatycznie dostaniemy dzięki Dependency Injection. DbContext jest natomiast całą bazą – a jak wiemy, repository powinien mieć dostęp tylko do jednej tabelki – a jest nią pojedyńczy DbSet który ma w sobie userów. Przygotujmy więc sobie taki layout klasy UserRepository:

using Microsoft.EntityFrameworkCore;

namespace ProtonSoftwareEntityFrameworkCore.Database
{
    public class UserRepository
    {
        private ApplicationDbContext Database;

        private DbSet<User> DbSet => Database.Users;

        public UserRepository(ApplicationDbContext dbContext)
        {
            Database = dbContext;
        }
    }
}

A teraz dodajmy sobie trochę podstawowych metod, żeby mieć jakiś użytek z tego repository:

public void Add(User entity) => DbSet.Add(entity);

public void Update(User entity) => DbSet.Update(entity);

public void Delete(User entity) => DbSet.Remove(entity);

public User GetById(int id) => DbSet.Find(id);

public IQueryable<User> GetAll() => DbSet;

Mogłoby się wydawać, że to tyle, ale jednak zmiany w bazie wykonane w taki sposób nie zostały by zapisane – przykładowo DbSet.Add(user) nie wystarczy by dodać nowego użytkownika. DbContext posiada metodę zwaną SaveChanges() która wysyła wszystkie zmiany do faktycznej bazy danych i to właśnie po jej wykonaniu możemy być pewni, że przykładowo user zostanie dodany poprawnie do bazy. Rzuca też ona sporo exceptionów w razie nieprawidlowości w nowych danych, które próbujemy zapisać. Dodajmy ją sobie do naszych metod – ja zrobie ją jako dodatkową opcję, ponieważ czasem chcemy np. dodać dwóch userów i dopiero potem zapisać, dzięki temu leci tylko jeden request do bazy danych zamiast dwóch. Daje nam to taki efekt:

public bool Add(User entity, bool saveChanges = true) 
{
    try
    {
        DbSet.Add(entity);
        if (saveChanges)
        {
            return Database.SaveChanges() > 0;
        }
    }
    catch (Exception ex)
    {
        return false;
    }

    return false;
} 

public bool Update(User entity, bool saveChanges = true)
{
    try
    {
        DbSet.Update(entity);
        if (saveChanges)
        {
            return Database.SaveChanges() > 0;
        }
    }
    catch (Exception ex)
    {
        return false;
    }

    return false;
} 

public bool Delete(User entity, bool saveChanges = true)
{
    try
    {
        DbSet.Remove(entity);
        if (saveChanges)
        {
            return Database.SaveChanges() > 0;
        }
    }
    catch (Exception ex)
    {
        return false;
    }

    return false;
} 

public User GetById(int id) => DbSet.Find(id);

public IQueryable<User> GetAll() => DbSet;

I gotowe. To teraz, pora na ProductRepository – chcemy zrobić to samo. Copy paste? No jasne, że nie :). Jak widać, praktycznie cały kod będzie identyczny dla produktów, więc to idealny czas na wejście do gry BaseRepository. Dzięki temu też usuniemy dostęp repository do całej bazy, ponieważ zrobimy ją jako private w BaseRepository, a repository które po nim dziedziczą będą mieć tylko dostęp do protected DbSet’u. Oprócz tego, tak jak wspominałem chcę, żeby ten kod był możliwy do wykorzystywania w każdym przypadku, zrobimy sobie jeszcze te wszystkie metody w wersji asynchronicznej, bo często się też takie wykorzystuje. Finalnie, nasz BaseRepository będzie wyglądać tak:

using Microsoft.EntityFrameworkCore;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace ProtonSoftwareEntityFrameworkCore.Database
{
    public abstract class BaseRepository<TypeOfEntity, TypeOfId> 
        where TypeOfEntity : BaseEntity<TypeOfId>
    {
        private ApplicationDbContext Database;

        protected abstract DbSet<TypeOfEntity> DbSet { get; }

        public BaseRepository(ApplicationDbContext dbContext)
        {
            Database = dbContext;
        }

        public bool Add(TypeOfEntity entity, bool saveChanges = true)
        {
            try
            {
                DbSet.Add(entity);
                if (saveChanges)
                {
                    return Database.SaveChanges() > 0;
                }
            }
            catch (Exception ex)
            {
                return false;
            }

            return false;
        }

        public async Task<bool> AddAsync(TypeOfEntity entity, bool saveChanges = true)
        {
            try
            {
                DbSet.Add(entity);
                if (saveChanges)
                {
                    return await Database.SaveChangesAsync() > 0;
                }
            }
            catch (Exception ex)
            {
                return false;
            }

            return false;
        }

        public bool Update(TypeOfEntity entity, bool saveChanges = true)
        {
            try
            {
                DbSet.Update(entity);
                if (saveChanges)
                {
                    return Database.SaveChanges() > 0;
                }
            }
            catch (Exception ex)
            {
                return false;
            }

            return false;
        }

        public async Task<bool> UpdateAsync(TypeOfEntity entity, bool saveChanges = true)
        {
            try
            {
                DbSet.Update(entity);
                if (saveChanges)
                {
                    return await Database.SaveChangesAsync() > 0;
                }
            }
            catch (Exception ex)
            {
                return false;
            }

            return false;
        }

        public bool Delete(TypeOfEntity entity, bool saveChanges = true)
        {
            try
            {
                DbSet.Remove(entity);
                if (saveChanges)
                {
                    return Database.SaveChanges() > 0;
                }
            }
            catch (Exception ex)
            {
                return false;
            }

            return false;
        }

        public async Task<bool> DeleteAsync(TypeOfEntity entity, bool saveChanges = true)
        {
            try
            {
                DbSet.Remove(entity);
                if (saveChanges)
                {
                    return await Database.SaveChangesAsync() > 0;
                }
            }
            catch (Exception ex)
            {
                return false;
            }

            return false;
        }

        public TypeOfEntity GetById(TypeOfId id) => DbSet.Find(id);
        public async Task<TypeOfEntity> GetByIdAsync(TypeOfId id) => await DbSet.FindAsync(id);

        public List<TypeOfEntity> GetAll() => DbSet.ToList();
        public async Task<List<TypeOfEntity>> GetAllAsync() => await DbSet.ToListAsync();
    }
}

A nasze repository dla User oraz Product będzie dzięki temu wygladać tak:

public class UserRepository : BaseRepository<User, int>
{
    protected override DbSet<User> DbSet => Database.Users;

    public UserRepository(ApplicationDbContext dbContext) : base(dbContext) { }
}

public class ProductRepository : BaseRepository<Product, int>
{
    protected override DbSet<Product> DbSet => Database.Products;

    public ProductRepository(ApplicationDbContext dbContext) : base(dbContext) { }
}

W ten sposób, jeśli chcemy wykonywać na danej tabelce podstawowe operacje typu add/update/delete/get, mamy to wszystko dostarczone przez BaseRepository, a jeśli chcemy coś custom’owego, to piszemy odpowiednią funkcję w konkretnym repository. Cudowne w swojej prostocie.

Na koniec, jeszcze wydzielimy sobie interfejs do repository, ponieważ w taki sposób się je najczęściej wykorzystuje z Dependency Injection. Również tutaj, zrobimy sobie bazowy interfejs, oraz konkretne interfejsy do konkretnych repository. Interfejs zawiera tylko spis dostępnych metod, więc nasz IBaseRepository będzie wyglądać tak:

using System.Collections.Generic;
using System.Threading.Tasks;

namespace ProtonSoftwareEntityFrameworkCore.Database
{
    public interface IBaseRepository<TypeOfEntity, TypeOfId> where TypeOfEntity : BaseEntity<TypeOfId>
    {
        bool Add(TypeOfEntity entity, bool saveChanges = true);
        Task<bool> AddAsync(TypeOfEntity entity, bool saveChanges = true);
        bool Update(TypeOfEntity entity, bool saveChanges = true);
        Task<bool> UpdateAsync(TypeOfEntity entity, bool saveChanges = true);
        bool Delete(TypeOfEntity entity, bool saveChanges = true);
        Task<bool> DeleteAsync(TypeOfEntity entity, bool saveChanges = true);
        TypeOfEntity GetById(TypeOfId id);
        Task<TypeOfEntity> GetByIdAsync(TypeOfId id);
        List<TypeOfEntity> GetAll();
        Task<List<TypeOfEntity>> GetAllAsync();
    }
}

A nasze IUserRepository i IProductRepository tak:

public interface IUserRepository : IBaseRepository<User, int>
{
}

public interface IProductRepository : IBaseRepository<Product, int>
{
}

Oczywiście UserRepository i ProductRepository powinny implementować te interfejsy.

I to na tyle, przekazałem wszystko co potrzebne, by prawidłowo używać Repository Pattern w dowolnej Twojej aplikacji. Kod źródłowy jak zwykle dostępny jest na GitHub. Dzięki za poświęcony czas i powodzenia w kodowaniu!


Patryk Mikulski

Programowaniem zajmuje się od 4 lat i jest to moja pasja. Jestem autorem wszystkiego co związane jest z ProtonSoftware.NET. Specjalizuję się w platformie .NET z językiem C# na czele. Pracuję zarówno na etacie, jak i zajmuję się własnymi aplikacjami. Najlepiej czuję się w roli team leadera. Preferuję pełne skupienie na praktyce i efektach pracy - teoria jest dobra tylko w teorii.

3 Comments

ExoRank.com · 24 January 2020 at 6 h 27 min

Awesome post! Keep up the great work! 🙂

AffiliateLabz · 15 February 2020 at 22 h 15 min

Great content! Super high-quality! Keep it up! 🙂

Mckenzie Petek · 14 July 2020 at 20 h 37 min

Hiya, I’m really glad I’ve found this info. Nowadays bloggers publish just about gossip and internet stuff and this is really annoying. A good website with exciting content, this is what I need. Thank you for making this web site, and I will be visiting again. Do you do newsletters by email?

Leave a Reply

Your email address will not be published. Required fields are marked *

en_GBEnglish
en_GBEnglish