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. И напоследок: посмотреть, как работает данный проект можно по этому адресу. А вот тут можно скачать весь проект. Ну и до кучи — ссылка на мой блог.

ссылка на оригинал статьи https://habr.com/ru/post/484582/

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *