Single Page Application : ASP.NET MVC .NET Core + Angular 4. Часть 5.

Новая статья после долгого (вынужденного) перерыва. Это будет уже пятой частью в сери статей о создание одностраничного приложения с использованием ASP.NET MVC .NET Core и Angular 4. Следующим логичным шагом в создании приложения будет добавления авторизации пользователей. Но сначало нужно немного обновить проект. Со времени последней публикации прошло несколько месяцев и за это время произошло несолько событий:

  • Вышел релиз PostgreSQL 10 и pgAdmin 4
  • Вышло обновление .NET Core до версии 2.0
  • Обновилась система авторизации ASOS

Таким образом в данной статье я хочу описать подготовительные работы по обновлению проекта:

  • переведу базу данных на PostgreSQL 10
  • приведу имена таблиц и полей в соответствие с философией PostgreSQL
  • добавлю таблицу пользователей
  • обновим проект до версии .NET Core 2.0
  • перепишу код доступа к БД. Избавлюсь от Entity Framework и буду использовать более эффективный npgsql драйвер
  • вынесу код доступа к БД в отдельный проект
  • реализую обьекты для управления пользователями

Это будет подготовительный этап. В следующей части я займусь непосредственно реализацией авторизации:

  • Добавлю поддержку ASOS в проект для авторизации при помощи токенов OpenID / OAuth2
  • Реализую код для авторизации и создания access_token и refresh_token для работы с Web API
  • Реализую страницы авторизации и добавления новго пользователя в Angular 4
  • Изменю структуру проекта, что бы сделать его более завершенным

Как видите, я собираюсь сделать много изменений, и именно поэтому решил разбить статью на две части. Так будет проще воспринимать новую информацию.
Итак, начнем.

Обновляемся до PostgreSQL 10 и pgAdmin 4

Как я уже говорил, в работе я использую PostgreSQL и эта БД очень мне нравится. В ней постоянно появляются новые функции и она стоит на одном уровне по функционалу с любой платной БД. Поэтому я и использую ее в своих примерах. Но вам совсем не обьязательно использовать эту БД. После изменений в проекте, вы сможете использовать любую БД очень легко.
В любом случае, для начала нужно скачать новый релиз PostgreSQL:

https://www.enterprisedb.com/downloads/postgres-postgresql-downloads#windows

После скачивания устанавливаем. Там все просто.

Так же скачиваем и устанавливаем pgAdmin 4

https://www.pgadmin.org/download/pgadmin-4-windows/

ОК, теперь мы готовы к миграции БД. По большому счету у нас как таковой БД и не было. Всего лишь одна таблица. Поэтому проще всего создать все заново.
Запускаем pgAdmin 4. Теперь у него асолютно новый вид. В левой панели раскрываем Databases -> PostgreSQL 10, кликаем правой кнопкой мышки на Login/Group Roles и выбираем пункт меню Create -> Login/Group Role …

PostgreSQL 10 - Создание нового пользователя

PostgreSQL 10 — Создание нового пользователя

Водим имя пользователя, который будет использоваться для логина в PostgreSQL 10 сервер. Используем то же самое имя что и прежде: spa_user

PostgreSQL 10 - Создание нового пользователя - Ввод имени пользователя

PostgreSQL 10 — Создание нового пользователя — Ввод имени пользователя

Вводим пароль как и прежде: spa_u1234

PostgreSQL 10 - Создание нового пользователя - Ввод пароля

PostgreSQL 10 — Создание нового пользователя — Ввод пароля

Устанавливаем настройки, которые позволят новому пользователю логиниться к серверу.

PostgreSQL 10 - Создание нового пользователя - Кстановка прав доступа

PostgreSQL 10 — Создание нового пользователя — Кстановка прав доступа

ОК, новый пользователь готов. Теперь пора создать нашу базу данных. Щелкаем правой кнопкой мышки на PostgreSQL 10 –> Databases и выбираем команду Create -> Database …

PostgreSQL 10 - Создание новой базы данных

PostgreSQL 10 — Создание новой базы данных

Вводим имя новой базы данных: spa

А так же меняем владельца на нашего пользователя spa_user

PostgreSQL 10 - Создание новой базы данных - Ввод имени и пользователя

PostgreSQL 10 — Создание новой базы данных — Ввод имени и пользователя

База данных готова. Пора добавлять таблицы. Если вы помните предыдущую статью, мы использовали EF Code First. Т.е. мы добавили классы C# а затем разрешили EF создать базу данных и нужные таблицы за нас. Это очень удобно при прототипировании приложения или при создании простого демо приложения. Но для более серьезных проектов EF не лучшее решение. За удобство нужно платить, и в слчае EF и PostgreSQL это сильно заметно. Производительность не на высоте, даже для простых запросов. Именно поэтому я не использую EF в своих проектах. Видать со времен когда я программировал на С/С++ я люблю полный контроль над тем, что происходит в коде. И производительность парктически всегда на первом месте.

Итак, создадим таблицы вручную используя инструменты pgAdmin 4. Раскрываем список в левой части pgAdmin 4 и находим Databases -> spa -> Schemas -> public -> Tables. Правый щелчок мышки и выбираем команду Create -> Table …

PostgreSQL 10 - Создание таблицы Users

PostgreSQL 10 — Создание таблицы Users

Водим

  • Имя таблицы (Name) : users
  • Владель (Owner) : spa_user

Остально еставляем по умолчанию и переключаемся на закладку Columns. На этой закладке мы создадим наши колонки в таблице users. Вводите имена колонок, типов и опции как на скриншоте ниже.

PostgreSQL 10 - Создание таблицы Users

PostgreSQL 10 — Создание таблицы Users

Мы создали простую таблицу, в которой будем хранить пользователей. Минимальный набор данных, который нам нужно знать о пользователях: логин, пароль (в хешированом виде), и возможно полное имя (не обьязательно). Кроме того нам нужен уникальный первичный ключ, поле id. Как вы наверное заметили, я придерживаюсь стиля записи в стили under score. Это потому, что PostgreSQL чуствителен к регистру, и при записи имен таблиц, полей и других обектов с использованием символов верхнего регистра, имена нужно экранировать в запросах. Если все писать в нижнем регистре, то экранирование не требуется.

Теперь нам стоит дать имя нашему первичному ключю. Перейдем на закладку Constrains и дадим имя первичному ключу pk_users как на скриншоте ниже.

PostgreSQL 10 - Создание таблицы Users

PostgreSQL 10 — Создание таблицы Users

И последняя настройка. Что бы наш пользователь базы данных (spa_user) мог работать с таблицей, мы должны назначить ему права доступа. Перейдем на закладку Security, в спиcке Grantee выбираем пользователя под которым мы будем работать в БД (spa_user), а в поле Privileges устанавливаем отметку на чекбоксе All. Должно получиться как на скриншоте ниже. Это дает право пользователю spa_user производить любые действия с данной таблицей. Но он не может сделать ничего больше с другими обьктами в БД.

PostgreSQL 10 - Создание таблицы Users

Можно нажать кнопку Save и таблица пользователей готова.

Теперь добавим таблицу с тестовыми данными, изменив ее. Назовем ее orders. И таблица будет хранить информацию о неких заказах (в данный момент неважно каких). Для теста мы будем использовать следующую информацию о заказе:

  • Номер заказа
  • Пользователь, кто сделал заказ
  • Время, когда сделан закза
  • Email,на котоырй нужно выслать письмо с информацией о заказе
  • Сумма заказа
  • Описание заказа

Так как у нас есть таблица с пользователями, то мы будем хранить в таблице Orders только id пользователя. Дальше я покажу скриншоты с шагами, которые нужно сделать, что бы создать эту страницу.

PostgreSQL 10 - Создание таблицы Orders

PostgreSQL 10 — Создание таблицы Orders. Задаем имя и владельца.

PostgreSQL 10 - Создание таблицы Orders

PostgreSQL 10 — Создание таблицы Orders. Создаем поля данных.

PostgreSQL 10 - Создание таблицы Orders

PostgreSQL 10 — Создание таблицы Orders. Создаем первичный ключ.

PostgreSQL 10 - Создание таблицы Orders

PostgreSQL 10 — Создание таблицы Orders. Настраиваем ограничения (опционально)

PostgreSQL 10 - Создание таблицы Orders

PostgreSQL 10 — Создание таблицы Orders. Настраиваем права доступа.

Все, база данных готова.

Обновление проекта до версии .NET Core 2.0

Следующий шаг – обновление прокта до последний (на момент написания статьи) весрии .NET Core 2.0

Открываем проект в Visual Studio 2017. Открываем NuGet Manager.

Visual Studio 2017 - Открытие NuGet Manager

Visual Studio 2017 — Открытие NuGet Manager

Для начала перейдем в раздел установленных пакетов и удалим не нужные пакеты.

 

Visual Studio 2017 - NuGet Manager - Установленные пакеты

Visual Studio 2017 — NuGet Manager — Установленные пакеты

Нам не понядобяться такие пакеты:

Microsoft.EntityFrameworkCore.SqlServer
Microsoft.EntityFrameworkCore.Design
Microsoft.EntityFrameworkCore.SqlServer.Design
Microsoft.EntityFrameworkCore.Tools
Npgsql.EntityFrameworkCore.PostgreSQL

Удаляйте все эти пакеты из проекта.

Теперь меням .NET Core версию проекта. Переходим в окно свойств проекта и на закладке Application выбираем .NET Core 2.0 в выпадающем списке Target Framework.

 

Visual Studio 2017 - Настройки проекта

Visual Studio 2017 — Настройки проекта

Если вдруг у вас нет новой версии, то вам возможно нужно обновить вашу Visual Studio 2017 до последней версии.

Затем снова открываем NuGet Manager и переходим на закладку Updates. Как видите, у мена показывается, что необходимо обновить 8 пакетов. Выбираем все пакеты и нажимаем кнопку Update. Убедитесь что чекбокс Include prereleases не выделен, иначе вы поставите beta версии пакетов.

 

Visual Studio 2017 - NuGet Manager - Пакеты с доступными обновлениями

Visual Studio 2017 — NuGet Manager — Пакеты с доступными обновлениями

Немного ждем. Это может занять некоторое время.После того как процесс обновления закончиться наш проект будет использовать библиотеки для .NET Core 2.0.
Но если вы попробуете скомпилировать проект, то он не будет работать. Это потому что мы удалили библиотеки для работы с базой данных.

ВАЖНО: Возможно вы можете встретить ошибку вроде

Duplicate 'Content' items were included. The .NET SDK includes 'Content' items from your project directory by default.
You can either remove these items from your project file, or set the 'EnableDefaultContentItems' property to 'false' if you want to explicitly include them in your project file.
For more information, see https://aka.ms/sdkimplicititems.
The duplicate items were:
'wwwroot\app\about.component.ts';
'wwwroot\app\app.component.ts';
'wwwroot\app\app.module.ts';
'wwwroot\app\app.routing.ts';
'wwwroot\app\contact.component.ts';
'wwwroot\app\index.component.ts';
'wwwroot\app\main.ts';
'wwwroot\app\models\TestData.ts';
'wwwroot\app\rxjs-operators.ts';
'wwwroot\app\services\SampleData.services.ts';
'wwwroot\styles.css';
'wwwroot\systemjs-angular-loader.js';
'wwwroot\systemjs.config.extras.js';
'wwwroot\systemjs.config.js';
'wwwroot\tsconfig.json'

Не волнйтесь, просто Visual Studio 2017 включило файлы дважды. Что бы испраить ошибку нужно сделать простые шаги:

  1. нажимаем иконку Show All Files в Solution Explorer
  2. нажимаем правой кнопкой мышки напапке wwwroot и выбираем опцию Exclude From Project
  3. затем снова на той же папке нажимаем Include in Project

Все, ошибка исправлена.

 

Новый код доступа к БД

Так как мы удалили поддержку EF из проекта, а так же изменили структуру БД, нам нужно переписать код доступа к БД.

Во-первых, мы вынесем весь код для работы с БД в отдельный проект включенный в наш Solution. Так мы отделим зоны ответственности кода.

Во-вторых, доступ к обьектам, которые будут работать с БД будет осуществляться только через интерфейсы. Так мы сможеи использовать Dependency Injection и в принципы скрыть конкретную реализацию для хранения данных от основного приложения. Кроме того, использование интерфейсов позволяет легко писать тесты (но мы этим занимтаься не будем).

В-третьих, для работы с PostgreSQL мы будем использовать драйвер для C# — NpgSQL.

Ну что же, начнем с создания нового проекта. Назовем его SpaDatasource.

 

Visual Studio 2017 - Создание нового проекта SpaDatasource

Visual Studio 2017 — Создание нового проекта SpaDatasource

Удаляем файл Class1.cs из проекта, он нам не нужен. Вместо него создадим следующие папки:

Entitites
В этой папке будем хранить POCO классы для работы с базой данных

Interfaces
В этой папке будем хранить все наши интерфейсы для работы с БД и ее обьектами

Implementations
В этой папке будут храниться конкретные реализации интерфейсов

Helpers
Эта папка для хранения вспомогательных классов

Небольшое оступление, прежде чем мы начнем создавать интерфесы и классы.
Для упарвления пользователями и авторизацией / аутенфикацией в ASP.NET Core есть очень мощный набор библиотек ASP.NET Identity. В этой библиотеке определяются интерфесы для работы с пользователями, хранилищами пользователей, управление их ролями в системе. Кроме того, есть конкретная реализация на оснвое EF для MS SQL Server. Эту библиотеку можно подключить как NuGet пакет и включить в работу буквально парой строчек. Все будет работать из коробки.

Но есть несколько ограничений:

  • только EF
  • только MS SQL Server (хотя есть кастомные реализации и для других БД если сильно поискать)
  • огромное количество интерфейсов, методов и таблиц в БД с огромным количеством информации, которые не нужны большинству приложений
  • то не работает если у вас есть готовая БД в которой пользователя и связанная информация храняться в другом виде (отличаются таблицы, поля, их имена и типы)

Т.е. если вы начинаете новый проект, и максимальная производительность для него не важна, и вы не против огромного количества информации, которая возможно вам никогда не потребуется, то такое коробочное решение вам подойдет. В остальных случаях лучше самостоятельно разработать систему управления пользователями основываясь на нуждах вашего приложения.
Теперь создадим интерфейсы. И первый интерфейс – это ISpaDatasource. Создаем его в папке Interfaces. Именно через него можно будет получать информацию из БД и сохранять ее в БД. Для начала наш интерфейс будет очень простым:

public interface ISpaDatasource
{
	#region API : Открытие и закрытие соединения

	void Open();
	void Close();

	#endregion

	#region API : Работа с пользователями

	IEnumerable Users();
	User FindUserById(int id);
	User FindUserByName(string name);
	void InsertUser(User user);
	void UpdateUser(User user);

	#endregion

	#region API : Работа с заказами

	IEnumerable Orders();
	Order FindOrderById(int id);
	IEnumerable FindOrdersForUser(int userId);
	void InsertOrder(Order order);

	#endregion

}

В этом интерфейсе мы определили методы для работы с пользователями и заказами.

Следующий интерфейс создает менеджера пользователей. Именно он будет работать с интерфейсом базы данных и управлять пользователями, а не приложение напрямую.
Зачем создавать отдельный интерфейс для менеджера пользователей если все операции определенны в интерфейсе БД? Ответ достаточно прост — таким образом вы разделяем код и делаем его слабо связаным. Так как менеджер пользователей имеет доступ только к интерфейсу БД, то мы можем менять код работы с БД в конкретной реализации как нам угодно. Мы можем изменить интерфейс доступа к БД и приложение об этом не узнает, мы можем изменить имена таблиц и полей в БД, можем заменить даже саму БД на другую, а менеджер пользователей и приложение об этом не узнают и будут работать как и прежде. Пока мы соблюдаем контракт методов в интерфесе, за ним мы можем делать все что угодно.

Итак, следующий интерфейс IUserManager:

public interface IUserManager
{
	Task FindByNameAsync(string username);
	Task CheckPasswordAsync(User user, string password);
	Task CreateAsync(User user, string password);
}

Для нашего простого примера будет достаточно этих двух методов. В реальном приложении их должно быть намного больше. Примерный список можно посмотреть если зайти на страницу описания стандартного ASP.NET Интерфеса IUserManager.

С интерфейсами разобрались, теперь нужно обьявить наши POCO обьекты. В нашем случае у нас две сущности, которые храняться в БД: пользователь (user) и заказ (order). Вот эти две сущности нам и нужно представить в виде обьектов. Создаем их в папке Entities.

Класс для представления пользователя User.cs:

public class User
{
	public long Id;
	public string Login;
	public string PasswordHash;
	public string FullName;
}

Класс для представления заказа Order.cs:

public class Order
{
	public long Id;
	public DateTime Time;
	public int UserId;
	public string EmailAddress;
	public decimal Currency;
	public string OrderDescription;
}

Интерфейсы и классы сущностей есть, теперь можно заняться реализайией интерфейсов.

Перед тем, как работать с БД нам нужно установить драйвер для работы с PostgreSQL. Такой драйвер есть и это NpgSQL. Снова открываем NuGet Manager и переключаемся на закладку Browse. В поле поиска вводим npgsql.

 

Visual Studio 2017 - Устанвока модуля npgsql

Visual Studio 2017 — Устанвока модуля npgsql

Нажимаем Install. Теперь мы можем работать с нашей БД.

 

Реализация интерфеса ISpaDatasource

Начнем реализацию с интерфеса ISpaDatasource. Поместим реализацию в класс SpaDatasource. Создаем файл в SpaDatasource.cs папке Implementations. Реализация интерфеса будет выглядеть следующим образом:

using Npgsql;
using NpgsqlTypes;
using SpaDatasource.Entitites;
using SpaDatasource.Interfaces;
using System;
using System.Collections.Generic;
using System.Text;

namespace SpaDatasource.Implementations
{
    public class SpaDatasource : ISpaDatasource, IDisposable
    {
        #region Атрибуты

        private bool _IsAlreadyDisposed = false;

        private string _ConnectionString;
        private NpgsqlConnection _Conn;

        #endregion

        #region Инициализация и освобождение ресурсов

        public SpaDatasource(string connString)
        {
            _ConnectionString = connString;
        }

        public void Dispose()
        {
            Dispose(true);  
            GC.SuppressFinalize(this);  
        }

        protected virtual void Dispose(bool isDisposing)
        {
            if (_IsAlreadyDisposed)
                return;

            if (isDisposing)
            {
                // освобождаем управляемые ресурсы тут
                Close();
            } 

            // если есть неуправляемые ресурсы - то их нужно совободить тут

            _IsAlreadyDisposed = true;
        }

        #endregion

        #region Открытие и закрытие соединения с БД

        public void Open()
        {
            if (_IsAlreadyDisposed)
                throw new ObjectDisposedException("MyDbContext", "Called [Open] method on disposed object.");

            if(_Conn != null)
            {
                if(_Conn.State != System.Data.ConnectionState.Closed && _Conn.State != System.Data.ConnectionState.Broken)
                    throw new InvalidOperationException("Called [Open] on already opened connection.");
            }

            _Conn = new NpgsqlConnection(_ConnectionString);
            _Conn.Open();
        }

        public void Close()
        {
            if(_Conn == null)
                return;

            if(_Conn.State == System.Data.ConnectionState.Closed || _Conn.State == System.Data.ConnectionState.Broken)
                return;

            _Conn.Close();
        }

        #endregion

        #region Работа с пользователями

        public User FindUserById(int id)
        {
            CheckConnValidity("FindUserById");

            string sql = "SELECT id, login, password_hash, full_name FROM users WHERE id = :id";
            NpgsqlCommand command = CreateCommandWithTimeout(sql, _Conn);
            command.Parameters.Add(new NpgsqlParameter("id", NpgsqlDbType.Integer));
            command.Parameters[0].Value = id;

            return GetUserFromCommand(command);
        }

        public User FindUserByName(string name)
        {
            CheckConnValidity("FindUserByName");

            string sql = "SELECT id, login, password_hash, full_name FROM users WHERE login = :login";
            NpgsqlCommand command = CreateCommandWithTimeout(sql, _Conn);
            command.Parameters.Add(new NpgsqlParameter("login", NpgsqlDbType.Text));
            command.Parameters[0].Value = name;

            return GetUserFromCommand(command);
        }

        public void InsertUser(User user)
        {
            CheckConnValidity("InsertUser");

            string sql = "INSERT INTO users (login, password_hash, full_name) VALUES (:login, :password_hash, :full_name) RETURNING id";
            NpgsqlCommand command = CreateCommandWithTimeout(sql, _Conn);

            command.Parameters.Add(new NpgsqlParameter("login", NpgsqlDbType.Text));
            command.Parameters[0].Value = user.Login;

            command.Parameters.Add(new NpgsqlParameter("password_hash", NpgsqlDbType.Text));
            command.Parameters[1].Value = user.PasswordHash;

            command.Parameters.Add(new NpgsqlParameter("full_name", NpgsqlDbType.Text));
            command.Parameters[2].Value = user.FullName;

            long id = (long)command.ExecuteScalar();

            user.Id = id;
        }

        public void UpdateUser(User user)
        {
            CheckConnValidity("UpdateUser");

            string sql = "UPDATE users SET login = :login, password_hash = :password_hash, full_name = :full_name";
            NpgsqlCommand command = CreateCommandWithTimeout(sql, _Conn);

            command.Parameters.Add(new NpgsqlParameter("id", NpgsqlDbType.Integer));
            command.Parameters[0].Value = user.Id;

            command.Parameters.Add(new NpgsqlParameter("login", NpgsqlDbType.Text));
            command.Parameters[0].Value = user.Login;

            command.Parameters.Add(new NpgsqlParameter("password_hash", NpgsqlDbType.Text));
            command.Parameters[1].Value = user.PasswordHash;

            command.Parameters.Add(new NpgsqlParameter("full_name", NpgsqlDbType.Text));
            command.Parameters[2].Value = user.FullName;

            command.ExecuteNonQuery();
        }

        public IEnumerable<User> Users()
        {
            CheckConnValidity("Users");

            List<User> list = new List<User>();

            string sql = "SELECT id, login, password_hash, full_name FROM users";
            NpgsqlCommand command = CreateCommandWithTimeout(sql, _Conn);

            NpgsqlDataReader dr = command.ExecuteReader();
            while(dr.Read())
            {
                User u = GetUserFromDataReader(dr);
                list.Add(u);
            }

            dr.Close();

            return list;
        }

        #endregion

        #region Работа с заказми

        public IEnumerable<Order> Orders()
        {
            CheckConnValidity("Orders");

            List<Order> list = new List<Order>();

            string sql = "SELECT id, \"time\", user_id, email, currency, description FROM orders";
            NpgsqlCommand command = CreateCommandWithTimeout(sql, _Conn);

            NpgsqlDataReader dr = command.ExecuteReader();
            while(dr.Read())
            {
                Order u = GetOrderFromDataReader(dr);
                list.Add(u);
            }

            dr.Close();

            return list;
        }

        public IEnumerable<Order> FindOrdersForUser(int userId)
        {
            CheckConnValidity("FindOrdersForUser");

            List<Order> list = new List<Order>();

            string sql = "SELECT id, \"time\", user_id, email, currency, description FROM orders WHERE user_id = :user_id";
            NpgsqlCommand command = CreateCommandWithTimeout(sql, _Conn);

            command.Parameters.Add(new NpgsqlParameter("user_id", NpgsqlDbType.Integer));
            command.Parameters[0].Value = userId;

            NpgsqlDataReader dr = command.ExecuteReader();
            while(dr.Read())
            {
                Order u = GetOrderFromDataReader(dr);
                list.Add(u);
            }

            dr.Close();

            return list;
        }

        public Order FindOrderById(int id)
        {
            CheckConnValidity("FindOrderById");

            string sql = "SELECT id, \"time\", user_id, email, currency, description FROM orders WHERE id = :id";
            NpgsqlCommand command = CreateCommandWithTimeout(sql, _Conn);

            command.Parameters.Add(new NpgsqlParameter("id", NpgsqlDbType.Integer));
            command.Parameters[0].Value = id;

            return GetOrderFromCommand(command);
        }

        public void InsertOrder(Order order)
        {
            CheckConnValidity("InsertOrder");

            string sql = "INSERT INTO orders (\"time\", user_id, email, currency, description) VALUES (:time, :user_id, :email, :currency, :description) RETURNING id";
            NpgsqlCommand command = CreateCommandWithTimeout(sql, _Conn);

            command.Parameters.Add(new NpgsqlParameter("time", NpgsqlDbType.TimeTZ));
            command.Parameters[0].Value = order.Time;

            command.Parameters.Add(new NpgsqlParameter("user_id", NpgsqlDbType.Integer));
            command.Parameters[1].Value = order.UserId;

            command.Parameters.Add(new NpgsqlParameter("email", NpgsqlDbType.Text));
            command.Parameters[2].Value = order.EmailAddress;

            command.Parameters.Add(new NpgsqlParameter("currency", NpgsqlDbType.Numeric));
            command.Parameters[3].Value = order.Currency;

            command.Parameters.Add(new NpgsqlParameter("description", NpgsqlDbType.Text));
            command.Parameters[4].Value = order.OrderDescription;

            long id = (long)command.ExecuteScalar();

            order.Id = id;
        }

        #endregion

        #region Вспомогательные классы

        private void CheckConnValidity(string methodName)
        { 
            if (_IsAlreadyDisposed)
                throw new ObjectDisposedException("MyDbContext", "Called [" + methodName + "] method on disposed object.");

            if(_Conn == null)
                throw new InvalidOperationException("Called [" + methodName + "] method on null connection object.");

            if(_Conn.State == System.Data.ConnectionState.Closed) 
                throw new InvalidOperationException("Called [" + methodName + "] method on closed connection object.");

            if(_Conn.State == System.Data.ConnectionState.Broken)
                throw new InvalidOperationException("Called [" + methodName + "] method on broken connection object.");
        }

        private NpgsqlCommand CreateCommandWithTimeout(string cmdText, NpgsqlConnection conn, int Timeout = 30)
        {
            NpgsqlCommand command = new NpgsqlCommand(cmdText, conn)
            {
                CommandTimeout = Timeout
            };

            return command;
        }

        private User GetUserFromCommand(NpgsqlCommand command)
        {
            User user = null;

            NpgsqlDataReader dr = command.ExecuteReader();
            if (dr.Read())
            {
                user = GetUserFromDataReader(dr);
            }

            dr.Close();

            return user;
        }

        private User GetUserFromDataReader(NpgsqlDataReader dr)
        {
            return new User
                {
                    Id             = dr.GetInt64(0),
                    Login          = dr.GetString(1), 
                    PasswordHash   = dr.GetString(2),
                    FullName       = dr.IsDBNull(3) ? null : dr.GetString(3)
                };
        }

        private Order GetOrderFromCommand(NpgsqlCommand command)
        {
            Order order = null;

            NpgsqlDataReader dr = command.ExecuteReader();
            if (dr.Read())
            {
                order = GetOrderFromDataReader(dr);
            }

            dr.Close();

            return order;
        }

        private Order GetOrderFromDataReader(NpgsqlDataReader dr)
        {
            return new Order
                {
                    Id                  = dr.GetInt64(0),
                    Time                = dr.GetDateTime(1), 
                    UserId              = dr.GetInt32(2),
                    EmailAddress        = dr.GetString(3),
                    Currency            = dr.GetDecimal(4),
                    OrderDescription    = dr.IsDBNull(5) ? null : dr.GetString(5)
                };
        }

        #endregion

    }
}

Как видите, реализация достатчоно обьемная. Остановлюсь на соновных моментах.

Функции для открытия и закрытия соединния очень просты. Используется стандартный Npgsql API. Для открытия нужна строка подключения. Мы ее добавим в основной проект позже.

В начале каждого метода проверяется, если обьект еще в хорошем состоянии и соединение к БД установленно. Для этого есть метод:

private void CheckConnValidity(string methodName)

Для соединения с БД имеет смысл указать таймаут. Если этого не сделать то можно исчерпать пул соединений, которые будут ждать установки соединения / завершения команды слишком долго. Для этогоо есть метод:

private NpgsqlCommand CreateCommandWithTimeout(string cmdText, NpgsqlConnection conn, int Timeout = 30)

Он создает обьект команды Npgsql на основе переданого запроса и устанавливает таймаут на операцию (30 секунд по умолчанию)

Все операции с БД выполняются при помощи класса NpgsqlCommand. Этот класс строится на основе SQL запроса с параметрами. Всегда нужно использовать параметризированый запрос вместо вставки значений в строку запроса. Так вы даете возможность закешировать запрос на стороне сервера, и не дадит возможности осуществить SQL иньекцию.

Код работы с БД немного многословен, особенно по сравнению с EF и LINQ2SQL. Все что реализыется внутри этих библиотек в нашем случае нужно написать руками. В этом методе есть свои недостатки:

  • нужно писать много кода
  • нужно знать язык SQL для составленяи правильных запросов
  • как следствие легко допустить ошибку в коде

Но при этом мы получаем и приимущества:

  • максимально быстрый код
  • полный контроль над тем, как и когда мы работаем с БД

Тут каждый должен решить для себя что важнее в его проекте – быстрота написания кода и потеря в производительности или больше время на кодирование и поддержку, но максимальная сорость работы. Для себя я выбрал второе 🙂

 

Реализация интерфеса IUserManager

Добавим в папку Implementations новый класс SpaUserManager. Этот класс будет реализовывать интерфейс IUserManager.

using SpaDatasource.Interfaces;
using System;
using System.Collections.Generic;
using System.Text;
using SpaDatasource.Entitites;
using System.Threading.Tasks;
using SpaDatasource.Helpers;

namespace SpaDatasource.Implementations
{
    public class SpaUserManager : IUserManager
    {
        private ISpaDatasource    _SpaDatasource;

        public SpaUserManager(ISpaDatasource spaDatasource)
        {
            _SpaDatasource = spaDatasource;
        }

        public Task<bool> CheckPasswordAsync(User user, string password)
        {
            bool isValid = PasswordHasher.Verify(password, user.PasswordHash);

            return Task.FromResult<bool>(isValid);
        }

        public Task<User> FindByNameAsync(string username)
        {
            return Task<User>.Run( () => 
            { 
                User u = null;

                try
                {
                    _SpaDatasource.Open();

                    u = _SpaDatasource.FindUserByName(username);
                }
                finally
                {
                    _SpaDatasource.Close();
                }

                return u;
            } );
        }

        public Task CreateAsync(User user, string password)
        {
            return Task<User>.Run( () => 
            { 
                try
                {
                    user.PasswordHash = PasswordHasher.Hash(password);

                    _SpaDatasource.Open();

                    _SpaDatasource.InsertUser(user);
                }
                finally
                {
                    _SpaDatasource.Close();
                }
            } );
        }
    }
}

Тут все просто. При создании обьекта SpaUserManager мы в конструкторе передаем обьект доступа к БД через интерфейс ISpaDatasource.

У нас есть всего два метода.

public Task CheckPasswordAsync(User user, string password)

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

public Task FindByNameAsync(string username)

Этот метод ищет пользователя по заданому имени пользователя (логину). Поиск тоже должен выполняться асинхроно. В этом случае нам нужно делат запрос к БД, поэтому мы создаем новую задачу в пуле задач и в ней ищем пользователя в БД. Так как время на соединение и поиск в БД может быть существенное (в теории) то лучше выполнять эту операцию в отдельном потоке и не блокировать основной поток. Для ASP.NET это не столь важно, так как каждый запрос выполняется в одном потоке, а мы не сможем завершить обработку запроса пока не получим пользователя из БД. Но если наш менеджер будет использоваться в desktop приложении, то асинхроность будет очень кстати.

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

Следующий метод для создания нового пользователя

Task CreateAsync(User user, string password)

Внутрь метода передается обьект нового метода и пароль. Пароль нужно передавать отдельно, так как в обьекте пользователя храниться только хэш пароля. И только менеджер пользователей знает как с ним работать.

И как раз для этого был добавлен новый класс PasswordHasher. Именно он должен уметь генерировать хэш пароля и проверять соответствие между сохраненным хэшем и введеным паролем.

Его реализацию я взял со StackOverflow и немного доработал. Добавим этот новый класс в прокт в папку Helpers в файл PasswordHasher.cs:

using System;
using System.Collections.Generic;
using System.Security.Cryptography;
using System.Text;

namespace SpaDatasource.Helpers
{
    public class PasswordHasher
    {
        private const int SaltSize = 16;
        private const int HashSize = 32;

        public static int Iterations { set; get; } = 10000;

        public static string Hash(string password)
        {
            //create random salt
            byte[] salt;
            new RNGCryptoServiceProvider().GetBytes(salt = new byte[SaltSize]);

            //create hash
            Rfc2898DeriveBytes pbkdf2 = new Rfc2898DeriveBytes(password, salt, Iterations);
            byte[] hash = pbkdf2.GetBytes(HashSize);

            //combine salt and hash
            byte[] hashBytes = new byte[SaltSize + HashSize];
            Array.Copy(salt, 0, hashBytes, 0, SaltSize);
            Array.Copy(hash, 0, hashBytes, SaltSize, HashSize);

            return Convert.ToBase64String(hashBytes);
        }

        public static bool Verify(string password, string hashedPassword)
        {
            if(hashedPassword == null || hashedPassword.Length <= 0)
                return false;

            //get hashbytes
            byte[] hashBytes = Convert.FromBase64String(hashedPassword);

            if(hashBytes.Length < SaltSize)
                return false;

            //get salt
            byte[] salt = new byte[SaltSize];
            Array.Copy(hashBytes, 0, salt, 0, SaltSize);

            //create hash with given salt
            Rfc2898DeriveBytes pbkdf2 = new Rfc2898DeriveBytes(password, salt, Iterations);
            byte[] hash = pbkdf2.GetBytes(HashSize);

            //get result
            for (int i = 0; i < HashSize; i++)
            {
                if (hashBytes[i + SaltSize] != hash[i])
                {
                    return false;
                }
            }

            return true;
        }
    }
}

Это очень хороший метод генерации хэшей паролей. Он использует случайно сгенерированую соль для каждого пароля и использует один из последних методов надежного шифрования без возможнсоти востановления.

До полной готовности нам осталось добавить всего пару файлов. В принципе нашу библиотеку доступа к данным можно начинать использовать прямо так. Но хочу добавить один метод расширения, который позволит легко и стандартным сопособом регестрировать наши обьекты и интерфейсы в Dependency Injection (DI) инфраструктуре ASP.NET.

Как вы знаете в ASP.NET Core все DI можно зарегестрировать в специальной коллекции сервисов. Это делается в методе который расположен в файле Startup.cs каждого ASP.NET проекта:

public void ConfigureServices(IServiceCollection services)

Например все обьекты MVC регистрируются следующим образом:

services.AddMvc();

Вот и мы можем сделать то же самое для нашей библиотеки. Для этого добавим новый файл DIServiceExtensions.cs в папку Helpers. Добавим в него такой код:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace Microsoft.Extensions.DependencyInjection
{
    public static class DIServiceExtensions
    {
        public static void AddSpaDatasource(this IServiceCollection services, SpaDatasource.Helpers.Options options)
        {
            services.AddScoped<SpaDatasource.Interfaces.ISpaDatasource>((s) => 
                {
                    SpaDatasource.Implementations.SpaDatasource dbConntext = new SpaDatasource.Implementations.SpaDatasource(options.ConnectionString);
                    return dbConntext;
                });
        }

        public static void AddUserManager(this IServiceCollection services)
        {
            services.AddScoped<SpaDatasource.Interfaces.IUserManager, SpaDatasource.Implementations.SpaUserManager>();
        }

    }
}

Это функции, которые расширяют интерфейс IServiceCollection. Используя эти методы расширения мы регестрируем наши интерфесы в глобальной системе DI. Вызов services.AddScoped позволяет системе создавать единственный объект со временем жизни HTTP запроса. Т.е. когда система встретит необходимость в создании обекта она создаст его и он будет жить до конца выполнения HTTP Запроса.

Для передачи параметров обьекту SpaDatsource я создал простой класс SpaDatasource.Helpers.Options. Внутри класса есть только одно поле, которое нужно для передачи строки подключения.

Но что бы это работало, нужно установить NuGet пакет, который реализует логику DI. Открываем NuGet Manager и ищем в нем покет с именем

Microsoft.Extensions.DependencyInjection.Abstractions
Visual Studio 2017 - NuGet Manager - Установка Mictorosft DI пакета

Visual Studio 2017 — NuGet Manager — Установка Mictorosft DI пакета

Устанавливаем его, и теперь интерфейс IServiceCollection доступен нам.

После всех изменений структура проекта должна выглядеть как на скриншоте ниже

Visual Studio 2017 - Как должен выглядеть проект SpaDatasource

Visual Studio 2017 — Как должен выглядеть проект SpaDatasource

Теперь можно скомпилировать проект SpaDatasource. Все должно собраться.

 

Подключение SpaDatasource к основному проекту

Теперь настало время подключить новый источник данных к нашему основному проекту. И начнем мы снова с удаления ненужных классов и файлов. Во первых нам больше не нужны старые класы для работы с данными. Удалим полностью папку Data.

Следующий шаг – установим зависимость проекта AngularSpa от проекта SpaDatasource. Нажимаем правую кнопку на корневом элементе солюшена в Solution Explorer и выбираем пункт меню Project Dependencies.

 

Visual Studio 2017 - Установка зависимостей проектов

Visual Studio 2017 — Установка зависимостей проектов

Настраиваем зависимость проекта AngularSpa от проекта SpaDatasource как показано на скриншоте ниже.

 

Visual Studio 2017 - Установка зависимостей проектов

Visual Studio 2017 — Установка зависимостей проектов

Теперь когда будет собираться проект AngularSpa вначале будет проверен проект SpaDatasource на изменения и при необходимости пересобран.
Но этого мало. Нужно так же настроить references между нашими проектами, что бы сделать интерфейсы и классы из проекта SpaDatsource доступными для проекта AngularSpa. Нажимаем правую кнопку на корневом элементе проекта AngularSpa и выбираем пункт меню Add -> Referencies.

 

Visual Studio 2017 - Установка сылок на библиотеки

Visual Studio 2017 — Установка сылок на библиотеки

В открывшемя окне выбираем в левой части опции Projects -> Solution и включаем чекбокс для SpaDatasource. Нажимаем ОК. Все готово.

Visual Studio 2017 - Установка сылок на библиотеки

Visual Studio 2017 — Установка сылок на библиотеки

Открываем файл Startup.cs. Удаляем в нем несколько строк. Вначале строки для включения прсотранства имен

using AngularSpa.Data;
using Microsoft.EntityFrameworkCore;

Затем код, который подключал стандартный EF в DI проекта

services
	.AddEntityFrameworkNpgsql()
	.AddDbContext(options =&gt; options.UseNpgsql(connString));

— итак же строки кода, которые создавали тестовую БД на лету

if(env.IsDevelopment())
{
	DbInitializer.Initialize(context);
}

Теперь нужно подключить нашу систему для работы с БД ипользователями к DI проекта. Сделать это нужно в методе ConfigureServices после строки services.AddMvc();

services.AddSpaDatasource(new SpaDatasource.Helpers.Options 
	{ 
		ConnectionString = Configuration.GetConnectionString("SpaDbConnection")
	});

services.AddUserManager();

Как видите наши методы расширения смотряться тут как родные.

Как видно, мы читаем строку подключения к БД из стандартной конфигурации проекта, которая храниться в файле appsettings.json. Подключение к БД не изменилось, так что тут ничего больше менять не нужно. Правда при установке PostgreSQL 10 может измениться TCP порт, так что это нужно будет изменить в строке подключения.

Теперь откроем файл SampleDataController.cs. Внутри этого файла мы использовали старый код доступа к БД. Его нужно изменить, что бы использовать новый. Во первых переименуем его в OrdersController.cs. Теперь этот файл будет выглядетьс ледующим образом:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using AngularSpa.ViewModels;
using SpaDatasource.Interfaces;
using SpaDatasource.Entitites;

// For more information on enabling Web API for empty projects, visit https://go.microsoft.com/fwlink/?LinkID=397860

namespace AngularSpa.Api
{
    [Route("api/[controller]")]
    public class OrdersController : Controller
    {
        ISpaDatasource _SpaDatasource = null;

        public OrdersController(ISpaDatasource ds)
        {
            _SpaDatasource = ds;
        }

        [HttpGet]
        public OrderInfo Get()
        {
            OrderInfo order = null;

            try
            {
                _SpaDatasource.Open();
                Order o = _SpaDatasource.Orders().LastOrDefault();

                if(o != null)
                {
                    order = new OrderInfo
                    {
                        Id = o.Id,
                        Time = o.Time,
                        UserId = o.UserId,
                        EmailAddress = o.EmailAddress,
                        Description = o.OrderDescription,
                        Currency = o.Currency
                    };
                }
            }
            finally
            {
                _SpaDatasource.Close();
            }

            return order;
        }

        [HttpPost]
        public OrderInfo Post([FromBody]OrderInfo order)
        {
            try
            {
                Order o = new Order
                {
                    Time = DateTime.Now,
                    UserId = order.UserId,
                    EmailAddress = order.EmailAddress,
                    OrderDescription = order.Description,
                    Currency = order.Currency
                };

                _SpaDatasource.Open();
                _SpaDatasource.InsertOrder(o);

                order.Id = o.Id;
            }
            finally
            {
                _SpaDatasource.Close();
            }

            return order;
        }
    }
}

Как видите, я оставил только два метода, которые используються в проекте и убрал неиспользуемые.

В связи с тем, что мы переименовали контроллер, у нас измениться и Angular часть проекта а так же класс для передачи данных от серера к представлению Angular. Начнем это исправлять.

Во первых класс TestData больше не используется и вместо него у нас OrderInfo. Так что переименуем файл TestData.cs в папке ViewModels в OrderInfo.cs. Так же переименуется и класс внутри файла. А теперь добавим новый файл с именем UserInfo.cs. Внутри этого файла мы определим класс для работы с пользователями.

Итак, файл UserInfo.cs

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Threading.Tasks;

namespace AngularSpa.ViewModels
{
    public class UserInfo
    {
        [Display(Description = "Record #")]
        public long Id { get; set; }

        [Required]
        [StringLength(30, MinimumLength = 6)]
        [Display(Description = "Username", Name ="Username", Prompt="Username")]
        public string Username { get; set; }

        [Required]
        [StringLength(100, ErrorMessage = "The {0} must be at least {2} characters long.", MinimumLength = 6)]
        [DataType(DataType.Password)]
        [Display(Description = "Password", Name = "Password")]
        public string Password { get; set; }

        [StringLength(30, MinimumLength = 6)]
        [Display(Description = "Full Name", Name ="FullName", Prompt="Full Name")]
        public string FullName { get; set; }

    }
}

Файл OrderInfo.cs

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Threading.Tasks;

namespace AngularSpa.ViewModels
{
    public class OrderInfo
    {
        [Display(Description = "Record #")]
        public long Id { get; set; }

        [Display(Description = "Time when order created.", Name ="Time", Prompt="Time")]
        public DateTime Time { get; set; }

        [Required]
        [Display(Description = "User who create order", Name ="User", Prompt="User")]
        public int UserId { get; set; }

        [Display(Description = "Payment Amount (in dollars)", Name = "Amount", Prompt = "Payment Amount")]
        [DataType(DataType.Currency)]
        public decimal Currency { get; set; }

        [Required, RegularExpression(@"([a-zA-Z0-9_\-\.]+)@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.)|(([a-zA-Z0-9\-]+\.)+))([a-zA-Z]{2,4}|[0-9]{1,3})", ErrorMessage = "Please enter a valid email address.")]
        [EmailAddress]
        [Display(Description = "Email Address", Name = "EmailAddress", ShortName = "Email", Prompt = "Email Address")]
        [DataType(DataType.EmailAddress)]
        public string EmailAddress { get; set; }

        [Display(Description = "Order Description", Name = "Description")]
        public string Description { get; set; }
    }
}

Теперь приступим к изменениям в Angular части проекта.

У нас больше нет SampleData контролера, соотвественно нужно изменить и SampleData Angular Service. Но начнем с файла модели. Переименуем файл в папке /wwwroot/app/models/TestData.ts в /wwwroot/app/models/OrderInfo.ts. Так же изменим его содержимое, что бы отображать последние изменения в проекте:

import { Component } from '@angular/core';

export class OrderInfo 
{
    id: number;
    time: Date;
    userid: number;
    currency: number;
    emailAddress: string;
    description: string;
}

Переименовываем файл SampleData.services.ts в Orders.services.ts. Так же изменяем его внутрености:

import { Injectable } from '@angular/core';
import { Http, Response, Headers } from '@angular/http';
import { Observable } from 'rxjs/Observable';
import { OrderInfo } from '../models/OrderInfo';

@Injectable()
export class OrdersService
{
    private url: string = 'api/orders';

    constructor(private http: Http) { }

    getSampleData(): Observable<OrderInfo>
    {
        return this.http.get(this.url)
            .map((resp: Response) => resp.json())
            .catch(this.handleError);
    }

    addSampleData(orderInfo: OrderInfo): Observable<OrderInfo>
    {
        let headers = new Headers(
        {
            'Content-Type': 'application/json'
        });

        return this.http
            .post(this.url, JSON.stringify(orderInfo), { headers: headers })
            .map((resp: Response) => resp.json())
            .catch(this.handleError);
    }

    private handleError(error: Response | any)
    {
        let errMsg: string;

        if (error instanceof Response)
        {
            const body = error.json() || '';
            const err  = body.error || JSON.stringify(body);
            errMsg = `${error.status} - ${error.statusText || ''} ${err}`;
        } else
        {
            errMsg = error.message ? error.message : error.toString();
        }

        console.error(errMsg);

        return Observable.throw(errMsg);
    }
}

Следующий файл для изменения — about.component.ts. В нем нужно отобразить изменения в нашем классе модели а так же изменившейся сервис. Новый файл выглядит следующим образом:

import { Component, OnInit } from '@angular/core';
import { OrdersService } from './services/Orders.services';
import { OrderInfo } from './models/OrderInfo';

@Component
({
    selector: 'my-about',
    templateUrl: '/partial/aboutComponent'
})

export class AboutComponent implements OnInit
{
    orderInfo: OrderInfo = null;
    errorMessage: string;

    constructor(private ordersService: OrdersService) { }

    ngOnInit()
    {
        this.getOrder();
    }

    getOrder()
    {
        this.ordersService.getSampleData()
            .subscribe((data: OrderInfo) => this.orderInfo = data, error => this.errorMessage = <any>error);
    }

    addOrder(event: Event):void
    {
        event.preventDefault();

        if (!this.orderInfo)
            return;

        this.ordersService.addSampleData(this.orderInfo)
            .subscribe((data: OrderInfo) => this.orderInfo = data, error => this.errorMessage = <any>error);
    }
}

Следующий файл на изменение — app.module.ts. тут нужно исправить две строчки:

Первая строка

import { SampleDataService } from './services/SampleData.services';

заменить на

import { OrdersService } from './services/Orders.services';

И еще одну:

providers: [SampleDataService, Title, { provide: APP_BASE_HREF, useValue: '/' }],

заменить на

providers: [OrdersService, Title, { provide: APP_BASE_HREF, useValue: '/' }],

И последнйи файл, которы йнужно исправить – это AboutComponent.cshtml. Снова, нам нужно отобразить изменения произошедшие в проекте:

  • новая модель данных
  • изменить поля ввода под новую модель данных
  • изменить имена функций для запроса ис охранения заказа

Новый файл выглядит следующим образом:

 
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@addTagHelper "*,AngularSpa"
@model AngularSpa.ViewModels.OrderInfo

@{
    ViewData["Title"] = "About";
}
<h2>@ViewData["Title"].</h2>
<h3>@ViewData["Message"]</h3>

<p>Examples of Angular 4 data served by ASP.Net Core Web API:</p>
<form #testForm="ngForm">
    <div *ngIf="orderInfo != null">
        <div class="row">
            <div class="col-md-6">
                <div class="panel panel-primary">
                    <div class="panel-heading">Data Entry</div>
                    <div class="panel-body">
                        
                        <tag-di for="UserId"></tag-di>
                        <tag-di for="Currency"></tag-di>
                        <tag-di for="EmailAddress"></tag-di>
                        <tag-di for="Description"></tag-di>

                        <div class="panel-footer">
                            <button type="button" class="btn btn-warning" (click)="addOrder($event)">Save</button>
                        </div>
                    </div>
                </div>
            </div>
            <div class="col-md-6">
                <div class="panel panel-primary">
                    <div class="panel-heading">Data Display</div>
                    <div class="panel-body">

                        <tag-dd for="Id"></tag-dd>
                        <tag-dd for="UserId"></tag-dd> 
                        <tag-dd for="Currency" pipe="currency:'USD':true:'1.2-2'"></tag-dd>
                        <tag-dd for="EmailAddress"></tag-dd> 
                        <tag-dd for="Description"></tag-dd> 

                    </div>
                    <div class="panel-footer">
                        <button type="button" class="btn btn-info" (click)="getOrder()">Get Last Record</button>
                    </div>
                </div>
            </div>
        </div>
    </div>
</form>

<div *ngIf="errorMessage != null">
    <p>Error:</p>
    <pre>{{ errorMessage  }}</pre>
</div>

Если сейчас собрать и запустить проект, то перейдя на страницу About (в следующей статье мы дадим ей название получше) мы ничего не увидим. Это потому что в БД нет записей и Angular директива *ngIf=»orderInfo != null» делает нашу форму невидимой.

Я добавил в БД одного пользователя и один тестовый заказ вручную. После этого формы ввода и отображения появились.

 

Результаты работы в Internet Explorer

Результаты работы в Internet Explorer

На этом этапе и закончим. Статья получилась обьемной, поэтому следующие темы будут расмотрены в следующей статье. А именно:

  • Добавление поддержки ASOS в проект для авторизации при помощи токенов OpenID / OAuth2
  • Реализуя кода для авторизации и создания access_token и refresh_token для работы с Web API
  • Реализую страницы авторизации и добавления новго пользователя в Angular 4
  • Изменение структуры проекта, что бы сделать его более завершенным.

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

Исходный код

Полностью готовый Visual Studio 2017 проект для четвертой части можно загрузить тут: Visual Studio 2017 ASP.NET MVC Core + Angular SPA Project.

Все части серии

Single Page Application : ASP.NET MVC .NET Core + Angular 4. Часть 1.
Single Page Application : ASP.NET MVC .NET Core + Angular 4. Часть 2.
Single Page Application : ASP.NET MVC .NET Core + Angular 4. Часть 3.
Single Page Application : ASP.NET MVC .NET Core + Angular 4. Часть 4.
Single Page Application : ASP.NET MVC .NET Core + Angular 4. Часть 5 [читаете сейчас].
Single Page Application : ASP.NET MVC .NET Core 2 + Angular 5. Часть 6.

P.S. Эта статья вышла с большой задержкой, связанной с моей большой загрузкой на основной работе. Надеюсь что смогу избежатьв дальнейшем таких больших перерывов. И следующую стью написать в течении следующих 1-2 недели.

2 комментария

  1. Вся теория, полученная по книгам ранее, сломана: Модель — класс, описывающий Объект, а так же его методы, например, объект User с методами Get(), Add(), Delete(), List() и все это в одном классе. А теперь получается:
    — один класс для описания одного Объекта (модель)
    — один или несколько классов для перечисления всех методов всех объектов (интерфейс)
    — один или несколько классов для описания всех унаследованных методов из интерфейса
    Касаемо установления соединения не совсем понятно, получается в startup.cs лишняя 32 строка
    string connString = Configuration.GetConnectionString(«SpaDbConnection»);
    Получается что при каждом запросе к базе постоянно создается новое подключение с лимитом в 30 секунд, которое в дальнейшем закрывается принудительно?
    Часть вызовов синхронны (SpaDatasource) и часть асинхронны (SpaUserManager).
    С подключением к БД надо что-то придумать …

    1. Вся теория, полученная по книгам ранее, сломана: Модель – класс, описывающий Объект, а так же его методы, например, объект User с методами Get(), Add(), Delete(), List() и все это в одном классе. А теперь получается:
      – один класс для описания одного Объекта (модель)
      – один или несколько классов для перечисления всех методов всех объектов (интерфейс)
      – один или несколько классов для описания всех унаследованных методов из интерфейса

      Хмм, можно конечно делать полный класс со всеми методами, но это будет неудобно.
      К примеру класс User с методами Add, Delete, List, etc. Что бы работать с БД ему нужнолибо делать все реализацию доступа к БД внутри либо использовать стороний класс.
      Снова, без интерфейсов тут будет так все туго завязано одно на другом, что это станет со временем очень тяжело изменять, так как даже мпленькое изменение будет затрагивать большоек оличество связей между классами.
      Интефесы вводят контракты, которые отлично разраничивают зоны видимости и создают сладо связанные классы. Такие классы менять намного проще.

      Мне кажеться чем проще класс, тем проще его сопроваждать и изменять.
      Если класс очень большой — это большая головная боль.
      Лучше иметь 10 прсотых классов, слабо связаных между собой через интерфейсы, чем один большой монолит.
      Это все равно что модель танка отлитая из метала и собранная из элементов лего. Какую из них проще будет изменить?

      Касаемо установления соединения не совсем понятно, получается в startup.cs лишняя 32 строка
      string connString = Configuration.GetConnectionString(“SpaDbConnection”);
      Получается что при каждом запросе к базе постоянно создается новое подключение с лимитом в 30 секунд, которое в дальнейшем закрывается принудительно?

      Эта строк апросто возращает сроку с информация о соединениии к БД.
      Она вызывается только раз. Затем это используется для создания обекта доступа к БД SpaDatasource.
      Новое подключение создается только в момент вызова SpaDatasource::Open()
      И закрывается при вызове SpaDatasource::Close()
      Таймаут нужен что бы следить, как долго выполняется SQL запрос и обрывать принудительно те, которые работают слишком долго.
      Что бы не расходовать зря ресурсы сервера.

      В кратце как создается соединение к БД:
      — пользователь послал запрос на сервер
      — ASP.NET создает обьект контроллера
      — В параметре конструктора контролера указан интерфейс обекта доступа к БД, который мы зарегестрировали в ServiceManager
      — ServiceManager находит соотвествие между интерфейсом и конкретным классом, которы йего реализует
      — Создается класс (для ISpaDatasource это SpaDatasource) и интерфейс на класс передается в качестве параметра конструктора контроллера
      — Котролер внутри метода обработки запрос использует интерфейс что бы вызвать ISpaDatasource::Open и именно в этот момент происходит установка соединения к БД
      — Далее делаем что нам нужно, это может занять как 1 секунду так и 60, 120, 600 секунд. Соединение не закроется пока мы не вызовем ISpaDatasource::Close. Timeout нужен только для выполнения SQL запроса
      — Вызываем ISpaDatasource::Close и разрушаем соединени

      Т.е. на каждый запрос к серверу будет создаваться свой обьект соединения к БД. Это значит что в некоторые моменты времени мы можем одновременно иметь несколько соединений к БД из разных запросов.
      Это все нормально работает, все решается на уровне драйвера БД.
      Вот если бы у нас был один обьект на все соединения — тогда это наша задача обеспечить безопасную многопоточную работу.

      Часть вызовов синхронны (SpaDatasource) и часть асинхронны (SpaUserManager).

      Я это исправлю 🙂

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

Ваш e-mail не будет опубликован.