Митап .Net разработчиков TomskDotNet#1

29 ноября в Точке Кипения состоялся первый томский митап .NET программистов :)
Организацией, рекламой, фирменными печеньками, сладостями, кока-колой и прочими безобразиями занималась компания МЦЦ Томск (в том числе и я :)).

Что же всё таки было? А вот что:

Видео и презентации по ссылкам выше :)

Всем знаний — и присоединяйтесь к нам 31 января, на TomskDotNet #2!

How to make Swagger correctly handle file uploads (IFormFile) in Asp.Net Core

I love Swagger (OpenAPI). It’s a really nice way to share your API definitions, and it is so easy to integrate it into ASP.Net Core applications, that there’s literally no excuse not to use it.
The only glitch in Swagger’s integration is the lack of IFormFile support. So, if you’d like to upload a file you typically write the following action:

[HttpPost()]
public IActionResult UploadFile(IFormFile file)

And that’s how it’s gonna be displayed in Swagger-UI (the real problem is, of course, in wrong swagger.json definition). As you could see, there’s everything but an ability to upload a file (actually, Swagger exposed all properties of IFormFile, which is completely useless).
Swagger

There’s a nice discussion on Stackoverflow regarding this subject, and a couple of answers that solve the problem either via hardcoding or putting an attribute on an action. That didn’t sound like a generic solution for me, so I went down and implemented an Operation Filter that fixes the issue without any additional work.
The integration is quite simple (as with any other Swagger filter)

services.AddSwaggerGen(c => c.OperationFilter<FileUploadOperation>());

And here’s the actual filter. It handles IFormFile action parameters, IFormFile within other classes and even arrays(or any IEnumerables) of IFormFile.

    /// <summary>
    /// adds an ability to upload files via Swagger (and autogenerated js client)
    /// </summary>
    public class FileUploadOperation : IOperationFilter
    {
        public void Apply(Operation operation, OperationFilterContext context)
        {
            if (context.ApiDescription.HttpMethod != "POST")
                return;

            if (operation.Parameters == null)
                operation.Parameters = new List<IParameter>();

            var isFormFileFound = false;

            //try to find IEnumerable<IFormFile> parameters or IFormFile nested in other classes
            foreach (var parameter in operation.Parameters)
            {
                if (parameter is NonBodyParameter nonBodyParameter)
                {
                    var methodParameter =
                    context.ApiDescription.ParameterDescriptions.FirstOrDefault(x => x.Name == parameter.Name);
                    if (methodParameter != null)
                    {
                        if (typeof(IFormFile).IsAssignableFrom(methodParameter.Type))
                        {
                            nonBodyParameter.Type = "file";
                            nonBodyParameter.In = "formData";
                            isFormFileFound = true;
                        }
                        else if (typeof(IEnumerable<IFormFile>).IsAssignableFrom(methodParameter.Type))
                        {
                            nonBodyParameter.Items.Type = "file";
                            nonBodyParameter.In = "formData";
                            isFormFileFound = true;
                        }
                    }
                }
            }



            //try to find IFormFile parameters of method
            var formFileParameters = context.ApiDescription.ActionDescriptor.Parameters
                .Where(x => x.ParameterType == typeof(IFormFile)).ToList();
            foreach (var apiParameterDescription in formFileParameters)
            {
                operation.Parameters.Add(new NonBodyParameter
                {
                    Name = apiParameterDescription.Name,
                    In = "formData",
                    Description = "Upload File",
                    Required = true,
                    Type = "file"
                });
            }

            if (formFileParameters.Any())
            {
                foreach (var propertyInfo in typeof(IFormFile).GetProperties())
                {
                    var parametersWithTheSameName = operation.Parameters.Where(x => x.Name == propertyInfo.Name);
                    operation.Parameters.RemoveRange(parametersWithTheSameName);
                }
            }


            if (isFormFileFound)
                operation.Consumes.Add("multipart/form-data");
        }
    }

Комфортная Android-like верстка в iOS (XibFree)

Давайте признаем очевидное. Верстка экранов в iOS ужасна. Сравнивая с html, с WPF, с Android, с WinPhone — везде iOS проигрывает.
Верстка в iOS исторически напоминала WinForms (с аналогом якорей-Anchor’ов в виде AutoresizingMask). Однако WinForms уже давно отошло в прошлое, а iOS всё живет и живет :)
Да, на смену AutoresizingMask пришли Constraints, но работа с ними до жути неудобна, дизайнер ненаглядный, а результат работы — нечитаемый уже через неделю после создания.

А вообще, помимо удобства, наибольшей проблемой, конечно, становится динамическая верстка элементов. Если мы пришли из мира WPF/Android, то скучать по простейшему StackLayout/LinearLayout будем очень-очень сильно.
Возьмем например типичную задача: верстка элементов в строчку, при этом некоторые элементы могут быть спрятаны.

<stacklayout>
    <button></button>
    <button Visibility="Collapsed"></button>
    <button></button>
</stacklayout>

Чтобы задать такую верстку в iOS — придется изрядно помучаться. В случае со «старым» AutoresizingMask — это и вовсе невозможно, и придется разруливать координаты кнопок руками при каждом изменении видимости кнопок.
В случае «нового» и «продвинутого» LayoutConstraints — это, конечно, возможно, но намного сложнее и куда менее очевидно, чем xml-верстка.
Continue reading

Oneliner: VerificationException: TaskAwaiter<> violates the constraint of type parameter ‘TAwaiter’ при использовании async в .net 4

Если при использовании async/await в .NET 4.0 вы видите что-то невнятное вроде

System.Security.VerificationException: Method System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1[Cloud.API.TreeResult].AwaitUnsafeOnCompleted: type argument ‘Microsoft.Runtime.CompilerServices.TaskAwaiter`1[Cloud.API.TreeResult]’ violates the constraint of type parameter ‘TAwaiter’.

проверьте, во всех ли проектах одинаковая версия пакета Microsoft.Bcl (обновить всё до последней версии можно командой Update-Package Microsoft.Bcl).

О стартапе-ловушке, или Роберт Мартин хочет нам навредить

Сделал перевод ответа Роба Эштона на заметку Роберта Мартина «О стартапе-ловушке».

После появления перевода оригинальной статьи дяди Боба не мог не ответить переводом зацепившего ответа :) Тема моя любимая — про тесты.

Не стесняйтесь плюсануть на хабре (http://habrahabr.ru/post/172039/), если понравится :)

Mono for android и русские имена пользователей в windows

Пробовал на выходных поиграться с Mono For Android, и внезапно оказалось, что наличие в имени моей учетной записи кириллических символов вносит некоторые проблемы :)

Проблемы в основном связаны с конвертациями юникода-cp1251 в путях к файлам и их решение достаточно тривиально, и тем не менее, сохраню пару пунктов на будущее:

  • НЕ УСТАНАВЛИВАЙТЕ Mono For Android ПОД ПОЛЬЗОВАТЕЛЯМИ С КИРИЛЛИЦЕЙ В ИМЕНИ! :) Это самый простой путь и остальные шаги в этом случае не понадобятся
  • Проблема с путём к Android-SDK. По умолчанию он располагается в c:/Users/%USERNAME%/AppData/Local/Android/android-sdk/. Проще всего скопировать его в корень диска и изменить путь в Visual Studio -> Tools -> Options -> Mono For Android -> Android SDK Location
  • Проблема с путём к образам эмулируемых устройств. Образы устройств, которые запускаются в эмуляторе хранятся в папкахc:/Users/%USERNAME%/.android/avd/%DEVICENAME%. Пути к этим папкам прописываются в файлах c:/Users/%USERNAME%/.android/avd/%DEVICENAME%.ini. Папку устройства скопировать куда-нибудь в «нормальный» путь (без русских букв) и поменять путь в ini-файле. Несмотря на то, что в пути к самим ini файлам тоже есть русские буквы, это, как ни странно, к проблемам не приводит :)

Всем удачи с Mono for android!

T4MVCJS отрефакторен и выложен на codeplex

Недавно дошли руки до выкладывания T4MVCJS в opensource. Распространение исходников в зип-архиве показалось слегка устаревшей методикой и мы переехали на codeplex :)

Попутно было слегка отрефакторено использование T4MVC, вместо простой «копипасты» теперь используется оригинальный исходник с вырезанными из него строками, отвечающими за генерацию T4MVC-хэлперов. Таким образом легко и просто используется весь парсинг, осуществляемый T4MVC, и обновление до новых версий будет представлять куда меньше проблем (скопипастить файл, выкинуть 400 подряд идущих строк — вуа-ля :)).
Помимо эстетического удовлетворения это позволило с лёгкостью обрабатывать MVC Area (предыдущая версия, этого не умела, за репорт этого бага спасибо Брайану Бетти).

Заодно я задумался о проблеме существования двух экшенов с одинаковыми именами — в Javascript перегрузка функций, к сожалению, недоступна. В результате на свет появляются экшены Edit, Edit1, Edit2, etc. :) Если кто-нибудь предложит более адекватное решение проблемы — я бы с удовольствием его обсудил :)

P.S. в качестве системы контроля версий T4MVCJS используется Mercurial, так что при желании внести изменения — форкайте с удовольствием :)

P.P.S. На момент изначальной публикации поста ареи-таки не работали. Начиная с версии 1.0.10 всё ок.

Nuget пакет для JsValidator’a

Наконец-то создан nuget-пакет для JsValidator‘a, о котором я не так давно писал.
Теперь интеграция проверки яваскрипта в любой проект займет минимум времени: Install-Package JsValidator в nuget-консоли — это всё, что нужно. Название пакета, как нетрудно догадаться — JsValidator.

Пакет делает всё то, что раньше приходилось делать руками: скачивает бинарники валидатора, создает тестовый конфиг, прописывает себя в post-build-events.
При удалении пакета всё, где пакет наследил, аккуратненько удаляется.

По факту окончания разработки JsValidator’a написан анонс проекта на Хабрахабре. Желающие приглашаются к обсуждению в любом из источников :)