.NET Разработчик
6.5K subscribers
427 photos
2 videos
14 files
2.04K links
Дневник сертифицированного .NET разработчика.

Для связи: @SBenzenko

Поддержать канал:
- https://boosty.to/netdeveloperdiary
- https://patreon.com/user?u=52551826
- https://pay.cloudtips.ru/p/70df3b3b
Download Telegram
День шестьсот пятьдесят второй. #ЗаметкиНаПолях
Жизненный цикл запроса в
ASP.NET Core MVC 3. Продолжение
Начало
Промежуточное ПО
Маршрутизация

Инициализация контроллера
Наконец запрос попадает во фреймворк MVC. Здесь под фреймворком понимается набор контроллеров с методами действий, фильтров и представлений Razor.
Для начала рассмотрим два важных класса.

Первый называется инициатором действий контроллера (Controller Action Invoker) и наследуется от второго абстрактного класса – инициатора ресурсов (Resource Invoker). Эти классы управляют конвейером MVC (см. картинку ниже). Делегат запроса в промежуточном ПО конечных точек передаёт данные контекста HTTP-запроса и выбранного метода действия в инициатор действий контроллера и его родительский класс. Логика инициатора ресурсов сначала выполняет фильтры авторизации, ресурсов и промежуточного ПО, чтобы понять, нужно ли вообще создавать контроллер. Если нужно, фабрика контроллеров, использует контекстные данные, предоставленные вызывающей стороной, для создания экземпляра контроллера. Затем инициируется поток исполнения соответствующего метода действия этого экземпляра контроллера. Когда метод действия возвращает результат, инициатор ресурсов выполняет результат для генерации ответа.

Фильтры
Фильтры - позволяют внедрять пользовательский код на различных этапах жизненного цикла запроса. MVC «из коробки» включает несколько встроенных фильтров, но мы также можем легко написать свои. Выполнение фильтров управляется инициатором действий контроллера, который вставляет их код в различные этапы конвейера MVC. Их место в жизненном цикле запроса определяется их типом:
1. Фильтры авторизации, ресурсов и промежуточного ПО выполняются перед созданием контроллера. То есть эти фильтры могут решить, должен ли запрос отправляться дальше по конвейеру или его следует прервать (например, при неудачной авторизации).
2. Фильтры действий и результатов действий запускаются до и после выполнения метода действия и результата соответственно.
3. Фильтры исключений могут выполняться в любом месте конвейера.

Фильтры постоянно добавлялись в MVC с ранних версий, поэтому в .NET Core 3 их довольно много. С фильтрами авторизации, действий и исключений всё более-менее понятно. Рассмотрим подробнее редко используемые фильтры ресурсов и промежуточного ПО.

Фильтры ресурсов реализуют интерфейс IResourceFilter, определяющий два метода OnResourceExecuting и OnResourceExecuted. Фильтры ресурсов уникальны тем, что они оборачивают выполнение большей части конвейера MVC. Это первое место для внедрения пользовательской логики после авторизации (OnResourceExecuting) и последнее место перед тем, как MVC вернёт ответ (OnResourceExecuted). В некотором смысле фильтры ресурсов похожи на промежуточное ПО внутри конвейера MVC. С их помощью можно выполнять кэширование или управлять поведением привязки модели.

Фильтры промежуточного ПО позволяют запускать стандартные компоненты промежуточного ПО внутри конвейера MVC. Есть две основные причины их использования:
- повторное использование логики компонента промежуточного ПО;
- предоставление компоненту промежуточного ПО большего объёма данных контекста изнутри MVC, чем это возможно на этапе выполнения промежуточного ПО.
Фильтры промежуточного ПО выполняются после фильтров авторизации, но на той же общей стадии, что и фильтры ресурсов.

Продолжение следует…

Источник:
https://app.pluralsight.com/library/courses/aspnet-core-3-mvc-request-life-cycle/
👍1
День шестьсот пятьдесят третий. #ЗаметкиНаПолях
Жизненный цикл запроса в
ASP.NET Core MVC 3. Продолжение
Начало
Промежуточное ПО
Маршрутизация
Инициализация контроллера

Выполнение метода действия
Метод действия - это любой открытый метод контроллера, который действует как конечная точка обработки входящего запроса. Выполнение метода действия управляется инициатором действий контроллера. Перед выполнением метода действия выполняется привязка модели и фильтры действий, которые могут выполняться как до, так и после метода действия. Сам метод действия создает объект результата. Методы действий не генерируют напрямую ответ для браузера. Они отвечают за предоставление данных ответа и типа ответа. Ответственность за фактическую визуализацию этих данных ответа вынесена в объекты результата действия.

Привязка модели
Привязка модели - это процесс сопоставления данных входящего запроса с параметрами метода действия. Привязка модели использует данные из различных источников HTTP-запроса, таких как URL-адрес, данные формы, заголовки и т.д. Методы действия могут требовать в качестве параметров сложный тип или несколько простых типов. Параметры метода действия также могут влиять на то, какой метод выбирается для обработки запроса.

Процессом привязки модели изнутри управляют три компонента:
1. Провайдеры связывателей моделей (model binder providers) - решают, какой связыватель модели следует использовать для параметров выбранного метода действия.
2. Связыватель модели (model binder) - выполняет работу по заполнению параметров значениями. Существуют разные типы связывателей и провайдеров связывателей для обработки различных типов данных.
3. Провайдеры значений (value providers) - извлекают данные из входящего запроса и передают их связывателю модели во время процесса привязки.

Допустим, у нас есть входящий запрос на обновление продукта, который достигает стадии привязки модели (см. картинку ниже). Инициатор действий контроллера использует фабрику связывателей модели, которая последовательно перебирает все провайдеры связывателей модели, зарегистрированные в приложении. Каждый из этих провайдеров работает с разными типами данных, поэтому каждый проверяет, может ли он предоставить связыватель для заполнения параметров методов действия. В нашем случае встроенный провайдер сложных типов (Complex Provider), создаст связыватель для обработки сложного параметра типа Product. Стоит отметить, что используется первый же провайдер, который сможет предоставить связыватель. Затем связыватель сложных моделей (Complex Model Binder) использует доступные провайдеры значений для извлечения данных из запроса и создания экземпляра типа Product. Он также выполняет валидацию модели, чтобы убедиться, что значения приемлемы и соответствуют атрибутам валидации. После того, как параметры заполнены, они передаются методу действия.

Кроме того, существует два типа компонентов, которые могут предоставлять значения:
1. Провайдеры значений (value providers) предоставляют структурированные данные в виде пар ключ-значение из таких источников, как URL-адрес и данные формы.
2. Средства форматирования ввода (input formatters) обрабатывают более сложные данные тела запроса в стандартных форматах, таких как JSON или XML.

В зависимости от конфигурации приложения и метода действия процесс привязки модели может немного отличаться от приведённого. Также стоит отметить, что, хотя описанный процесс точен концептуально, фактический код не повторяет его дословно. Связывание модели - сложная система. MVC создаёт и кэширует множество различных внутренних компонентов в зависимости от варианта использования.

Продолжение следует…

Источник:
https://app.pluralsight.com/library/courses/aspnet-core-3-mvc-request-life-cycle/
День шестьсот пятьдесят четвёртый. #Оффтоп
Отвлечёмся ненадолго от жизненного цикла запроса в ASP.NET Core. Вчера в нашем чате завязалась дискуссия про базы данных, прелести и недостатки SQL и хранимых процедур. Вот решил вам на выходные дать задачку из собственной практики. Можно рассматривать её как из цикла #ЗадачиНаСобеседовании.

Допустим, у нас есть интернет-магазин (или любой аналогичный каталог товаров и их производителей/поставщиков, скажем, Алиэкспресс). Количество товаров очень большое. Пользователь ищет товар по названию или по категории, и ему выдаётся некоторый набор товаров с их характеристиками и производителем/поставщиком. Предположим для простоты, что из-за большого количества товаров, вся информация в рамках одного поискового запроса собирается в хранимой процедуре и кэшируется во временную таблицу в БД. То есть, если пользователь изменил сортировку или фильтры в текущем поиске, нам не нужно каждый раз извлекать данные из нескольких таблиц, все они уже кэшированы.

Пользователю нужно выдать только определённое количество результатов (разбив на страницы или просто выдавать только ТопХХХ). Пользователь может сортировать результаты (по цене, релевантности, рейтингу и т.п.), применять фильтры или переходить по страницам. Все эти запросы в рамках одного поискового запроса идут к временной таблице, которая создана ранее.

Теперь собственно задача. Производители хотят знать, сколько раз, по каким поисковым запросам и с какими товарами они появлялись на страницах сайта. То есть, если был запрос на «смартфон», который вернул 20000 результатов во временную таблицу, пользователю из них показали 1000 на первой странице и 1000 на второй, а дальше он смотреть не стал. Нам нужно сохранить для статистики данные только тех производителей, которые реально были показаны пользователю (попали в 2000 из 20000). (Да, я специально взял большие числа, а не стандартные 10 на страницу, потому что с 10 товарами решение вполне очевидно и не затратно).

Вопрос: как вы это реализуете? Оцените преимущества и недостатки каждого варианта.

Поскольку я сам с этим столкнулся, мне будет очень интересно узнать ваши версии в комментариях. Как такового правильного ответа тут, наверное, нет.

Жду ваших комментариев. Свою реализацию тоже опишу в комментариях позже.
День шестьсот пятьдесят пятый. #ЗаметкиНаПолях
Жизненный цикл запроса в
ASP.NET Core MVC 3. Окончание
Начало
Промежуточное ПО
Маршрутизация
Инициализация контроллера
Выполнение метода действия

Выполнение результата действия и Отображение представления
Результаты действий отвечают за отображение результата метода действия в ответ, отправляемый клиенту. Результаты действий реализуют интерфейс IActionResult, который определяет метод ExecuteResultAsync, принимающий контекст действия (ActionContext). Этот метод реализуется различными типами результатов действий для отображения различных типов ответов. Допустим, у нас есть метод действия, отвечающий за отправку сообщения «Hello world» клиенту. Метод действия может представить результат по-разному, например:
- ContentResult выведет просто строку "Hello world",
- JsonResult выведет JSON {result: "Hello world"},
- ViewResult выведет HTML страницу с разметкой из соответствующего представления,
- FileResult выведет файл.
Выбрать тип результата можно с помощью вспомогательных методов контроллера, например:
- return View("Hello world") для ViewResult,
- return Content("Hello world") для ContentResult, и т.п.

Отображение представления
Отображение представлений в MVC управляется механизмом представлений Razor (View Engine), который отвечает за поиск и отрисовку представлений, выбранных методами действий. Существует три основных класса управляющих этим процессом:
1. View Engine - управляет процессом поиска представления. Класс реализует IViewEngine, определяющий два метода: GetView и FindView. GetView вызывается первым и пытается найти представление, используя его имя прямой путь к нему. Если представление не найдено, вызывается метод FindView, который использует дополнительную информацию из контекста метода действия.

2. View Engine Result - содержит свойства, представляющие результаты поиска представления:
- IEnumerable<string> SearchedLocations – места, где производился поиск,
- bool Success – успешность поиска,
- IView View – экземпляр найденного представления,
- string ViewName – имя представления.
Соответствующие свойства заполняются двумя методами класса:
- Found() – вызывается при обнаружении представления,
- NotFound() – если представление не найдено.

3. View - фактически отображает разметку Razor и HTML, а также любые данные, предоставленные методом действия. Класс реализует IView, определяющий свойство Path (путь к представлению) и метод визуализации представления RenderAsync. RenderAsync используется MVC для фактического создания ответа, который отправляется клиенту. Разметка Razor и код HTML конвертируются в класс C#, содержащий метод ExecuteAsync (о нём ниже).

Процесс визуализации представления в MVC начинается с вызова вспомогательного метода представления в методе действия контроллера (см. рисунок ниже). За кулисами этот метод генерирует экземпляр ViewResult, который будет выполнен инициатором ресурсов, как и любой другой результат действия. ViewResult просит у механизма представлений Razor найти представление. Механизм возвращает экземпляр класса View Engine Result. Если представление было найдено, экземпляр содержит обнаруженное представление. В противном случае предоставляется список мест, где осуществлялся поиск. Если представление было найдено, вызывается метод ExecuteAsync класса View для отображения ответа на запрос.

Источник: https://app.pluralsight.com/library/courses/aspnet-core-3-mvc-request-life-cycle/
.NET Разработчик pinned «День шестьсот пятьдесят четвёртый. #Оффтоп Отвлечёмся ненадолго от жизненного цикла запроса в ASP.NET Core. Вчера в нашем чате завязалась дискуссия про базы данных, прелести и недостатки SQL и хранимых процедур. Вот решил вам на выходные дать задачку из собственной…»
День шестьсот пятьдесят шестой. #Оффтоп #97Вещей
97 Вещей, Которые Должен Знать Каждый Программист
66. Архитектура не Завершена без Документации
Создание хорошей архитектуры - только половина дела. Необходимо описать её для всех, кто будет её использовать: инженерам, тестировщикам, владельцам продукта, аналитикам и менеджерам.

Стоит потратить время на чёткое документирование архитектуры, так как это может упростить разработку. Документация даёт всем понимание, как будет работать система, определяет общий язык, упрощает обучение новичков. Но как должна выглядеть документация, как сделать её полезной и избежать превращения документирования в обузу?

1. Ориентируйтесь на аудиторию, а не на методологию
Основной целью документации является обмен информацией, а значит любая документация должна быть написана для целевой аудитории – её читателей. Представлений архитектуры очень много: от неформальных прямоугольников и стрелок до UML или Archimate. Правильный формат может оказаться сложной задачей, особенно если вы ориентируетесь на нетехническую аудиторию.

2. Учитывайте разные взгляды
Вы вряд ли найдёте универсальный подход к документации. Разным аудиториям потребуются разные взгляды на архитектуру. План архитектуры для разработчиков будет отличаться от более абстрактного представления для менеджеров. Это хорошо. Предоставление описаний одной и той же архитектуры с разных сторон может стать проверкой прочности вашего дизайна.

3. Определите общий язык
Каждая система содержит свои абстракции, шаблоны и термины. Этот общий язык должен быть где-то чётко определён, иначе каждая аудитория начнёт придумывать свои противоречащие остальным определения.

4. Краткость – сестра таланта
Способность выражать сложные проекты в простой и доступной форме чрезвычайно важна. Длинные сложные документы могут быть пугающими и содержать неактуальный материал, затрудняющий понимание. Стремитесь к тому, чтобы документации было ровно столько, сколько нужно. Если вы не можете описать систему ясно и кратко, вы либо слишком усложняете решение, либо не понимаете его полностью. Отбрасывайте несущественные детали. Архитектурная документация должна объяснять основные концепции и шаблоны, лежащие в основе проекта, а не представлять собой сборник деталей реализации.

5. Схемы переоценены
Во многих случаях документальное оформление проекта приносит пользу архитектору больше, чем кому-либо. Схема - это возможность систематизировать свои мысли, разобраться в решении и научиться ясно его описывать. Просто имейте в виду, что не каждый сможет понять сложную схему. Стремитесь поддерживать согласованный уровень абстракции, контролировать сложность, определять чёткую область видимости и следить за тем, чтобы всё было последовательно помечено.

6. Создайте централизованный репозиторий
Архитектурная документация должна быть легко доступной для широкой аудитории. Подойдет простая вики, желательно с возможностью поиска, управлением версиями и комментариями. Также кто-то должен взять на себя ответственность за организацию структуры, удаление оставленных черновиков и обновление контента. Неформальные договорённости о поддержке актуальности документации обычно приводят к хаосу.

7. Стремитесь к независимой оценке документации
Документация также допускает обзоры, как и код. Рецензирование должно быть совместным процессом. Есть соблазн держать идеи при себе, пока вы не доведёте их до ума. Но чтобы по-настоящему оценить их состоятельность, требуется немного смелости. Нужно опубликовать черновики и ждать отзывов. Это поможет привлечь людей, а также упростит изменение курса разработки, если это необходимо.

Источник: https://www.ben-morris.com/architecture-without-documentation-is-incomplete/
День шестьсот пятьдесят седьмой. #ЧтоНовенького
Новый Опыт Работы с Git в Visual Studio
Git стал встроенной системой контроля версий в Visual Studio 2019, начиная с версии 16.8. Вот некоторые нововведения, которые помогут упростить работу с Git в Visual Studio.

Создание репозитория
Чтобы начать работу с Git, Visual Studio позволяет вам одним щелчком мыши добавить локальный код в Git и GitHub. Диалоговое окно «Создание репозитория Git» (см. картинку 1 ниже) содержит новый интегрированный процесс входа в GitHub, аналогичный тому, что существует для учётных записей Microsoft. Вы можете сделать репозиторий общедоступным или приватным. Также вы можете создать локальный репозиторий или отправить свой код в существующий удаленный репозиторий в Azure DevOps или у любого другого поставщика.

Меню Git верхнего уровня
Теперь вы можете получить доступ к функциям Git, используя меню Git верхнего уровня, либо по Alt+G. Здесь также есть подменю Локальные репозитории (Local Repositories), с помощью которого вы можете легко переключаться между локальными репозиториями Git, которые вы ранее открывали в Visual Studio.

Просмотр файлов в Проводнике Решения
После того, как вы открыли или клонировали репозиторий, Visual Studio поможет вам сразу перейти к вашему коду. Проводник решения загружает корень репозитория и просматривает каталог на наличие нужных файлов. Visual Studio автоматически найдёт и загрузит решение из файла .sln. Если файлов .sln несколько, вам будет предложен список доступных решений на выбор. Впоследствии вы можете переключаться между текущим открытым решением и списком решений с помощью кнопки «Переключить представление» (Switch Views) на панели инструментов проводника решений (см. картинку 2 ниже).

Оптимизировано окно Git Changes
Новое окно Git Changes предназначено для обеспечения быстрого доступа к часто используемым операциям Git. Вы можете создавать новые ветки, выполнять stash, stage, вносить правки и фиксировать изменения в одном месте, не теряя контекста. В верхней части окна кнопки fetch, pull и push позволяют синхронизировать коммиты и теги с удалёнными репозиториями. В окне Git Changes также есть индикатор, отображающий количество исходящих и входящих коммитов. Он работает как ссылка для перехода в окно репозитория Git. Оттуда вы можете просмотреть сводку исходящих и входящих коммитов перед синхронизацией (см. картинку 3 ниже).

Окно репозитория Git
Окно репозитория Git позволяет легко визуализировать всю историю вашего репозитория. Вы можете щёлкнуть правой кнопкой мыши по ветке, чтобы выполнить различные операции над ревизиями (см. картинку 4 ниже).

Улучшено разрешение конфликтов
В окне Git Changes перечислены неподтверждённые или неслитые изменения. Золотистая панель в конфликтующем файле предлагает вам открыть Редактор Слияния (Merge Editor). Три панели редактора помогут вам визуально разрешить каждый конфликт в файле. Вы также можете принять локальные или входящие изменения одним щелчком мыши (см. картинку 5 ниже).

Источник: https://devblogs.microsoft.com/visualstudio/announcing-the-release-of-the-git-experience-in-visual-studio/
День шестьсот пятьдесят восьмой. #ЗаметкиНаПолях
Вернемся к основам C#:
Разница Между => и {get;} = для Свойств
Судя по примерам кода, которые я встречал несколько раз, оказывается, не все понимают разницу между => и { get; } = для свойств.

Вот пример кода:
public class C
{
public Foo A { get; } = new Foo();
public Foo B => new Foo();
}

Это одно и то же или нет?

Ответ: нет, это не одно и то же.

A - это свойство только с аксессором get (также известное как свойство только для чтения или неизменяемое свойство). Когда создаётся экземпляр класса C, это свойство инициализируется новым экземпляром класса Foo, ссылка на который будет возвращаться с этого момента при обращении к свойству A.

Свойство B также определяет только аксессор, но на этот раз аксессор get содержит new Foo(); в теле. Иначе говоря, свойство B возвращает новый экземпляр Foo каждый раз, когда вы обращаетесь к нему.

В ранних версиях C# это выглядело бы так:
public class C
{
readonly Foo _a = new Foo();

public Foo A
{
get { return _a; }
}

public Foo B
{
get { return new Foo(); }
}
}

Источник: https://www.tabsoverspaces.com/233844-back-to-csharp-basics-difference-between-and-get-for-properties
День шестьсот пятьдесят девятый. #юмор
Прекрасна ведь?)))
День шестьсот шестидесятый. #ЧтоНовенького #EFCore5
Вместе с .NET 5 выпущено множество обновлений. Про новшества в C#9 я писал в постах с тегом #CSharp9. Теперь рассмотрим, что нового в Entity Framework Core 5.0.

Отношение многие-ко-многим
EF Core 5.0 поддерживает отношения многие-ко-многим без явной привязки вспомогательной таблицы. Рассмотрим следующие сущности постов в блоге и тегов:
public class Post {
public int Id { get; set; }
public string Name { get; set; }
public ICollection<Tag> Tags { get; set; }
}
public class Tag {
public int Id { get; set; }
public string Text { get; set; }
public ICollection<Post> Posts { get; set; }
}

Заметьте, что Post содержит коллекцию элементов Tag и наоборот. EF Core 5.0 по соглашению распознает это как отношение многие-ко-многим. То есть добавлять специальный код в OnModelCreating не требуется. Когда для создания базы данных используются миграции (или EnsureCreated), EF Core автоматически создаст вспомогательную таблицу. Например, в SQL Server для этой модели EF Core сгенерирует:
CREATE TABLE [Posts] (
[Id] int NOT NULL IDENTITY,
[Name] nvarchar(max) NULL,
CONSTRAINT [PK_Posts] PRIMARY KEY ([Id])
);
CREATE TABLE [Tag] (
[Id] int NOT NULL IDENTITY,
[Text] nvarchar(max) NULL,
CONSTRAINT [PK_Tag] PRIMARY KEY ([Id])
);
CREATE TABLE [PostTag] (
[PostsId] int NOT NULL,
[TagsId] int NOT NULL,

);

Создание и связывание экземпляров объектов Tag и Post приведёт к автоматическому обновлению вспомогательной таблицы. После вставки сообщений и тегов EF автоматически создаст строки во вспомогательной таблице. Для запросов Include и другие операции будут работать так же, как и для любых других отношений.

В отличие от EF6, EF Core также позволяет полностью настраивать вспомогательную таблицу. Например, приведенный ниже код настраивает отношение «многие-ко-многим» через вспомогательный объект, в котором вспомогательный объект также содержит свойство с полезными данными (PublicationDate):
protected override void OnModelCreating(ModelBuilder modelBuilder) {
modelBuilder
.Entity<Post>()
.HasMany(p => p.Tags)
.WithMany(p => p.Posts)
.UsingEntity<PostTag>(
j => j
.HasOne(pt => pt.Tag)
.WithMany()
.HasForeignKey(pt => pt.TagId),
j => j
.HasOne(pt => pt.Post)
.WithMany()
.HasForeignKey(pt => pt.PostId),
j => {
j.Property(pt => pt.PublicationDate)
.HasDefaultValueSql("CURRENT_TIMESTAMP");
j.HasKey(t => new { t.PostId, t.TagId });
});
}

Источник: https://docs.microsoft.com/ru-ru/ef/core/what-is-new/ef-core-5.0/whatsnew#many-to-many
День шестьсот шестьдесят первый. #ЗаметкиНаПолях
Как разрешить загрузку исполняемых файлов .exe с помощью UseStaticFiles
в
ASP.NET Core
Иногда требуется возвращать клиенту небезопасные файлы, вроде исполняемых .exe. ASP.NET Core по умолчанию очень сильно ограничен в целях безопасности, поэтому необходимо конкретно указывать, что разрешается возвращать. Простое включение в метод Configure() файла Startup.cs промежуточного ПО статических файлов:
app.UseStaticFiles();
не сработает для «необычных» для веб-среды типов, а вернёт ошибку 404.

Метод UseStaticFiles может принимать параметр для опций, поэтому можно обновить список соответствий mime-типов и расширений. Поможет следующая небольшая вспомогательная функция:
private StaticFileOptions GetStaticFileOptions()
{
var p = new FileExtensionContentTypeProvider();
p.Mappings[".exe"] = "application/octet-stream";

return new StaticFileOptions {
ContentTypeProvider = p
};
}

Затем её можно передать в UseStaticFiles:
app.UseStaticFiles(GetStaticFileOptions());

Ещё один вариант – разрешить возвращать файлы с незнакомым расширением, задав для неизвестных файлов mime-тип по умолчанию:
app.UseStaticFiles(new StaticFileOptions
{
// разрешаем возвращать неизвестные типы файлов
ServeUnknownFileTypes = true,
// задаём mime-тип для неизвестных файлов
DefaultContentType = "plain/text"
}

Источники:
-
https://www.hanselman.com/blog/how-to-allow-executable-exe-files-to-be-downloaded-with-aspnet-core-and-usestaticfiles-middleware
-
https://stackoverflow.com/questions/42831645/asp-net-core-download-exe-returns-404-error