ASP.NET MVC — Entity Framework, MySQL и использование Dependency Resolver для выбора репозитория

    Legacy технологии
    Предупреждение: ASP.NET MVC уже устарел. Рекомендуется использовать ASP.NET Core. Но если вам интересно, то читайте.

    Решил немного расширить предыдущую статью про ASP.NET MVC и MySQL. В ней речь шла про работу с MySQL в ASP.NET MVC не через практически стандартный ORM Entity Framework (EF), а с помощью прямого доступа к СУБД через ADO.NET. И была приведена реализация этого метода доступа. И хотя метод устаревший и не рекомендуемый к использованию, но иногда полезен: например, в высоконагруженных приложениях или когда разработчик сталкивается с ситуацией, когда ORM не может сгенерировать корректно работающий SQL-запрос. И иногда можно совмещать в приложении оба способа — и через ORM и через ADO.NET. В итоге я подумал, и решил дописать приложение: добавив в него реализацию репозитория для Entity Framework и сделать выбор из них зависимым от параметра приложения с помощью Dependency Resolver.

    Весь код можно взять вот по этому адресу, ниже этот код будет частично презентован с небольшими ссылками и пояснениями по отношению к предыдущему проекту. А здесь можно посмотреть на работу приложения.

    Изменяем проект


    1. Для использования Entity Framework с MySQL мы должны установить библиотеку MySQL.Data.EntityFramework (можно, конечно, и другую, просто эта от Oracle — владельца MySQL).


    Она потянет за собой MySQL.Data и собственно EntityFramework. В файл web.config внесены изменения:

    <entityFramework>
      <providers>
          <provider invariantName="MySql.Data.MySqlClient" type="MySql.Data.MySqlClient.MySqlProviderServices, MySql.Data.EntityFramework, Version=8.0.19.0, Culture=neutral, PublicKeyToken=c5687fc88969c44d" />
      </providers>
    </entityFramework>
    

    С MySQL.Data возникла интересная коллизия — поскольку MySQL.Data.EntityFramework потребовал версии MySQL.Data не ниже 8.0.19, то он обновился… и проект перестал работать. Стала возникать ошибка:



    Не удалось загрузить файл или сборку «Ubiety.Dns.Core» либо одну из их зависимостей. Невозможно проверить подпись строгого имени. Возможно, сборка была изменена или построена с отложенной подписью, но не полностью подписана правильным закрытым ключом. (Исключение из HRESULT: 0x80131045)

    Видимо в версию MySQL.Data 8.0.19 была добавлена не подписанная сборка Ubiety.Dns.Core. Пришлось в проект ещё и включить этот компонент через Nuget. Ошибка пропала.

    2. Кроме этого для реализации внедрения зависимостей добавим в проект Ninject — контейнер внедрения зависимостей (DI).

    3. Немного изменим структуру проекта: файлы репозитория вынесем в отдельный каталог Repository и создадим в нём еще подкаталоги ADO.NET (перенесём туда имеющиеся файлы LanguagesRepository.cs и UsersRepository.cs) и EF (тут будут файлы репозитория для Entity Framework).

    4. Кроме этого в файл web.config в раздел appConfig добавлен параметр приложения: <add key="ConnectionMethod" value="ADO.NET" />. Приложение будет принимать два значения: «Entity Framework» или «ADO.NET». В файл Base.cs добавил ссылку на этот параметр:

    public static string ConnectionMethod
    {
        get
        {
            return System.Configuration.ConfigurationManager.AppSettings["ConnectionMethod"];
        }
    }

    Entity Framework и MySQL – репозитарий


    Добавим в каталог Repository\EF файл DbContext.cs с классом EFDbContext:

    public class EFDbContext : DbContext {
        public EFDbContext() : base(Base.ConnectionString)
        { }
        public DbSet<UserClass> Users { get; set; }
        public DbSet<LanguageClass> Languages { get; set; }
    }

    В нём мы определяем используемую строку подключения к СУБД и наборы данных Users и Languages.

    Добавляем файл LanguagesRepository.cs с классом LanguagesRepositoryEF:

    public class LanguagesRepositoryEF : ILanguagesRepository
    {
        private EFDbContext context = new EFDbContext();
     
        public IList<LanguageClass> List()
        {
            return context.Languages.OrderBy(x => x.LanguageName).ToList();
        }
    }

    И файл UsersRepository.cs с классом UsersRepositoryEF:

    public class UsersRepositoryEF : IUsersRepository
    {
        private EFDbContext context = new EFDbContext();
     
        public IList<UserClass> List()
        { 
            return context.Users.ToList();
        }
     
        public IList<UserClass> List(string sortName, SortDirection sortDir, int page, int pageSize, out int count)
        {
            count = context.Users.Count();
            if (sortName != null) return context.Users.OrderByDynamic(sortName, sortDir).Skip((page - 1) * pageSize).Take(pageSize).ToList();
            else return context.Users.OrderBy(o => o.UserID).Skip((page - 1) * pageSize).Take(pageSize).ToList();
        }
     
        public bool AddUser(UserClass user)
        {
            user.Language = context.Languages.Find(user.Language.LanguageID);
            if (user.Language != null && context.Users.Add(user) != null)
            {
                try { context.SaveChanges(); }
                catch (System.Exception ex) {}
            }
            return user.UserID > 0;
        }
     
        public UserClass FetchByID(int userID)
        {
            UserClass user = null;
            try { user = context.Users.Find(userID); }
            catch (System.Exception ex) { }
            return user;
        }
     
        public bool ChangeUser(UserClass user)
        {
            bool result = false;
            user.Language = context.Languages.Find(user.Language.LanguageID);
            if (user.Language != null)
            {
                UserClass olduser = context.Users.Find(user.UserID);
                if (olduser != null)
                {
                    olduser.Email = user.Email;
                    olduser.Loginname = user.Loginname;
                    olduser.Language = user.Language;
                    olduser.SupporterTier = user.SupporterTier;
                    try { result = context.SaveChanges() > 0; }
                    catch (System.Exception ex) { }
                }
            }
            return result;
        }
     
        public bool RemoveUser(UserClass user)
        {
            bool result = false;
            UserClass olduser = context.Users.Find(user.UserID);
            if (olduser != null) context.Users.Remove(olduser);
            try { result = context.SaveChanges() > 0; }
            catch (System.Exception ex) { }
            return result;
        }
    }

    Видно, что размер файла явно короче подобного для ADO.NET — ORM делает за нас «грязную» работу — создает SQL-запросы самостоятельно.

    Однако, я столкнулся с парой моментов, которые прокатывали в реализации ADO.NET, но не работают в EF.

    Первый, что пришлось внести изменение в файл UserClass.cs (в каталоге Domain): добавить еще одно поле для нормальной работы связи таблиц Users и Languages:

    [HiddenInput(DisplayValue = false)]
    public int? LanguageID { get; set; }
    

    И второй — оказалось что поля в MySQL типа Enum не работают через EF. Скорее всего причина этого в том, что перечисление в коде является целочисленным значением, а вот из БД значения через EF читаются как текст (если в запросе из MySQL читать значения поля типа enum MySQL возвращает как раз текстовые значения этого перечисления). И если в версии для ADO.NET я могу это обойти с помощью конструкции CAST(u.SupporterTier AS UNSIGNED) as SupporterTier, то с EF такая метаморфоза оказалась для меня непреодолимой — ни один из пробуемых вариантов не подошёл. Ну и поскольку технология Code First поле типа Enum генерирует в виде поля типа INT, то пришлось в БД поменять тип поля SupporterTier:

    CHANGE COLUMN `SupporterTier` `SupporterTier` INT(4) UNSIGNED NOT NULL DEFAULT '1' ;

    Выбор репозитория с помощью параметра приложения


    Воспользуемся внедрением через конструктор, прямо как написано в учебнике. Во-первых, нам надо создать интерфейсы для нашего общего репозитария: создаем файл LanguagesRepository.cs в каталоге Repository с содержимым:

    public interface ILanguagesRepository
    {
        IList<LanguageClass> List();
    }
    

    И файл UsersRepository.cs с содержимым:

    public interface IUsersRepository
    {
        IList<UserClass> List();
     
        IList<UserClass> List(string sortName, SortDirection sortDir, int page, int pageSize, out int count);
     
        bool AddUser(UserClass user);
     
        UserClass FetchByID(int userID);
     
        bool ChangeUser(UserClass user);
     
        bool RemoveUser(UserClass user);
    }
    

    Ну и наследуем соответствующие классы от этих интерфейсов:

    public class LanguagesRepositoryADO : ILanguagesRepository
    public class UsersRepositoryADO : IUsersRepository
    public class LanguagesRepositoryEF : ILanguagesRepository
    public class UsersRepositoryEF : IUsersRepository
    

    Ну и в контроллер UsersController вносим добавления, которые позволят ему работать с этими интерфейсами:

    private ILanguagesRepository repLanguages;
    private IUsersRepository repUsers;
     
    public UsersController(ILanguagesRepository langsParam, IUsersRepository usersParam) 
    { 
        repLanguages = langsParam;
        repUsers = usersParam;
    }
    

    И в контроллере изменяем места обращения к объектам этих классов на объекты repLanguages и repUsers, соответственно. Но нам потребуется передавать экземпляры классов репозиториев через конструктор контроллера, что, конечно, неудобно. Чтобы этого избежать, нам нужно сильное колдунство типа Dependency Resolver (DR). И для этого мы будем использовать Ninject:

    Регистрируем DR в файле Global.asax.cs в методе Application_Start:

    DependencyResolver.SetResolver(new NinjectDependencyResolver());
    

    Создадим файл NinjectDependencyResolver.cs в каталоге Infrastructure с классом NinjectDependencyResolver (унаследовавшего от интерфейса IDependencyResolver):

    public class NinjectDependencyResolver : IDependencyResolver 
        { 
            private IKernel kernel; 
            public NinjectDependencyResolver() 
            { 
                kernel = new StandardKernel(); 
                AddBindings(); 
            } 
            public object GetService(Type serviceType) 
            { 
                return kernel.TryGet(serviceType); 
            } 
            public IEnumerable<object> GetServices(Type serviceType) 
            { 
                return kernel.GetAll(serviceType); 
            } 
            private void AddBindings() 
            { 
                if (Domain.Base.ConnectionMethod == "Entity Framework")
                {
                    kernel.Bind<ILanguagesRepository>().To<LanguagesRepositoryEF>();
                    kernel.Bind<IUsersRepository>().To<UsersRepositoryEF>();
                }
                else
                {
                    kernel.Bind<ILanguagesRepository>().To<LanguagesRepositoryADO>();
                    kernel.Bind<IUsersRepository>().To<UsersRepositoryADO>();
                }
            }
        }
    

    И получается, что единственное место, в котором определяеся какой метод работы с СУБД используется (напрямую, через ADO.NET или через Entity Framework) это метод AddBindings в классе NinjectDependencyResolver. Настоящая магия, если не знать как это работает.

    В методе AddBindings в зависимости от значения параметра приложения «ConnectionMethod» происходит связка интерфейсов ILanguagesRepository и IUsersRepository с конкретными классами реализующими методы интерфейсов. Поскольку при старте приложения мы зарегистрировали DR как объект класса NinjectDependencyResolver, а в классе мы указали привязку интерфейсов репозиториев к конкретному классу, то при запросе фреймворка MVC на создание объекта контроллера UsersController, Ninject при анализе класса обнаружит, что он требует реализацию интерфейсов ILanguagesRepository и IUsersRepository и создаст экземпляры конкретных классов и передаст их в конструктор контроллера (через DR и фрейворк MVC).

    Итого


    Приложение теперь поддерживает и метод доступа к СУБД через ORM Entity Framework. При этом метод доступа через ADO.NET никуда не делся и выбирается при запуске приложения по параметру, для чего было использован метод внедрения зависимости через конструктор контроллера с помощью библиотеки Ninject.



    P.S. И напоследок: посмотреть, как работает данный проект можно по этому адресу. А вот тут можно скачать весь проект. Ну и до кучи — ссылка на мой блог.
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама

    Комментарии 9

      0

      А какое у вас время жизни репозиториев? Singletone или на запрос?
      Если Singletone, то в текущем варианте будут проблемы с многопоточным доступом, так как контекст будет шариться между запросами.
      Если время жизни на запрос, то произойдёт утечка коннекций, потому что нет dispose контекстов

        +1
        Ну, поскольку объекты репозиториев создаются в момент генерации объекта контроллера, то они живы пока контроллер отрабатывает обращение к странице, потом вместе с ним и убиваются. А соединения с СУБД в ASP.NET по идее не убиваются сразу, а хранятся в некоем пуле соединений и по необходимости оттуда и берутся. Т.е. их не должно быть сильно больше чем максимальное количество одновременно работающих пользователей.
          0

          Всегда диспозил контекст. Решил немного погуглить и набрёл на статью, в которой объяснено, почему диспозить не обязательно, но всё же это можно считать хорошим паттерном. https://blog.jongallant.com/2012/10/do-i-have-to-call-dispose-on-dbcontext/

        0

        Удалено

          0
          Почему бы не оставить, уже давно оставленный asp.net и понять, что это устарело уже давно. Есть .net core, где это все под капотом есть.
            0
            Ну вот руки дойдут, перепишу данные пример под .NET Core, можно будет сделать сравнение с тем, что было и тем как стало.
              +1
              Уже давно все сравнено и написано. .net core уже давно в продакшене трудится. Додо пицца и 2гис используют его. Вы до сих пор пишете статьи о продукте 10 давности. Бывают ещё вопросы, что учить и ни одного ответа в защиту старого asp.net не увидел.
                +1
                Да я уже понял из обсуждений предыдущей статьи. Переползу и на .NET Core, пока очередная вундервафля и её не заменит. Не надо только разжигать холивар — какая технология лучше. Почему среди ИТ-шников так много хейтеров и холиварщиков? За прошлую статью мне вкорячили 6 минусов в рейтинг и 2 в карму и спасибо добрым людям, которые вернули ситуацию к тому что было в начале. Ладно бы я там накосячил и сделал всё плохо и статья была бы написана отвратно. Так нет же — только за использование устаревших технологий. Тут тоже специально пометил в спойлере, что лучше использовать .NET Core, хорошо хоть минусов не нахватал пока.
                Я работаю в компании, в которой до сих пор в ходу технологии 10-20 летней давности, а мне будут рассказывать про «устаревшие» технологии. У нас до сих пор большинство ERP-систем на SAP R/3 версии 4.0 работают.
            0
            1. try catch {} плохая практика
            2. Сейчас принято писать асинхронные методы, да и хорошая практика для сервера

            Ваша абстракция понятна, только, я не помню в своем случае когда коня/ORM меняли на переправе. Будут сидеть на EF до конца. Учитывая что абстракции резко ухудшают производительность как самой программы так и программистов, я бы задумался нужны ли они в таком виде. DbContext уже сам по себе неслабый репозиторий. Пряча от разработчиков LINQ, вы просто усложняете себе жизнь. IMHO, абстракции лучше строить на уровне бизнес задач/сервисов и в крайнем случае над отдельными сущностями.

            Простой пример, что вы сделаете когда надо достать Id первых 100 пользователей, у которых установлен язык английский? Что вам дадут два репозитория которые возвращают целые таблицы в память, а вам нужно достать всего сотню целых чисел из данных связанных двумя таблицами?
            Тут приходит на помощь LINQ, репозитории могут возвращать IQueryable. Только зачем эти репозитории тогда, если они просто враппер над DbContext? Я, например, не вижу в них смысла, это ухудшает читабельность, нагружает контроллер дополнительными зависимостями. Если контроллер большой, в него ижектят с 20-к репозиториев, несмотря на то что в основном нужны только несколько на один запрос.

            Слегка сумбурно, но это я сейчас наблюдаю во всех ASP.NET проектах в которых просят помочь разобраться с производительностью. Написано по патернам, которые я бы уже и сам назвал антипатернами.

            Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

            Самое читаемое