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

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

724

А для того, чтоб не писать еще один чатиксоц. С выходом новейшей версии 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. Может быть в финальной версии 2015 это будет пофикшено. Структура шаблонов проектов и рисунки снова запутаны — то что на нашем типе проекта нет значка vNext совершенно не означает, что наш проект не буде на новейшей версии.

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

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

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

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

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

Пройдемся по новшествах.

Даже для тех, кто не знаком с синтаксисом это изменение будет полностью удобным — конфигурации стали короче и понятнее. Никаких xml-конфигов. Сейчас все конфигурации в json. Файл проекта остался в xml, сейчас расширение файлов проекта — *.kporj раз это проект что выполняется на новеньком рантайме — K runtime.
Вначале в нем одна строка — «sources»: [ «src», «test» ] global.json — конфигурационный файл для всего солюшена.
Так же можно указать специфичный путь к 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 пакеты и опции проекта.

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

Архитектура, фронт-енд. Начинаем кодить.
Структуру проекта разумно сделать последующей: все статические файлы (одностраничный веб-сайт на AngularJs) помещаем в папку wwwroot, а для бек-енда сделаем новейший контроллер, который будет выступать как API для веб-сайта. Кстати в новейшей версии ASP.NET контроллеры MVC и WebAPI больше не различаются.
В app-e у меня находятся все вьюшки, контроллеры, сервисы angular. Я по привычке создаю в wwwroot страницу index.html и папку app. А в 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; }
}
}

Токены будем выдавать при первом обращении к страничке и хранить в cookies. Ну и для того, чтоб информация о пройденных уровнях юзера — добавим таблицу Progress где будут хранится токен юзера и номер его крайнего пройденного задания.
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, будет описано ниже)
Запустил, ничего не заработало Копируем миграционный файл в студию 2015. Оказалось, что в чистом виде скопировать нельзя, так как поменялись некие наименования классов, способов. А за одно и выяснить, работает ли для миграций обратная сопоставимость. Но по сколько какие-то передвижения там уже есть, то можно испытать через этот же костыль добавить свои таблицы в базу. Но для 2 обычных таблиц это фиксится за 2 минутки. Итак, быстренько создаем приложение в студии 2013, добавляем передвижения, добавляем такие же классы в модель.
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 клика в студии и все. Вылетала внутренняя ошибка (500) еще на какой-то самой ранешней стадии и даже не выходило поглядеть из-за чего же. В конце концов начал находить способом прибавления функциональности по чуток-чуток. Начал пробовать хостить как можно наиболее базисную функциональность. Не здесь-то было… Наверняка эти пробы я запомню на всю жизнь. Оказалось, что все падает из-за: Поначалу я решил, что какая-то функционально еще не поддерживается. Чтоб захостить веб-сайт в Azure я издержал в 5 раз больше времени чем на всё остальное. Задумывался может какая-то неувязка с базой либо миграциями, но снова же на обычных примерах все работало. Казалось бы, все просто.
Array.Empty<Finisher>()
Я так до сих пор и не сообразил — это (Empty<>) новейший способ из Core .NET либо еще откуда-то? И почему Azure не может с ним работать (быстрее всего версия фреймворка на Azure просто не знает, что это).

Выводы.
Естественно есть еще моменты, которые сыроваты. Но я этого и ждал. Достаточно не достаточно пока написано и задокументировано что и как применять. Остался ряд открытых вопросцев: В общем мне понравилось. Так же заместо ожидаемых пары часов писать пришлось существенно подольше.

Буду ли передвижения работать с nuget manager console, а не с обыкновенной консоли как на данный момент?
Что разумно добавлять в NPM пакеты, не считая grunt?
Что может быть в Dependencies не считая NPM и Bower пакетов?
Почему Array.Empty<> работает локально, но не работает в Azure?

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

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

UPD
Напишу еще раз:
habrahabr.ru код не несет никакой практической ценности, не отрефакторен, написан на коленке. !