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