Pull to refresh

Пишем с нуля квест на ASP.NET 5 (vNext) и Angular.js

Reading time8 min
Views47K
С выходом новой версии ASP.NET хочется попробовать, какая же она на практике. А для того, чтобы не писать еще один чатик\соц. сеть\блог..., для пилотного проекта выберем логический квест — и фреймворк посмотрим, и поиграть можно.
Результат:
сорсы на гитхабе для тех, кому интересно поиграться с новым ASP.NET
линк на квест для тех, кому интересно что получилось или потратить свое время на еще один логический квест.



Предварительные требования


Для работы с новой версией ASP.NET нужна Visual Studio 2015 — на данный момент доступна версия Preview, скачать можно здесь:
www.visualstudio.com/en-us/downloads/visual-studio-2015-downloads-vs.aspx
Никаких проблем с инсталляцией параллельно с другими версиями студии быть не должно.
Фактически, никакого другого софта кроме студии для базовой разработки на .NET стеке не нужно.

Создаем проект


Для создания нового ASP.NET приложения используем, как всегда, — File->New->Project (кстати меню в новой студии опять сделали с нормальным шрифтом, а не ВСЕ КАПСОМ и лично мне оно сейчас кажется непривычным)
Выбираем тип проекта ASP.NET Web Application. Структура шаблонов проектов и картинки опять запутаны — то что на нашем типе проекта нет значка vNext совсем не значит, что наш проект не буде на новой версии. Возможно в финальной версии 2015 это будет пофикшено.

Если вы хотите добавить ваш проект в систему контроля версий — можно поставить галочку внизу (Add to source control).

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


При создании проекта, если была поставлена отметка о добавлении в систему контроля версий — можно указать какая система версия нас интересует. Поддерживается TFS и Git.


При первом пуше (если используется Git можно сразу же указать внешний репозиторий)


Что нового в шаблонном проекте


Наверное для .NET программиста что впервые создал ASP.NET 5 проект нового будет довольно много. Даже если взглянуть на структуру стартового проекта:


Пройдемся по новшествах.
  • Никаких xml-конфигов. Теперь все конфигурации в json. Даже для тех, кто не знаком с синтаксисом это изменение будет абсолютно комфортным — конфигурации стали короче и понятнее. Файл проекта остался в xml, теперь расширение файлов проекта — *.kporj если это проект что выполняется на новом рантайме — K runtime.
  • global.json — конфигурационный файл для всего солюшена. Изначально в нем одна строчка — «sources»: [ «src», «test» ]
    Которая указывает где находятся сорсы. Так же можно указать специфический путь к nuget пакетам.
  • debugSettings.json — настройки Run/Debug для отдельно взятого проекта.
  • wwwroot — корневая папка сайта, предназначена для статических файлов (html, css, img, js).
  • Dependencies — если честно, понял не до конца, похоже, что NPM и Bower зависимости выделены отдельно в эту папку.
  • References — теперь в референсах не библиотеки, а пакеты, довольно удобно для навигации и поиска.
  • bower.json — пакеты Bower (front-end пакеты).
  • config.json — что-то типа прошлого Web.config, только теперь короткий и понятный.
  • gruntfile.js — конфигурация для Grunt (инструмент для сборки javascript).
  • package.json — пакеты NPM.
  • project.json — один из главных файлов проекта, включает nuget пакеты и настройки проекта.

Первое впечатление — набросали все что можно в кучу. Возможно где-то так оно и есть. С другой стороны просто и быстро можно добавить любой пакет, увидеть все зависимости. Хотя пока не до конца понятно, например, что кроме Grunt стоит включать в пакеты NPM. И, опять же, возможно в финальной версии все будет как-то аккуратней, пока довольно интересно видеть тулы для веб разработки с других платформ внутри Visual Studio.

Начинаем кодить. Архитектура, фронт-енд.


Структуру проекта логично сделать следующей: все статические файлы (одностраничный сайт на AngularJs) помещаем в папку wwwroot, а для бек-енда создадим новый контроллер, который будет выступать как API для сайта. Кстати в новой версии ASP.NET контроллеры MVC и WebAPI больше не различаются.
Я по привычке создаю в wwwroot страничку index.html и папку app. В app-e у меня находятся все вьюшки, контроллеры, сервисы angular. А в index.html — ссылки на все js файлы и layout.
По скольку наша главная страница — это index.html, то маршрут (route) по умолчанию в Startup.cs нужно удалить (чтобы, заходя на сайт, мы заходили на index.html, а не на Home/Index).

Добавим стандартную bootstrap-страничку и набор скриптов на index.html
<script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.3.12/angular.min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.3.12/angular-animate.min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.3.12/angular-resource.min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.3.12/angular-route.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/angular-ui-bootstrap/0.12.0/ui-bootstrap-tpls.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/angular-loading-bar/0.6.0/loading-bar.min.js"></script>
<script src="app/app.js"></script>
<script src="app/config.route.js"></script>
<script src="app/start/start.js"></script>
<script src="app/home/home.js"></script>
<script src="app/services/mainquest.js"></script>


а также стартовое представление — home.html и «рабочую» страницу start.html
Детальней можно посмотреть в коммите.

Бек-енд. Модель


Для начала нам нужно где-то хранить наши данные. В нашем проекте уже создан контекст для работы с базой — ApplicationDbContext. Давайте добавим в него модель нашего приложения.
В первую очередь нам нужны сами задания — назовем их QuestTask. Он будет хранить информацию о задании — номер, заголовок, содержание, ответ.
namespace HabraQuest.Models
{
    public class QuestTask
    {
            public int Id { get; set; }
            public string Title { get; set; }
            public string Content { get; set; }
            public int Number { get; set; }
            public string Answer { get; set; }
    }
}


Ну и для того, чтобы информация о пройденных уровнях пользователя — добавим таблицу Progress где будут хранится токен пользователя и номер его последнего пройденного задания. Токены будем выдавать при первом обращении к странице и хранить в cookies.
namespace HabraQuest.Models
{
    public class Progress
    {
        public int Id { get; set; }
        public string Token { get; set; }
        public int TaskNumber { get; set; }
    }
}


В общем наша модель готова — добавим эти свойства в контекст.
public class ApplicationDbContext : IdentityDbContext<ApplicationUser>
    {
        public DbSet<QuestTask> Tasks { get; set; }
        public DbSet<Progress> Progress { get; set; }
...


И тут я понял, что новый EntityFramework пока не поддерживает миграции, а в коде для создания базы стоит костыль.
(Потом я узнал, что все же миграции есть и можно их попробовать через команды k ef migration, будет описано ниже)
Но по сколько какие-то миграции там уже есть, то можно попробовать через этот же костыль добавить свои таблицы в базу. А за одно и узнать, работает ли для миграций обратная совместимость. Итак, быстренько создаем приложение в студии 2013, добавляем миграции, добавляем такие же классы в модель. Копируем миграционный файл в студию 2015. Оказалось, что в чистом виде скопировать нельзя, так как поменялись некоторые названия классов, методов. Но для 2 простых таблиц это фиксится за 2 минуты. Запустил, ничего не заработало
A relational store has been configured without specifying either the DbConnection or connection string to use.

Погуглил, посмотрел на код, понял, что теперь для миграций надо еще реализовывать IMigrationMetadata. Набросал реализацию. Опять ошибка.
Посмотрел код еще раз…

После 5-ти таких итераций понял, что в одном месте у меня таблица называется dbo.Progress в другом просто Progress. Написал одинаковое название и все заработало. Приятно, что фактически миграции можно писать самому, хотя, конечно же, такие вещи лучше делать стандартными инструментами.
Результат игры с миграциями можно посмотреть на гитхабе.
Итак у нас есть фронт-енд и модель. Осталось создать API.

Бек-енд. Controller-ы


! код не несет никакой практической ценности, не отрефакторен, написан на коленке.
! в основном хотелось попробовать новшества C# 6, типа инициализации свойств, оператор?..
Назовем наш контроллер по работе с заданиями квеста — QuestController и реализуем метод на get-запрос по проверке ответа и post запрос для функциональности «начать с начала».

[AllowAnonymous]
    [Route("api/[controller]")]
    public class QuestController : Controller
    {
        // GET api/MainQuest
        [HttpGet]
        public MainQuestViewModel Get(string token, string taskNumberString, string answer)
        {
            // реализация...
        }

        // POST api/MainQuest
        [HttpPost]
        public void Post(string token, bool startAgaing)
        {
            if (startAgaing)
            {
                using (var db = new ApplicationDbContext())
                {
                    var progress = db.Progress.Single(_ => _.Token == token);
                    progress.TaskNumber = 1;
                    db.SaveChanges();
                }
            }
        }
    }


и контроллер для статистики (get — сколько людей просмотрело\прошло текущее задание, post — добавить свое имя в таблицу финишировавших):
[AllowAnonymous]
    [Route("api/[controller]")]
    public class StatisticsController : Controller
    {
        // GET api/Statistics
        [HttpGet]
        public StatisticsResult Get(string token)
        {
            using (ApplicationDbContext db = new ApplicationDbContext())
            {
                var current = db.Progress.FirstOrDefault(_ => _.Token == token);
                int taskNumber = current?.TaskNumber ?? 1; // пример новой фичи C#

                return new StatisticsResult
                {
                    Watched = db.Progress.Count(_ => _.TaskNumber >= taskNumber),
                    Done = db.Progress.Count(_ => _.TaskNumber > taskNumber)
                };

            }
        }

        // POST api/Statistics
        [HttpPost]
        public Finisher[] Post(string token, string name)
        {
            //...
        }
    }

    public class Finisher
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public DateTime Time { get; set; }
    }

    public class StatisticsResult
    {
        public string Ok { get; } = "OK";   // пример новой фичи C#
        public int Watched { get; set; }
        public int Done { get; set; }
    }


Для того, чтобы данные с сервера приходили в удобном для javascript camelCase надо добавить следующий код в Startup.ConfigureServices:
services.AddMvc().Configure<MvcOptions>(options =>
            {
                options.OutputFormatters
                           .Where(f => f.Instance is JsonOutputFormatter)
                           .Select(f => f.Instance as JsonOutputFormatter)
                           .First()
                           .SerializerSettings
                           .ContractResolver = new CamelCasePropertyNamesContractResolver();
            });


Изменение модели. EF команды.


После того, как я узнал о новом подходе для миграций, конечно же захотелось его попробовать. Добавим еще одну модель — список людей, что закончили квест и оставили свое имя в конце. Модель довольно проста:
namespace HabraQuest.Models
{
    public class Finisher
    {
        public int Id { get; set; }
        public string Token { get; set; }
        public string Name { get; set; }
    }
}


Команды EF можно запустить с консоли, если подключены
«EntityFramework.SqlServer»: «7.0.0-beta2»,
«EntityFramework.Commands»: «7.0.0-beta2»,

Вот так выглядит окно команд:


Пока не уверен почему, но с первого раза создать миграцию не получилось, возможно из-за моих изменений в конфигурации. Для того, чтобы она заработала пришлось унаследовать контекст от DbContext и создать объект конфигурации внутри OnConfiguring контекста.
protected override void OnConfiguring(DbContextOptions options)
        {
            var efConfiguration = new Microsoft.Framework.ConfigurationModel.Configuration()
                .AddJsonFile("config.json")
                .AddEnvironmentVariables();
            options.UseSqlServer(efConfiguration.Get("Data:DefaultConnection:ConnectionString"));
        }


Но таким способом не подхватились предыдущие миграции, пришло удалить часть сгенерированного кода.
Кстати, задеплоить в azure получилось раза с 10. Хотя до сих пор не ясно в чем были проблемы.

Хостим в Azure… 3 дня


Казалось бы, все просто. Создать сайт в Azure, скачать профиль для публикации, 2 клика в студии и все. Не тут-то было… Наверное эти попытки я запомню на всю жизнь. Чтобы захостить сайт в Azure я потратил в 5 раз больше времени чем на всё остальное. Вылетала внутренняя ошибка (500) еще на какой-то самой ранней стадии и даже не получалось посмотреть из-за чего. Сначала я решил, что какая-то функционально еще не поддерживается. Начал пробовать хостить как можно более базовую функциональность. Но все другие примеры задеплоить получалось. Думал может какая-то проблема с базой или миграциями, но опять же на простых примерах все работало. В конце концов начал искать методом добавления функциональности по чуть-чуть. Оказалось, что все падает из-за:
Array.Empty<Finisher>()

Я так до сих пор и не понял — это (Empty<>) новый метод из Core .NET или еще откуда-то? И почему Azure не может с ним работать (скорее всего версия фреймворка на Azure просто не знает, что это).

Выводы.


В общем мне понравилось. Конечно есть еще моменты, которые сыроваты. Но я этого и ожидал. Так же вместо ожидаемых нескольких часов писать пришлось значительно дольше. Довольно мало пока написано и задокументировано что и как использовать. Остался ряд открытых вопросов:
  • Буду ли миграции работать с nuget manager console, а не с обычной консоли как сейчас?
  • Что логично добавлять в NPM пакеты, кроме grunt?
  • Что может быть в Dependencies кроме NPM и Bower пакетов?
  • Почему Array.Empty<> работает локально, но не работает в Azure?

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

Сорсы на гитхабе.
Линк на демо.

UPD
Напишу еще раз:
! код не несет никакой практической ценности, не отрефакторен, написан на коленке.
Tags:
Hubs:
Total votes 19: ↑15 and ↓4+11
Comments35

Articles