Пишем с нуля квест на 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-ы


    ! код не несет никакой практической ценности, не отрефакторен, написан на коленке.
    ! в основном хотелось попробовать новшества 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
    Напишу еще раз:
    ! код не несет никакой практической ценности, не отрефакторен, написан на коленке.
    Ads
    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More

    Comments 35

      –2
      Пара вещей, которые меня немного, ну скажем, удивили (?)

      Я, когда пришел в ангуляр, сам некоторое время делал глобальный конфиг, схему роутов и все это заталкивал в один module.config()
      Но через какое-то время пришло озарение, что этот config можно в любом месте делать. Он же как queue, просто добавляются новые шаги в конфигурирование модуля. С тех пор все конфигурации рядом с объектами, которым они принадлежат. Вот пример (правда это из свежего проекта, там 6to5 и ui.router но сути не меняет):

      import {OverviewController} from './controllers/overview_controller.js'
      import {PubNub} from '../../pubnub/index.js'
      export var OverviewPage = angular.module('dashboard.pages.overview', [PubNub.name, 'ui.router']);
      
      OverviewPage.config(function($stateProvider){
        $stateProvider.state(
          'app.dashboard.overview',
          {
            url: '/overview',
            resolve: {},
            views: {
              '@app.dashboard': {
                templateUrl: 'templates/pages/overview/index.html',
                controller: OverviewController,
                controllerAs: 'owCtrl'
              }
            }
          }
        )
      });
      
      
      

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

      Второе это

      mainQuestSvc.submitAnswer().get({ token: vm.userToken, taskNumberString: vm.taskId, answer: vm.answer }, function (result) {
                      if (result) {
                          if (result.isAnswerRight) {
                              ...
                              mainQuestSvc.Statistics().get({ token: vm.userToken }, function (result) {
                                  if (result && result.ok === "OK") {
                                      vm.watched = result.watched;
                                      vm.done = result.done;
                                  } else {
                                      alert('error');
                                  }
                              });
                          ...
                      }
                  });
      


      Такое, да в контроллере, да без промисов…

      image
        0
        По первому — спасибо. Интересный подход.
        По второму — да, тут лучше в код вообще не смотреть, на что я и надеялся :)
        +2
        > using (ApplicationDbContext db = new ApplicationDbContext())
        В контроллере это кажется ужасным.
          –5
          Так то да, но для демонстрации других сторон это нормально.
          Я не особо понял зачем так писать: (_ => _.Token == token)?
          Видел _ там где надо передать делегат с аргументами, но в теле лямбы они не нужны, но это не к статье.

          А так статья не о чем.
          +1
          > Буду ли миграции работать с nuget manager console, а не с обычной консоли как сейчас?
          Так оно и сейчас работает через nuget консоль. Начиная с EF 6.0 так точно. Пишем «Add-Migration», «Update-database»: всё создает, всё обновляет.
            0
            в статье использовалась EF 7.0.0-beta2 и в ней пока можно только с обычной консоли
            0
            как пройти 6й уровень?
              +2
              подсказка 1
              первая ассоциация на номер задания и символ в нем


              подсказка 2
              если ничего не ассоциируется — погуглить целиком все что написано в задании (3 символа)
                –2
                Простите конечно, но я считаю что это совсем не честный вопрос. Не у меня, не у моих знакомы 6.+ не ассоциируется с iPhone.
                я уже перебрал все математические термины, вписывал всякими способами названия и выражения и даже подумал про регулярки.
                Ну не как не айфон…
                  0
                  Извините, тяжело ориентироваться на широкую аудиторию.
                  В начале квеста сказано, что до 10 задания — никакой математики и программирования. Я ожидал, что если ассоциаций нет, то всегда можно нагуглить ответ.
                    0
                    А теперь попробуйте вбить это в яндекс. Представляете какая каша может образоваться в голове?
                    Я это же не «откусанное яблоко».
                    Хотя конечно до этого вопросы понравились.
                    0
                    Спойлер же…
                0
                Раньше писали, что теперь запуск ASP.NET проектов сильно ускорен за счёт того, что проект билдится в RAM, а не в реальные файлы. Стало ли это заметно?
                  0
                  Тяжело ответить потому что проект слишком маленький. Но вообще, я пока не могу сказать точно от чего, но чувствуется, что от момента написания код до момента «пробы» его проходит меньше времени. Скорее всего да, как-то влияет новый подход в сборке. Попробую протестировать как-то на сложных проектах.
                    0
                    Интересно, что быстрее: взять и загрузить сборку с диска или взять и начать компилить множество мелких *.cs файлов?
                    Мне кажется, что первый вариант будет быстрее.
                  +1
                  10 уровень. Кэп, где же ты?
                    0
                    подсказка 1
                    это наверное одно из первых, что учат по программированию со стороны математики


                    подсказка 2
                    системы счисления
                      +1
                      Скрытый текст
                      Значит задание некорректно написано. По-английски так сказать нельзя, правильно будет from base / to base.
                        0
                        спойлер
                        возможно, но со словом base было бы слишком просто, хотелось последние 3 задания сделать сложнее.
                          0
                          12-е задание ахтунг просто :)
                            +1
                            Заголовок спойлера
                            Речь про предлоги же
                            Правильнее будет From * to *
                              0
                              оу, спасибо. Да, согласен.
                      +1
                      С 12 сдаюсь. Уже всю голову сломал.
                        0
                        подсказка 1
                        а и a — разные буквы, хоть и выглядят одинаково.


                        подсказка 2
                        сделайте поиск по странице по букве a
                          +1
                          Отличный квест. Спасибо
                        0
                        На локальном IIS запустить смогли?
                          0
                          пробовал, без предварительных настроек не получилось, возможно надо что-то где-то наконфигурить.
                          0
                          как-то не очевиден ответ на пятый вопрос. Может подскажите?
                            0
                            подсказка 1
                            все написано в задании. и ответ тоже.


                            подсказка 2
                            В прямом смысле — ответ написан в задании. Так и написан, прямым текстом.
                              0
                              странно, я вроде вводил ответ верный ответ, но в итоге получал «неверный ответ». Может я в какой-то букве ошибся. Спасибо за ответ)
                            0
                            Подскажите, пожалуйста, как пройти пятый?
                              0
                              Ой, уже подсказали выше. Сорри.
                              0
                              Подскажите, пожалуйста, как пройти 8й уровень?
                                +1
                                подсказка 1
                                игра


                                подсказка 2
                                место короля в этой игре


                                подсказка 3
                                погуглите задание целиком
                                  0
                                  Спасибо большое! :)

                              Only users with full accounts can post comments. Log in, please.