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

З виходом нової версії 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-s
! код не несе ніякої практичної цінності, не отрефакторен, написаний на коліні.
! в основному хотілося спробувати нововведення 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: 10 завдання виправлено.

Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.