Страницы

Поиск по вопросам

Показаны сообщения с ярлыком asp.net-web-api. Показать все сообщения
Показаны сообщения с ярлыком asp.net-web-api. Показать все сообщения

вторник, 31 декабря 2019 г.

Уменьшение количества бойлерплейт кода в webapi-приложении

#c_sharp #api #архитектура #aspnet_web_api


Пишу клиент и сервер для проекта. Суть проекта: складской учет, возможно в будущем
будет расширяться с добавлением бухгалтерского. То есть склады, документы прихода,
расхода, перемещения, зарплата.

Имеется схема: C# desktop - WEB API Asp.Net Core - Database MySQL

Клиент посылает запрос на сервер, сервер обращается к БД и возвращается JSON. Через
HttpClient.

Схема MVC.
Пока протестировал на одной табличке. В контроллере прописал API(для примера):

    // GET: api/Products
    [HttpGet]
    public async Task>> GetProduct()
    {
        return await _context.Product.ToListAsync();
    }

    // GET: api/Products/5
    [HttpGet("{id}")]
    public async Task> GetProduct(int id)
    {
        var product = await _context.Product.FindAsync(id);

        if (product == null)
        {
            return NotFound();
        }

        return product;
    }



Подскажите, когда табличек будет много, и вариантов запросов будет
много, то необходимо каждый запрос вот так вот прописывать(по типу
того что в коде)?
Есть ли какое-то более оптимальное решение и все ли я делаю
правильно?

То есть в будущем я планирую для каждой таблицы создать контроллер и в нем прописывать
api запросы для сервера.

    


Ответы

Ответ 1



Вы делаете всё правильно, но для типовых операций придумали такую вещь, как скафолдинг (scaffolding). Настраивается это следующим образом. В папке C:\Users\{username}\.nuget\packages\microsoft.visualstudio.web.codegenerators.mvc\2.2.2\Templates (у вас могут быть и папки 2.2.1 и прочие) находятся шаблоны предоставляемые майкрософт по умолчанию вместе со студией, например: @inherits Microsoft.VisualStudio.Web.CodeGeneration.Templating.RazorTemplateBase using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; @{string modelName = (Model.ClassName.EndsWith("Controller") ? Model.ClassName.Substring(0, Model.ClassName.Length - 10) : Model.ClassName);} namespace @Model.NamespaceName { [Route("api/[controller]")] [ApiController] public class @Model.ClassName : ControllerBase { // GET: api/@modelName [HttpGet] public IEnumerable Get() { return new string[] { "value1", "value2" }; } // GET: api/@modelName/5 [HttpGet("{id}", Name = "Get")] public string Get(int id) { return "value"; } // POST: api/@modelName [HttpPost] public void Post([FromBody] string value) { } // PUT: api/@modelName/5 [HttpPut("{id}")] public void Put(int id, [FromBody] string value) { } // DELETE: api/ApiWithActions/5 [HttpDelete("{id}")] public void Delete(int id) { } } } Рекомендую не трогать шаблоны по умолчанию, а скопировать папку Templates в ваш проект и уже там отредактировать так, как вам необходимо: Ссылки по теме: Scaffold: beginners guide Metanit: Scaffolding в Visual Studio Custom scaffold templates in ASP.NET Core

Ответ 2



Почти наверняка большинство таблиц будет требовать одинаковых алгоритмов: получать энтити по ключу, получать их список по некоторому признаку с разбиением по страницам и т.п. Значит, этот код нужно как-то абстрагировать от типа таблицы. Вы можете абстрагировать DAL (унести DbContext и получение данных из контроллера в другое место). Какие-то базовые операции вынести в обобщенный код (например, базовый репозиторий, который умеет простой CRUD для любой таблицы), специфичные части запроса делать в наследниках, строя запрос по частям с помощью IQueriable. Завести сущность фильтра и передать ее в методы репозитория, где на основе его уже будет строиться запрос. Аналогичным образом можно и интерфейсный слой (контроллеры) сделать обобщенными, с вкраплением кастомной логики.

Ответ 3



Подскажите, когда табличек будет много, и вариантов запросов будет много, то необходимо каждый запрос вот так вот прописывать(по типу того что в коде)? Смотря сколько параметров в запросе. Если их мало, проще прописывать каждый по отдельности. Если их будет много, лучше использовать DTO (Data Transfer Object) и структурировать параметры в нём. То есть в будущем я планирую для каждой таблицы создать контроллер и в нем прописывать api запросы для сервера. Если у Вас на клиенте предусмотрено изменение всех таблиц, то Вам возможно придётся поступить именно так. И то, в зависимости от ситуации можно построить общую точку редактирования нескольких таблиц. А, вообще такой подход чаще всего не оправдан. Хотя бы потому, что не все таблицы нужно редактировать. например, таблицы, которые хранят перечисления. Правда, если совсем по-хорошему, следует ориентироваться на модель предметной области и уже исходя из неё проектировать систему.

пятница, 27 декабря 2019 г.

Ограничить доступ для редактирования передаваемой модели в PUT-методе

#c_sharp #шаблоны_проектирования #инспекция_кода #rest #aspnet_web_api


Есть вот такая сущность:

public class Product
{
    public int Id { get; set; }
    public string Name { get; set; }        
    public string CategoryName { get; set; } //доступно для редактирования только
пользователю «admin»
    // в реальном проекте свойств больше 
    // и среди них также есть доступные для редактирования только для admin
}


Необходимо на стороне сервера создать метод PUT, который обеспечил бы редактирование
свойства CategoryName только для авторизованного пользователя admin.

Какие практики REST существуют для реализации подобном логики?



Моя реализация такова:

public class ProductDto
{
    [Required]
    public string Name { get; set; }

    [Required]
    public string CategoryName { get; set; }
}

[Authorize]
[RoutePrefix("api/products")]
public class ProductsController : ApiController
{
    private readonly IProductService _productService;

    public ProductsController(IProductService productService)
    {
        _productService = productService;
    }

    [HttpPut]
    [Route("{id}")]
    public HttpResponseMessage Put([FromBody] ProductDto productDto)
    {
        if (!ModelState.IsValid)
        {
            throw new ArgumentException("dto");
        }

        var productId = RequestContext.RouteData.Values["id"].ToString();

        var userName = User.Identity.Name; //извлекаем имя авторизованного пользователя
        var isAdmin = userName == "admin";

        _productService.Update(productId, productDto, isAdmin);

        return Request.CreateResponse(HttpStatusCode.OK);
    }
}

public interface IProductService
{
    void Update(string id, ProductDto dto, bool isAdmin);
}

public class ProductService : IProductService
{
    private readonly IGenericRepository _productRepository;

    public ProductService(IGenericRepository productRepository)
    {
        _productRepository = productRepository;
    }

    public void Update(string id, ProductDto dto, bool isAdmin)
    {
        if (dto == null || id == null)
        {
            throw new ArgumentNullException();
        }

        var product = _productRepository.GetById(id);
        if (product == null)
        {
            throw new ArgumentException(nameof(id));
        }

        product.Name = dto.Name;            
        if(isAdmin)
        {
            product.CategoryName = dto.CategoryName;
        }


        _productRepository.Update(product);
    }
}



Какими изъянами обладает данный код, учитывая то, что входе масштабирования у сущности
Productбудут появляться новые свойства, среди которых также могут быть те которые редактировать
может только admin.
Или стоило попробовать использовать атрибуты валидации/перезагрузку PUT-метода и т.п?

    


Ответы

Ответ 1



Привязывать функционал к имени пользователя не лучшее решение в подобных ситуациях. Для разграничения доступа к функционалу есть специальные средства под названием "роли". Каждому зарегистрированному пользователю можно добавить одну или несколько ролей описывающих его права в системе. Исходя из этого следует второе правило, это разделение прав доступа к функционалу на отдельные методы - "action". В вашем случае первый метод будет вызывать обновление ограниченное количество полей модели, второй метод будет обновлять расширенные поля модели. Конечно для этого придется с клиента вызывать разные методы в зависимости от роли пользователя и в случае нехватки уровня доступа клиентский метод возвратит вменяемую ошибку (403) [RoutePrefix("api/test"), Authorize] public class TestController : ApiController { [HttpPut, Route("{id}")] public void PutAll(int id, [FromBody]BaseModelDto model) { } [HttpPut, Route("{id}/admin"), Authorize(Roles = "admin")] public void PutAdmin(int id, [FromBody]ModelDto model) { } } Что такое роли и как с ними работать можно почитать тут https://habrahabr.ru/post/231807/

Ответ 2



Потенциальное дублирование кода который проверяет что пользователь admin. Можно сделать ActionFilterAttribute и еще на подходе к методу разрешать/запрещать редактирование.

суббота, 21 декабря 2019 г.

Asp.Net MVC 5 или Web Api 2.0

#c_sharp #net #aspnet_mvc #aspnet_web_api #asp


Я совсем запутался... 
Asp.Net MVC - создание веб-приложений, Web Api - сервисов. Не могли бы вы объяснить
для каких задач пишутся сервисы, а для каких веб приложения? Насколько я понимаю, если
у нас клиент планируется не только веб, но еще, например, WPF-приложение, мобильное,
то выбор должен пасть в сторону сервисов.

Конкретно для моего случая: имеется некое десктопное приложение еще на WinWorms.
Хочется его перевести в веб. И встает логичный вопрос что использовать: Asp.Net Web
Api + AngularJs или Asp.Net MVC?

Буду благодарен за любую информацию.
    


Ответы

Ответ 1



Бакэнд для AngularJs можно написать хоть на ASP.NET MVC, хоть на Asp.Net Web Api, хоть на WCF (да, WCF тоже умеет делать REST-сервисы). Основная разница между ними в следующем: ASP.NET MVC подходит для генерации страниц на стороне сервера (но не мешает работать и клиентским фреймворкам). Выбрав связку Angular.js + Web Api, вы навсегда ограничите себя исключительно клиентской генерацией страниц, приобретя заодно возможные проблемы с индексацией поисковиками. Взамен - у вашего сайта автоматически появится API, которое можно сделать публичным. Думаю, начать надо с выяснения ответов на следующие вопросы: Сколько человек будут работать над проектом и какие технологии им знакомы? Насколько само ваше веб-приложение укладывается в концепцию HTTP? Оно будет состоять из набора отдельных динамически генерируемых страниц или из одной (или нескольких) страниц, содержащих формы, контролы и экраны? Пример приложения первого вида - Википедия. Пример приложения второго вида - Gmail. 2а. Предполагается ли, что страница никогда не будет обновляться полностью? 2б. Сможет ли пользователь открыть одновременно 2 части вашего приложения в одной вкладке? (+ ко второму виду) 2в. Понадобится ли пользователю делиться ссылкой с другими или ставить ссылку в закладки? (+ к первому виду) Нужно ли вам публичное API?

вторник, 10 декабря 2019 г.

Где правильно использовать async/await в WebAPI

#c_sharp #архитектура #aspnet_web_api #async_await


Мы никогда не использовали async/await (или что-то подобное) в нашем небольшом проекте.
Но сейчас проект растет, и мы решили добавить асинхронность.

Проект имеет следующую архитектуру:


Domain (репозиторий, EF)
UnitTests 
Service (новый, тут планируется перенести бизнес логику с контроллеров)
WebUI


Мой вопрос, в каком слое мы должны реализовать асинхронность? Или мы должны сделать
это на нескольких слоях? Пожалуйста, подскажите мне, где реализация будет наиболее
подходящей?

Например у меня есть метод в контроллере

public virtual HttpResponseMessage Get()
{
    var entity = Repository.GetAll();

    if (entity != null && entity.Any())
    {
        return Request.CreateResponse(HttpStatusCode.OK, entity);
    }

    var message = $"{GenericTypeName}: No content";
    return ErrorMsg(HttpStatusCode.NoContent, message);
}


Как мне сделать его асинхронным?
    


Ответы

Ответ 1



class Repository { public Task GetAllAsync(CancellationToken cancellationToken) { return Task.Run(() => { // тут что-то ресурсоёмкое вычисляем return entity; // возвращаем результат }); } } Второй пример, то же самое только с async await class Repository { public async Task GetAllAsync(CancellationToken cancellationToken) { // вернёт управление в вызывающий код await Task.Yield(); // тут что-то ресурсоёмкое вычисляем return entity; // возвращаем результат } } Третий пример, если нужно перед возвратом данных вычислить, что-то параллельно class Repository { public async Task GetAllAsync(CancellationToken cancellationToken) { var tasks = new List(); tasks.Add(Task.Run(() => { // тут что-то ресурсоёмкое вычисляем return entity; // возвращаем результат })); tasks.Add(Task.Run(() => { // тут что-то ресурсоёмкое вычисляем return entity; // возвращаем результат })); tasks.Add(Task.Run(() => { // тут что-то ресурсоёмкое вычисляем return entity; // возвращаем результат })); // дожидаемся окончания всех задач await Task.WhenAll(tasks); // дальше пробегаемся по списку задач, получаем результат работы каждой и что-то с ним делаем. И возвращаем уже вычисленный нами результат. } } Конечно в реальном боевом приложении код будет немного другой и зависеть от конкретной задачи но принцип будет один и тот же. Обращаю внимание, что async/await это синтаксическая обёртка над Task.Run с некоторыми отличиями. А отличия в обработке исключений в процессе выполнения задачи. Но прежде чем писать асинхронный код обязательно прочитайте про async/await И task иначе будет множество ошибок. Особенно внимательно нужно смотреть обработку исключений.

Ответ 2



Асинхронность распространяется на все слои. От нижнего к верхнему. В вашем случае, если Domain асинхронный то и WebUI будет асинхронным. Не забывайте, что асинхронность нужна только для операций с периферийными устройствами (диск, сеть...) для того, что бы при ожидании ответа не простаивал рабочий поток. class Repository { public Task GetAllAsync(CancellationToken cancellationToken) { using (var db = new DataContext()) { return db.EntitySet.ToListAsync(); } } } public virtual async Task Get(CancellationToken cancellationToken) { var entity = await Repository.GetAllAsync(cancellationToken); if (!entity.Any()) { var message = $"{GenericTypeName}: No content"; return Content(HttpStatusCode.NoContent, message); } return Ok(entity); } Обратите внимание http status code 204 - No content (это не ошибочный статус код) Для справки: https://ru.wikipedia.org/wiki/Список_кодов_состояния_HTTP И вообще вам прежде нужно почитать про Task и async await. Task.Run используется для cpu bound operations (Вычислительных операций процессора) async await для ожидания результата этих операций в вызывающем коде или ожидания операций I/O bound operations. То что касается entity framework то в 6 версии есть асинхронные методы их названия заканчиваются на Async, например .ToListAsync().

воскресенье, 8 декабря 2019 г.

Как правильно “готовить” авторизацию в SPA?

#c_sharp #aspnet_core #aspnet_web_api #oauth #jwt


Цель такая: написать бэкенд ASP.Net Core MVC* SPA для работы с ReactJS и дальнейшей
возможностью переиспользовать существующий API для создания, скажем, Android приложения.  
Платформа: .Net Core 2.1.

* - Если такое ещё можно назвать MVC, учитывая, что View не будет, а будет отдельная
директория ClientApp со всем содержимым фронтэнда.

Пошуршав интернеты, наткнулся на то, что ASP.Net Core Identity не актуален. Звучит
логично, учитывая, что тот сильно опирается на куки, а при общении через API куки таскать
неудобно. Хотя многие примеры нижеупомянутого JWT всё же используют IdentityUser.

Много инструкций с использованием JWT. Приличная часть из них слишком зациклена на
фронтенд реализации и практически ничего не говорит о бэкенде. Не нашёл примеров с
OAuth2, везде свой велосипед, причём Demo и не пригодный для реального использования. 
В тех же примерах по JWT используются Issuer, Audience и SecretKey, но ни слова о
том, по каким правилам их надо выбирать и/или генерировать, ну и где безопасно хранить
(если исключить примеры с хардкодом, то их обычно хранили в appsettings.json).

Также в процессе гугления (конкретно: попытке найти инфу об JwtSecurityTokenHandler
из System.IdentityModel.Tokens) MSDN Microsoft выдаёт:


  We’re no longer updating this content regularly. Check the Microsoft Product Lifecycle
for information about how this product, service, or technology is supported.


Что это значит? Microsoft более не поддерживает JWT? Технология в принципе уже неактуальна?
Что же тогда использовать?

В итоге мне теперь не совсем понятно как строить авторизацию в своём приложении:


Нужные ли мне IdentityUser из Microsoft.AspNetCore.Identity?
Нужен ли мне IdentityDbContext из Microsoft.AspNetCore.Identity.EntityFrameworkCore?
Актуален ли JWT?
Как построить авторизацию с OAuth2?


Хотелось бы сохранить доступность авторизованного пользователя из HttpContext.User
и к его Claims, чтобы не мучать БД лишний раз для получения Id/UserName/Avatar/LastOnline/etc. 

А также учесть то, что активно будут использоваться роли пользователей. 

UPD: Немного обновлю конечные цели, чтобы стало понятнее:


Это форум с разделами, постами и комментариями в формате вопрос-ответ 
Реализация фронтенда через SPA
Планируется открытое API
Это же API будет использовать SPA
Поддержка быстрой регистрации/входа через сторонние сервисы (Vk, FB, Google, etc.)

    


Ответы

Ответ 1



Вот проверенный код, который работает. Итак, настройка аутентификации в Startup public class Startup { public Startup(IConfiguration configuration) { Configuration = configuration; } public IConfiguration Configuration { get; } // This method gets called by the runtime. Use this method to add services to the container. public void ConfigureServices(IServiceCollection services) { Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); services.AddDbContext(options => options.UseMySql(Configuration.GetConnectionString("DefaultConnection"))); services.AddIdentity() .AddEntityFrameworkStores(); services.Configure(options => { //........ }); services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) .AddJwtBearer(options => { options.RequireHttpsMetadata = false; options.TokenValidationParameters = new TokenValidationParameters { // укзывает, будет ли валидироваться издатель при валидации токена ValidateIssuer = false, // будет ли валидироваться потребитель токена ValidateAudience = false, // будет ли валидироваться время существования ValidateLifetime = true, // установка ключа безопасности IssuerSigningKey = AuthOptions.GetSymmetricSecurityKey(), // валидация ключа безопасности ValidateIssuerSigningKey = true, }; }); services.AddAuthorization(options => { options.DefaultPolicy = new AuthorizationPolicyBuilder(JwtBearerDefaults.AuthenticationScheme) .RequireAuthenticatedUser() .Build(); }); // ......... } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. public void Configure(IApplicationBuilder app, IHostingEnvironment env) { // ............... app.UseAuthentication(); // ................ } } Класс AuthOptions public class AuthOptions { const string KEY = "mysupersecret_secretkey!"; // ключ для шифрации public const int LIFETIME = 300; // время жизни токена - 5 часов public static SymmetricSecurityKey GetSymmetricSecurityKey() { return new SymmetricSecurityKey(Encoding.ASCII.GetBytes(KEY)); } } Моделька для запроса токена public class TokenRequest { public string UserName { get; set; } public string Password { get; set; } } Контроллер для получения токена [Route("api/[controller]")] public class AccountController : Controller { private readonly UserManager _userManager; private readonly SignInManager _signInManager; public AccountController(UserManager userManager, SignInManager signInManager) { _userManager = userManager; _signInManager = signInManager; } [HttpPost("[action]")] public async Task Auth([FromBody] TokenRequest tokenRequest) { var username = tokenRequest.UserName; var password = tokenRequest.Password; var principal = await GetPrincipal(username, password); if (principal == null) { return StatusCode(400, "Invalid username or password."); } var now = DateTime.UtcNow; // создаем JWT-токен var jwt = new JwtSecurityToken( notBefore: now, claims: principal.Claims, expires: now.Add(TimeSpan.FromMinutes(AuthOptions.LIFETIME)), signingCredentials: new SigningCredentials(AuthOptions.GetSymmetricSecurityKey(), SecurityAlgorithms.HmacSha256)); var encodedJwt = new JwtSecurityTokenHandler().WriteToken(jwt); var response = new { token = encodedJwt, username = principal.Identity.Name }; return Json(response); } private async Task GetPrincipal(string username, string password) { var user = await _userManager.FindByNameAsync(username); if (user != null) { var check = await _userManager.CheckPasswordAsync(user, password); if (check) { var principal = await _signInManager.CreateUserPrincipalAsync(user); return principal; } } return null; } } И, по моему, это все. С этим все остальные вещи типа атрибутов авторизации, роли и прочее работает из коробки. Насколько я помню, ничего дополнительно делать не надо.

Ответ 2



Для начала, ASP.Net Core Identity никаким образом не опирается на куки! ASP.Net Core Identity - это прежде всего система для хранения и обработки информации о пользователях. Все что делает Identity во время аутентификации - это загружает из БД информацию о пользователе, заполняя набор утверждений о нем, после чего передает его ASP.NET Core. Соответственно, нет никакого смысла искать как использовать ASP.Net Core Identity вместе с Jwt - это попросту не связанные друг с другом вещи. Теперь про OAuth. Технология OAuth предназначена для того, чтобы сторонний разработчик мог создать приложение которое бы работало с вашим сайтом, но при этом не имел доступа к паролям ваших пользователей. Если же вы собрались создавать приложение на Андроиде самостоятельно - использование OAuth будет для вас избыточно! Все что вам нужно - это action в контроллере, который принимает логин с паролем и выдает токен. Ну а если вы решили дать доступ к своему API именно для сторонних разработчиков - Identity Server вам в помощь. Наконец, про JWT. Audience - это параметр токена, который означает целевую систему куда его можно предъявлять. Иными словами, это URL вашего сайта. Однако, нет никакого смысла в его использовании в ситуации когда у вас только один сайт: он нужен когда есть несколько сайтов со своими API и эти сайты не доверяют друг другу. Issuer - это URI провайдера, который выдал JWT. То есть, опять-таки, URL вашего сайта. Это поле используется только в тех случаях когда доступ к API можно получить при помощи токенов от разных поставщиков. В остальных случаях это поле вообще-то не обязательное. SecretKey - это, скорее всего, ключ, которым вы шифруете или подписываете JWT. Хранить его можно в appsettings.json или в переменных окружения. Главное - не публикуйте его на github. Вообще, желательно использовать разные ключи для разработки и на "боевом" сервере.

Ответ 3



Отталкивался я от ответа @tym32167 (поэтому и пометил его как правильный), но на всякий случай добавлю конкретно свою реализацию. Вдруг будет кому-то полезна. Startup.cs public class Startup { public Startup(IConfiguration configuration) { Configuration = configuration; } public IConfiguration Configuration { get; } public void ConfigureServices(IServiceCollection services) { // подключение DbContext'ов // подключение Identity services.Configure(options => { // ... }); // игнорирование регистра (url без заглавных букв) services.Configure(options => options.LowercaseUrls = true); // подключение репозиториев services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) .AddJwtBearer(options => { options.RequireHttpsMetadata = false; options.TokenValidationParameters = new TokenValidationParameters { ValidateIssuer = false, //ValidIssuer = "", ValidateAudience = false, //ValidAudience = "", ValidateLifetime = true, IssuerSigningKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(Configuration["Jwt:Key"])), ValidateIssuerSigningKey = true, }; }); services.AddAuthorization(options => { options.DefaultPolicy = new AuthorizationPolicyBuilder(JwtBearerDefaults.AuthenticationScheme) .RequireAuthenticatedUser() .Build(); }); services.AddMvc() .SetCompatibilityVersion(CompatibilityVersion.Version_2_1); services.AddSpaStaticFiles(configuration => { configuration.RootPath = "ClientApp/build"; }); } public void Configure(IApplicationBuilder app, IHostingEnvironment env) { if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } else { app.UseExceptionHandler("/Error"); //ToDo: do it better app.UseHsts(); } app.UseHttpsRedirection(); app.UseStaticFiles(); app.UseSpaStaticFiles(); app.UseAuthentication(); app.UseMvc(routes => { routes.MapRoute( name: "default", template: "{controller}/{action=Index}/{id?}"); }); app.UseSpa(spa => { spa.Options.SourcePath = "ClientApp"; if (env.IsDevelopment()) { // актуально для ReactJS spa.UseReactDevelopmentServer(npmScript: "start"); } }); } } appsettings.json { "Jwt": { "Key": "my_very_secret_key", "LifeTime": "300" } } Jwt:Key должен быть больше 16 символов (128 бит) иначе выбрасывает исключение при генерации токена. По крайней мере, при использовании шифрования HmacSha256. TokenRequest.cs public class TokenRequest { [Required(ErrorMessage = "")] [JsonProperty("login")] public string Login { get; set; } [Required(ErrorMessage = "")] [DataType(DataType.Password)] [JsonProperty("password")] public string Password { get; set; } } RegisterModel.cs public class RegisterModel { [Required(ErrorMessage = "")] [JsonProperty("username")] public string UserName { get; set; } [Required(ErrorMessage = "")] [EmailAddress] [DataType(DataType.EmailAddress)] [JsonProperty("email")] public string Email { get; set; } [Required(ErrorMessage = "")] [DataType(DataType.Password)] [JsonProperty("password")] public string Password { get; set; } [DataType(DataType.Password)] [Compare("Password", ErrorMessage = "")] [JsonProperty("confirm_password")] public string ConfirmPassword { get; set; } } AccountController.cs [Route("api/[controller]")] [ApiController] public class AccountController : ControllerBase { private readonly UserManager userManager; private readonly SignInManager signInManager; private readonly IConfiguration configuration; public AccountController( UserManager userManager, SignInManager signInManager, IConfiguration configuration) { this.userManager = userManager; this.signInManager = signInManager; this.configuration = configuration; } [HttpPost("[action]")] public async Task Register([FromBody] RegisterModel model) { if (!ModelState.IsValid) { var errors = ModelState.Values.SelectMany(v => v.Errors.Select(e => e.ErrorMessage)); return BadRequest(errors); } var userAccount = new UserAccount { Email = model.Email, UserName = model.UserName }; var result = await userManager.CreateAsync(userAccount, model.Password); if (result.Succeeded) { var token = await GetTokenAsync(userAccount); return Ok(new { token }); } return BadRequest(result.Errors.Select(e => e.Description)); } [HttpPost("[action]")] public async Task Auth([FromBody] TokenRequest tokenRequest) { var userAccount = tokenRequest.Login.Contains('@') ? await userManager.FindByEmailAsync(tokenRequest.Login) : await userManager.FindByNameAsync(tokenRequest.Login); if (userAccount == null) { return NotFound(new { message = $"User with login {tokenRequest.Login} not found!" }); } var passValided = await userManager.CheckPasswordAsync(userAccount, tokenRequest.Password); if (!passValided) { return UnprocessableEntity(new { message = "Invalid username or password." }); } var token = await GetTokenAsync(userAccount); return Ok(token); } private async Task GetTokenAsync(UserAccount userAccount) { var principal = await signInManager.CreateUserPrincipalAsync(userAccount); var identity = (ClaimsIdentity)principal.Identity; if (identity == null) { return null; } var now = DateTime.UtcNow; // created JWT-token var jwt = new JwtSecurityToken( notBefore: DateTime.UtcNow, claims: identity.Claims, expires: now.Add(TimeSpan.FromMinutes(int.Parse(configuration["Jwt:LifeTime"]))), signingCredentials: new SigningCredentials( new SymmetricSecurityKey( Encoding.ASCII.GetBytes(configuration["Jwt:Key"])), SecurityAlgorithms.HmacSha256)); var token = new JwtSecurityTokenHandler().WriteToken(jwt); return token; } } UserAccount наследник IdentityUser, а UserRole наследник IdentityRole

четверг, 11 июля 2019 г.

ASP.NET WebApi и связаны ресурсы

Доброго времени суток.
Нужна ваша помощь. Есть ASP.NET WebApi приложение в котором нужно представить связь двух ресурсов. Например есть сущность StreetType которая возвращает json:
{ "id":3, "name":"StreetType1", }
и сущность Street
{ "id":1, "name":"Street1", "streettypeid":3 }
Ранее для получения связанных ресурсов использовал OData запрос: http://localhost:3761/api/Street?$expand=StreetType
Но, так как в некоторых сущностей может быть 3 и более связанных ресурсы то писать такие запросы будет сложно ну и не очень красиво.
Было бы неплохо реализовать это следующим образом:
http://localhost:3761/api/Street/ - все Street http://localhost:3761/api/full/Street/ - все Street и связанные ресурсы (в этом случае только StreetType) http://localhost:3761/api/Street/1 - Street с ID = 1 http://localhost:3761/api/full/Street/1 - Street с ID = 1 и связанные ресурсы
Но есть проблема с системой маршрутизации и наследованием атрибутов. Так как все это должно быть реализовано в базовом классе.
Вопрос: Как правильно реализовать представление связанных объектов? Как оформить простой URL например для такого запроса OData: http://localhost:3761/api/City?$expand=CityType,Region


Ответ

Есть небольшая книга по проектированию API интерфейсов автора Brian Mulloy, называется Web API Design Crafting Interfaces that Developers Love. В ней приводятся примеры, как вашу проблему решали крупные корпорации, в частности
Facebook: /joe.smith/friends?fields=id,name,picture Google: ?fields=title,media:group(media:thumbnail) LinkedIn: /people:(id,first-name,last-name,industry)
Можно так же разделять поля сущности на обязательные и дополнительные, где обязательные выгружать всегда, а дополнительные по запросу.
В целом, запрос http://localhost:3761/api/City?$expand=CityType,Region выглядит неправильно, т.к. вы сцепляете разные сущности. Логичнее получать Region отдельным запросом.

среда, 10 июля 2019 г.

Преимущества WebApi перед Asp.net mvc при реализации api

Добрый день! Возник следующий вопрос. Имеется asp.net mvc приложение, работающее с некой бд (sql server). И есть еще одно приложение, для которого нужно дать некий api для работы с первым приложением (по большому счету с его базой данных). Предполагается что api должно представлять собой набор http методов, позволяющих получать ответ в формате json. Напрашивается использование технологии ASP.NET Web Api, про которую я слышал ранее. Проблема вот в чем. Я почти ничего не знаю об этой технологии (ну только что она позволяет достаточно легко и удобно создавать такие api и более "легкая" по сравнению с wcf). Пока что я вижу более простую для себя альтернативу в виде создания дополнительного контроллера в своем asp.net mvc приложении с экшнами, возвращающими JsonResult. Ну в общем такая простенькая реализация api. Преимущество такого подхода для меня состоит в том, что в этом случае не придется создавать новый проект и разворачивать его отдельно (мелочь но все же) а также что в mvc приложении доступ к данным происходит через orm и в случае с web api придется как-то дублировать код этого доступа и в web api проекте.
Теперь сам вопрос: есть ли какие-то весомые преимущества у технологии web api для решения данной задачи? Или наоборот есть ли какие-то существенные недостатки для решения этой задачи у подхода с отдельным контроллером и экшнами возвращающими JsonResult? Просто меня гложет сомнение что раз существует такая технология как WebApi,то наверное реализовывать свой api с помощью такого кустарного метода как простые Json-экшены несколько глупо? Наставьте пожалуйста на путь истинный! заранее спасибо!


Ответ

WEB API крайне удобен для создания RESTful-сервисов, который Вам как раз и нужно будет создать. Есть у него и свои нюансы, если MVC сочетает в себе и контроллеры и движок представления, то Web Api - это только контроллеры, возвращающие данные (JSON, XML или др. - не важно), которые работают с HTTP. Вы не сможете, к примеру, выполнить переадресацию пользователя на другую страницу, но вы можете послать HTTP-код, соответствующий редиректу. Вы не сможете отобразить ошибку авторизации, но вы пошлёте на клиент статус-код, в котором покажете ошибку 401 и сообщение к нему.
Абсолютно без разницы как и на чём вы будете писать клиентское приложение - ведь WebApi - это HTTP-стандарт.
Но если не брать в расчёт такие понятия как вид выводимых данных - то то, что происходит в контроллере - практически ничем не отличается от обычного MVC! Так что не стоит переживать о том, что не сможете использовать ORM

суббота, 15 июня 2019 г.

Приведение JSON модели к Dictionary

Возникла проблема, при тестировании методов restAPI.
Необходимо производить валидацию модели, полученной в результате выполнения запроса и эталонной модели. Я похоже изобретаю жуткий велосипед, но идея была такова, что бы преобразовать json модель типа: { "key0" : "value", "key1" : "value", ... , "key_n" : "value" } к словарю, и сравнить значения у полей.
Чувствую, что либо так не делают, либо есть подход проще. Если кто нибудь сталкивался или знает куда копать подскажите пожалуйста.


Ответ

Я бы вам посоветовал посмотреть на JSON.net тык. Дело в том, что ваша задача состоит в том, чтобы десериализовать полученный json и проверить данные. Есть несколько способов.
Сделать класс Class1, который будет содержать нужные поля. И потом десериализовать с помощью Class1 variable = JsonConvert.DeserializeObject(json) ; Ну а как проверить переменную класса на значения думаю не так уж и сложно.Чаще всего будет проще словить на моменте сериализации т.к. там сразу же будет ошибка при попытке сериализовать не тот тип или отсуствующее поле. Тут описано, как можно упростить процесс, описанный выше с помощью DataContractJsonSerializer. Работать с json как с обычным объектом. Т.е. десериализовать строку в объект и потом проверять наличие полей (keys) и значений по этим полям. Но я бы не рекомендовал это т.к. это дольше, сложнее и т.п.);
Можно еще посмотреть тут. Возможно это наведет вас на нужную мысль.
UPD Как сериализовать в WEb API

понедельник, 12 ноября 2018 г.

Asp.Net MVC 5 или Web Api 2.0

Я совсем запутался... Asp.Net MVC - создание веб-приложений, Web Api - сервисов. Не могли бы вы объяснить для каких задач пишутся сервисы, а для каких веб приложения? Насколько я понимаю, если у нас клиент планируется не только веб, но еще, например, WPF-приложение, мобильное, то выбор должен пасть в сторону сервисов.
Конкретно для моего случая: имеется некое десктопное приложение еще на WinWorms. Хочется его перевести в веб. И встает логичный вопрос что использовать: Asp.Net Web Api + AngularJs или Asp.Net MVC?
Буду благодарен за любую информацию.


Ответ

Бакэнд для AngularJs можно написать хоть на ASP.NET MVC, хоть на Asp.Net Web Api, хоть на WCF (да, WCF тоже умеет делать REST-сервисы). Основная разница между ними в следующем:
ASP.NET MVC подходит для генерации страниц на стороне сервера (но не мешает работать и клиентским фреймворкам). Выбрав связку Angular.js + Web Api, вы навсегда ограничите себя исключительно клиентской генерацией страниц, приобретя заодно возможные проблемы с индексацией поисковиками. Взамен - у вашего сайта автоматически появится API, которое можно сделать публичным.
Думаю, начать надо с выяснения ответов на следующие вопросы:
Сколько человек будут работать над проектом и какие технологии им знакомы? Насколько само ваше веб-приложение укладывается в концепцию HTTP? Оно будет состоять из набора отдельных динамически генерируемых страниц или из одной (или нескольких) страниц, содержащих формы, контролы и экраны?
Пример приложения первого вида - Википедия. Пример приложения второго вида - Gmail.
2а. Предполагается ли, что страница никогда не будет обновляться полностью?
2б. Сможет ли пользователь открыть одновременно 2 части вашего приложения в одной вкладке? (+ ко второму виду)
2в. Понадобится ли пользователю делиться ссылкой с другими или ставить ссылку в закладки? (+ к первому виду) Нужно ли вам публичное API?

вторник, 23 октября 2018 г.

Где правильно использовать async/await в WebAPI

Мы никогда не использовали async/await (или что-то подобное) в нашем небольшом проекте. Но сейчас проект растет, и мы решили добавить асинхронность.
Проект имеет следующую архитектуру:
Domain (репозиторий, EF) UnitTests Service (новый, тут планируется перенести бизнес логику с контроллеров) WebUI
Мой вопрос, в каком слое мы должны реализовать асинхронность? Или мы должны сделать это на нескольких слоях? Пожалуйста, подскажите мне, где реализация будет наиболее подходящей?
Например у меня есть метод в контроллере
public virtual HttpResponseMessage Get() { var entity = Repository.GetAll();
if (entity != null && entity.Any()) { return Request.CreateResponse(HttpStatusCode.OK, entity); }
var message = $"{GenericTypeName}: No content"; return ErrorMsg(HttpStatusCode.NoContent, message); }
Как мне сделать его асинхронным?


Ответ

class Repository { public Task GetAllAsync(CancellationToken cancellationToken) { return Task.Run(() => { // тут что-то ресурсоёмкое вычисляем
return entity; // возвращаем результат }); } }
Второй пример, то же самое только с async await
class Repository { public async Task GetAllAsync(CancellationToken cancellationToken) { // вернёт управление в вызывающий код await Task.Yield(); // тут что-то ресурсоёмкое вычисляем
return entity; // возвращаем результат } }
Третий пример, если нужно перед возвратом данных вычислить, что-то параллельно
class Repository { public async Task GetAllAsync(CancellationToken cancellationToken) { var tasks = new List(); tasks.Add(Task.Run(() => { // тут что-то ресурсоёмкое вычисляем
return entity; // возвращаем результат })); tasks.Add(Task.Run(() => { // тут что-то ресурсоёмкое вычисляем
return entity; // возвращаем результат })); tasks.Add(Task.Run(() => { // тут что-то ресурсоёмкое вычисляем
return entity; // возвращаем результат })); // дожидаемся окончания всех задач await Task.WhenAll(tasks);
// дальше пробегаемся по списку задач, получаем результат работы каждой и что-то с ним делаем. И возвращаем уже вычисленный нами результат. } }
Конечно в реальном боевом приложении код будет немного другой и зависеть от конкретной задачи но принцип будет один и тот же.
Обращаю внимание, что async/await это синтаксическая обёртка над Task.Run с некоторыми отличиями. А отличия в обработке исключений в процессе выполнения задачи.
Но прежде чем писать асинхронный код обязательно прочитайте про async/await И task иначе будет множество ошибок. Особенно внимательно нужно смотреть обработку исключений.

четверг, 18 октября 2018 г.

Как правильно “готовить” авторизацию в SPA?

Цель такая: написать бэкенд ASP.Net Core MVC* SPA для работы с ReactJS и дальнейшей возможностью переиспользовать существующий API для создания, скажем, Android приложения. Платформа: .Net Core 2.1
* - Если такое ещё можно назвать MVC, учитывая, что View не будет, а будет отдельная директория ClientApp со всем содержимым фронтэнда.
Пошуршав интернеты, наткнулся на то, что ASP.Net Core Identity не актуален. Звучит логично, учитывая, что тот сильно опирается на куки, а при общении через API куки таскать неудобно. Хотя многие примеры нижеупомянутого JWT всё же используют IdentityUser
Много инструкций с использованием JWT. Приличная часть из них слишком зациклена на фронтенд реализации и практически ничего не говорит о бэкенде. Не нашёл примеров с OAuth2, везде свой велосипед, причём Demo и не пригодный для реального использования. В тех же примерах по JWT используются Issuer, Audience и SecretKey, но ни слова о том, по каким правилам их надо выбирать и/или генерировать, ну и где безопасно хранить (если исключить примеры с хардкодом, то их обычно хранили в appsettings.json).
Также в процессе гугления (конкретно: попытке найти инфу об JwtSecurityTokenHandler из System.IdentityModel.Tokens) MSDN Microsoft выдаёт:
We’re no longer updating this content regularly. Check the Microsoft Product Lifecycle for information about how this product, service, or technology is supported.
Что это значит? Microsoft более не поддерживает JWT? Технология в принципе уже неактуальна? Что же тогда использовать?
В итоге мне теперь не совсем понятно как строить авторизацию в своём приложении:
Нужные ли мне IdentityUser из Microsoft.AspNetCore.Identity? Нужен ли мне IdentityDbContext из Microsoft.AspNetCore.Identity.EntityFrameworkCore? Актуален ли JWT? Как построить авторизацию с OAuth2?
Хотелось бы сохранить доступность авторизованного пользователя из HttpContext.User и к его Claims, чтобы не мучать БД лишний раз для получения Id/UserName/Avatar/LastOnline/etc.
А также учесть то, что активно будут использоваться роли пользователей.
UPD: Немного обновлю конечные цели, чтобы стало понятнее:
Это форум с разделами, постами и комментариями в формате вопрос-ответ Реализация фронтенда через SPA Планируется открытое API Это же API будет использовать SPA Поддержка быстрой регистрации/входа через сторонние сервисы (Vk, FB, Google, etc.)


Ответ

Вот проверенный код, который работает. Итак, настройка аутентификации в Startup
public class Startup { public Startup(IConfiguration configuration) { Configuration = configuration; }
public IConfiguration Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container. public void ConfigureServices(IServiceCollection services) { Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
services.AddDbContext(options => options.UseMySql(Configuration.GetConnectionString("DefaultConnection")));
services.AddIdentity() .AddEntityFrameworkStores();
services.Configure(options => { //........ });
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) .AddJwtBearer(options => { options.RequireHttpsMetadata = false; options.TokenValidationParameters = new TokenValidationParameters { // укзывает, будет ли валидироваться издатель при валидации токена ValidateIssuer = false, // будет ли валидироваться потребитель токена ValidateAudience = false, // будет ли валидироваться время существования ValidateLifetime = true,
// установка ключа безопасности IssuerSigningKey = AuthOptions.GetSymmetricSecurityKey(), // валидация ключа безопасности ValidateIssuerSigningKey = true, }; });
services.AddAuthorization(options => { options.DefaultPolicy = new AuthorizationPolicyBuilder(JwtBearerDefaults.AuthenticationScheme) .RequireAuthenticatedUser() .Build(); });
// ......... }
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline. public void Configure(IApplicationBuilder app, IHostingEnvironment env) { // ............... app.UseAuthentication(); // ................ } }
Класс AuthOptions
public class AuthOptions { const string KEY = "mysupersecret_secretkey!"; // ключ для шифрации public const int LIFETIME = 300; // время жизни токена - 5 часов public static SymmetricSecurityKey GetSymmetricSecurityKey() { return new SymmetricSecurityKey(Encoding.ASCII.GetBytes(KEY)); } }
Моделька для запроса токена
public class TokenRequest { public string UserName { get; set; } public string Password { get; set; } }
Контроллер для получения токена
[Route("api/[controller]")] public class AccountController : Controller { private readonly UserManager _userManager; private readonly SignInManager _signInManager;
public AccountController(UserManager userManager, SignInManager signInManager) { _userManager = userManager; _signInManager = signInManager; }
[HttpPost("[action]")] public async Task Auth([FromBody] TokenRequest tokenRequest) { var username = tokenRequest.UserName; var password = tokenRequest.Password;
var principal = await GetPrincipal(username, password); if (principal == null) { return StatusCode(400, "Invalid username or password."); }
var now = DateTime.UtcNow; // создаем JWT-токен var jwt = new JwtSecurityToken( notBefore: now, claims: principal.Claims, expires: now.Add(TimeSpan.FromMinutes(AuthOptions.LIFETIME)), signingCredentials: new SigningCredentials(AuthOptions.GetSymmetricSecurityKey(), SecurityAlgorithms.HmacSha256)); var encodedJwt = new JwtSecurityTokenHandler().WriteToken(jwt);
var response = new { token = encodedJwt, username = principal.Identity.Name };
return Json(response); }
private async Task GetPrincipal(string username, string password) { var user = await _userManager.FindByNameAsync(username); if (user != null) { var check = await _userManager.CheckPasswordAsync(user, password); if (check) { var principal = await _signInManager.CreateUserPrincipalAsync(user); return principal; } } return null; } }
И, по моему, это все. С этим все остальные вещи типа атрибутов авторизации, роли и прочее работает из коробки. Насколько я помню, ничего дополнительно делать не надо.