Repozytorium

W aplikacji WPM będę korzystał z repozytorium, które będzie pośredniczyć pomiędzy warstwą prezentacji a warstwą dostępu do danych. Repozytorium będzie także niezależne od typu bazy danych. O wzorcu repozytorium można dokładniej poczytać m.in. tutaj.

Na początku był interfejs

Interfejs repozytorium umieściłem w projekcie WPM.DomainModel. Interfejs będzie definiował podstawowe operacje na bazie danych takie jak wstawianie i aktualizowanie danych (InsertOrUpdate),pobranie elementu wg identyfikatora (GetById), usuwanie (Delete) i filtrowanie elementów (GetAll). Dodałem także możliwość podglądu zapytań SQL wysyłanych do bazy (Log). W organizacji interfejsu pomógł mi także ten wpis. Ostatecznie interfejs ma postać:

    public interface IWPMRepository
    {
        IEnumerable<TEntity> GetAll<TEntity>(Func<TEntity, bool> query) where TEntity : class;
        void InsertOrUpdate<TEntity>(TEntity entity) where TEntity : class;
        void Delete<TEntity>(TEntity entity) where TEntity : class;
        TEntity GetById<TEntity>(int id) where TEntity : class;
        Action<string> Log { set; }
    }

Implementacja

Klasa która będzie implementowała interfejs repozytorium będzie już zależna od rodzaju bazy danych. Ja, w przypadku mojej aplikacji, będę korzystał z baz danych Microsoftu i z Entity Framework Core 1. Klasa będzie umieszczona w oddzielnym projekcie WPM.DataAccessMsSql. Na początku chciałem zrobić oddzielną klasę repozytorium i klasę kontekstu bazy, lecz gdy zacząłem grzebać w kodzie klasy DBContext na githubie, w komentarzach do tej klasy znalazłem takie zdanie:

A DbContext instance represents a session with the database and can be used to query and save instances of your entities. DbContext is a combination of the Unit Of Work and Repository patterns.

Wynika z tego, że samą klasę DBContext należy traktować jako repozytorium (w internecie było już wcześniej sporo dyskusji na ten temat). Więc aby uniknąć repozytorium w repozytorium, zaimplementowałem interfejs IWPMRepository bezpośrednio w klasie MsSqlContext:

    public class MsSqlContext : DbContext, IWPMRepository
    {
        public IEnumerable<TEntity> GetAll<TEntity>(Func<TEntity,bool> query) where TEntity : class
        {
            return this.Set<TEntity>().Where(query.ToExpression()).ToList();
        }

        public void InsertOrUpdate<TEntity>(TEntity entity) where TEntity : class
        {
            if (this.Entry(entity).State == EntityState.Detached)
            {
                this.Set<TEntity>().Add(entity);
            }
            this.SaveChanges();
        }

        public void Delete<TEntity>(TEntity entity) where TEntity : class
        {
            this.Set<TEntity>().Remove(entity);
            this.SaveChanges();
        }

        public TEntity GetById<TEntity>(int id) where TEntity : class
        {
             return Set<TEntity>().Find(id);
        }

        public Action<string> Log
        {
            set
            {
                var serviceProvider = this.GetInfrastructure<IServiceProvider>();
                var loggerFactory = serviceProvider.GetService<ILoggerFactory>();
                MsSqlLoggerProvider logger = new MsSqlLoggerProvider
                {
                    Log = value
                };
                loggerFactory.AddProvider(logger);
            }
        }

        protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        {
           var builder = new ConfigurationBuilder()
                .AddJsonFile("../../config.json")
                .AddEnvironmentVariables();
            var configuration = builder.Build();
            var sqlConnectionString = configuration["DataAccessMsSql:ConnectionString"];
            optionsBuilder.UseSqlServer(sqlConnectionString);
        }

        protected override void OnModelCreating(ModelBuilder builder)
        {
            builder.Entity<WPMCredential>();
            base.OnModelCreating(builder);
        }
    }

Krótkie wyjaśnienie

W @5 użyłem małej metody rozszerzającej, która zamienia funkcje na Expression:

        public static Expression<Func<TEntity, bool>> ToExpression<TEntity>(this Func<TEntity, bool> func)
        {
            return x => func(x);
        }

W @25 musiałem użyć własnej metody Find, ponieważ nie jest ona zaimplementowana w EF core. Kod metody rozszerzającej wziąłem z tej strony.
W liniach @28 – @40 użyłem własnego loggera do pobierania zapytań do bazy. Przykład konfigurowania loggera znalazłem na tej stronie. Ostatecznie klasa MsSqlLoggerProvider wygląda tak:

    internal class MsSqlLoggerProvider : ILoggerProvider
    {
        public ILogger CreateLogger(string categoryName)
        {
            Logger log = new Logger
            {
                LogAction = this.Log
            };
            return log;
        }

        public void Dispose()
        { }

        public Action<string> Log { private get; set;  }

        private class Logger : ILogger
        {
            public Action<string> LogAction { private get; set; }

            public bool IsEnabled(LogLevel logLevel)
            {
                return true;
            }

            public void Log(LogLevel logLevel, int eventId, object state, Exception exception, Func<object, Exception, string> formatter)
            {
                if (this.LogAction!=null)
                {
                    LogAction(formatter(state, exception));
                }
            }

            public IDisposable BeginScopeImpl(object state)
            {
                return null;
            }
        }
    }

Logi można wyrzucać na przykład do okna Output w VS:

_repository.Log = (message) => Debug.WriteLine(message);

Wracając do repozytorium – linie @42 – @50 zostały omówione w tym wpisie. Metoda OnModelCreating (@52 – @56) zawiera konfiguracje kontekstu, ale jest ona wywoływana tylko przy tworzeniu pierwszej instancji kontekstu a następnie keszowana – aby zwiększyć wydajność. Ja w tej metodzie wiążę klasy encji z projektu WPM.DomainModel z kontekstem (@54). Ponieważ trzymam encje oddzielnie od kontekstu, bez tej konfiguracji miałbym pustą bazę danych.

Słowo końcowe

Przewiduję że kontekst będę jeszcze przebudowywał parę razy, w miarę potrzeb i poznawania Entity Framework Core 1. Jak na razie migracje działają, pobieranie danych i odczyt też. Na początku zrobiłem tylko jedną encję więc nic nie mogło pójść źle – zobaczymy przy większej ilości klas. Muszę teraz trochę podziałać w warstwie prezentacji, więc następny wpis będzie o użyciu repozytorium w MVC Core (chociaż kusi mnie, po lekturze tego wpisu, zrobienie fake-wego repozytorium używając biblioteki Bogus i sprawdzenie czy rzeczywiście mogę używać dowolnego źródła danych bez większych ingerencji w program).

One thought on “Repozytorium

Dodaj komentarz

Twój adres email nie zostanie opublikowany. Pola, których wypełnienie jest wymagane, są oznaczone symbolem *