Решил немного расширить предыдущую статью про 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. И напоследок: посмотреть, как работает данный проект можно по этому адресу. А вот тут можно скачать весь проект. Ну и до кучи — ссылка на мой блог.
buldo
А какое у вас время жизни репозиториев? Singletone или на запрос?
Если Singletone, то в текущем варианте будут проблемы с многопоточным доступом, так как контекст будет шариться между запросами.
Если время жизни на запрос, то произойдёт утечка коннекций, потому что нет dispose контекстов
Duke Автор
Ну, поскольку объекты репозиториев создаются в момент генерации объекта контроллера, то они живы пока контроллер отрабатывает обращение к странице, потом вместе с ним и убиваются. А соединения с СУБД в ASP.NET по идее не убиваются сразу, а хранятся в некоем пуле соединений и по необходимости оттуда и берутся. Т.е. их не должно быть сильно больше чем максимальное количество одновременно работающих пользователей.
buldo
Всегда диспозил контекст. Решил немного погуглить и набрёл на статью, в которой объяснено, почему диспозить не обязательно, но всё же это можно считать хорошим паттерном. https://blog.jongallant.com/2012/10/do-i-have-to-call-dispose-on-dbcontext/