Я новичок в ASP.Net, ASP.NET MVC и EntityFrameWork (как правило) и в вариантах .NET Core (в частности). В настоящее время я пытаюсь запустить свой первый пример/тестовый проект (в Visuals Studio 2015), но у меня есть пара проблем, для которых я не смог найти решения в Google.
Часть руководств и инструкций, которым я следовал до сих пор:
- https://dzone.com/articles/how-to-create-rest-apiweb-api-with-aspnet-core-10 (для первого ознакомления)
- http://www.restapitutorial.com/lessons/httpmethods.html (для чего веб-API должен вернуться)
- https://docs.efproject.net/en/latest/platforms/aspnetcore/existing-db.html (создайте БД и scaffold-dbcontext)
- https://docs.asp.net/en/latest/fundamentals/logging.html (для общего использования регистраторов)
- https://github.com/NLog/NLog.Extensions.Logging (для настройки ведения журнала с NLog)
- https://docs.asp.net/en/latest/tutorials/web-api-help-pages-using-swagger.html (для настройки и использования swagger)
Эти учебники и инструкции описывают только фрагмент решения каждый, но эти фрагменты не подходят друг другу и вызывают проблемы. Поэтому я пытаюсь собрать недостающие части воедино.
Чего я хочу добиться, так это (как можно более простого) примера проекта.
- Демо/пример проекта ASP.NET Core WEB API (в Visual Studio 2015)
- который хранит данные в базе данных (SQL) (а не в каком-то рукописном репозитории) с использованием ядра EntityFramework (всего 1 таблица Person, содержащая 3 столбца: id как идентификатор первичного ключа, 2 столбца, имя и фамилия как nvarchar (30))
- where one can
- request (GET) all persons (WORKS in the code below)
- (GET) конкретного человека по id или по фамилии (работает в коде ниже)
- создать (POST) нового человека (работает в приведенном ниже коде)
- (УДАЛИТЬ) человека по id (работает в коде ниже)
- полная замена (PUT) по id (КАК СДЕЛАТЬ?)
- изменить (ИСПРАВИТЬ) фамилию (люди все еще женятся) отправив только идентификатор и новую фамилию (КАК СДЕЛАТЬ?)
- using a repository between the controller and the dbContext (for reusability of the repository functions)
- have the controller to be standard conform (return correct error code/error results)
- иметь рабочую обработку исключений
Мои реализации под вопросом
- IPersonRepository.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using PersonExample.Models;
namespace PersonExample.Repository
{
public interface IPersonRepositoy
{
IEnumerable GetAll();
Person GetById(int id);
IEnumerable GetByLastname(string lastname);
IEnumerable SearchByLastname(string namePart);
int Create(Person item);
int Delete(int id);
int Replace(int id, Person item);
int Modify(int id, string newLastname);
}
}
- PersonRepository.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using PersonExample.Models;
namespace PersonExample.Repository
{
public class PersonRepository : IPersonRepositoy
{
private readonly PersonDbContext _dbContext;
private readonly ILogger _logger;
public PersonRepository(PersonDbContext dbContext, ILogger logger)
{
_dbContext = dbContext;
_logger = logger;
}
public IEnumerable GetAll()
{
//always returns an IEnumberable (even if it is empty)
_logger.LogDebug(string.Format("{0}.GetAll()", GetType().Name));
return _dbContext.Person;
}
public Person GetById(int id)
{
//SingleOrDefault() returns an instance of Person or null
_logger.LogDebug(string.Format("{0}.GetById({1})", GetType().Name, id));
return _dbContext.Person.Where(i => i.Id == id).SingleOrDefault();
}
public IEnumerable GetByLastname(string lastname)
{
//always returns an IEnumberable (even if it is empty)
_logger.LogDebug(string.Format("{0}.GetByLastname({1})", GetType().Name, lastname));
return _dbContext.Person.Where(i => i.Lastname == lastname);
}
public IEnumerable SearchByLastname(string namePart)
{
//always returns an IEnumberable (even if it is empty)
_logger.LogDebug(string.Format("{0}.SearchByLastname({1})", GetType().Name, namePart));
return _dbContext.Person.Where(i => i.Lastname.Contains(namePart));
}
public int Create(Person item)
{
_logger.LogDebug(string.Format("{0}.Create({1}) (id: {2}, firstname: {3}, lastname: {4})",
GetType().Name, item, item.Id, item.Firstname, item.Lastname));
//Add seems to be atomic > Attach would save linked objects too but seems to fail on simple objects
//what exceptions could occure to catch somewhere else (e.g. if lastname would have a unique contraint)?
_dbContext.Person.Add(item);
int res;
try {
res = _dbContext.SaveChanges();
} catch (Microsoft.EntityFrameworkCore.DbUpdateException e)
{
_logger.LogError(string.Format("", GetType().Name));
res = -1;
}
if (res == 0)
{
_logger.LogError(string.Format("{0}.Create({1}) -> no items were created/changed", GetType().Name, item));
}
else
{
_logger.LogDebug(string.Format("{0}.Create({1}) -> {2} item(s) were created/changed", GetType().Name, item, res));
}
return res;
}
public int Delete(int id)
{
_logger.LogDebug(string.Format("{0}.Delete({1}", GetType().Name, id));
Person item = _dbContext.Person.Where(i => i.Id == id).SingleOrDefault();
if (item != null)
{
_dbContext.Person.Remove(item);
int res = _dbContext.SaveChanges();
if (res == 0)
{
_logger.LogError(string.Format("{0}.Delete({1} -> no items deleted", GetType().Name, id));
} else
{
_logger.LogDebug(string.Format("{0}.Delete({1} -> {2} item(s) deleted", GetType().Name, id, res));
}
return res;
}
else
{
_logger.LogError(string.Format("{0}.Delete({1} -> not item found by id", GetType().Name, id));
return -1; // better way to indicate not found?
}
}
public int Replace(int id, Person item)
{
//how to implement replace
throw new NotImplementedException();
}
public int Modify(int id, string newLastname)
{
//how to implement modify
throw new NotImplementedException();
}
}
}
- PersonController.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using PersonExample.Repository;
using PersonExample.Models;
namespace PersonExample.Controllers
{
[Route("api/[controller]")]
public class PersonController : Controller
{
private readonly IPersonRepositoy _repo;
private readonly ILogger _logger;
public PersonController(IPersonRepositoy repo, ILogger logger)
{
_repo = repo;
_logger = logger;
}
// GET: api/values
[HttpGet]
public IEnumerable Get()
{
_logger.LogDebug(string.Format("{0}.GetAll()", GetType().Name));
IEnumerable data = _repo.GetAll();
_logger.LogDebug(string.Format("{0}.GetAll() -> returned {1} result(s)", GetType().Name, "?"));
return data;
}
// GET api/values/5
[HttpGet("{id:int}", Name = "GetPerson")]
public IActionResult Get(int id)
{
_logger.LogDebug(string.Format("{0}.GetById({1})", GetType().Name, id));
Person item = _repo.GetById(id);
if (item == null)
{
_logger.LogError(string.Format("{0}.GetById({1}) -> no item found by id", GetType().Name, id));
return NotFound(id);
}
return new ObjectResult(item);
}
[HttpGet("{lastname}")]
public IEnumerable Get(string lastname)
{
//example to demonstrate overloading of types (int for id, string for lastname)
_logger.LogDebug(string.Format("{0}.GetByLastname()", GetType().Name));
IEnumerable data = _repo.GetByLastname(lastname);
_logger.LogDebug(string.Format("{0}.GetByLastname() -> returned {1} result(s)", GetType().Name, "?"));
return data;
}
[HttpGet("search/{namepart}")]
public IEnumerable Search(string namepart)
{
//example to demonstrate url modification (how would I do multiple name parts?)
_logger.LogDebug(string.Format("{0}.Search({1})", GetType().Name, namepart));
IEnumerable data = _repo.SearchByLastname(namepart);
_logger.LogDebug(string.Format("{0}.Search({1}) -> returned {2} result(s)", GetType().Name, namepart, "?"));
return data;
}
// POST api/values
[HttpPost]
public IActionResult Post([FromBody]Person value)
{
//how to validate data and what to return in error cases?
_logger.LogDebug(string.Format("{0}.Post({1})", GetType().Name, value));
if (value == null)
{
_logger.LogDebug(string.Format("{0}.Post({1}) -> bad request: item is null", GetType().Name, value));
return BadRequest();
}
//return 409 Conflict if resource exists -> where and how to check?
int res = _repo.Create(value);
if (res == 0) //no items changed
{
_logger.LogError(string.Format("{0}.Post({1}) -> zero items changed", GetType().Name, value));
return NotFound(); //what to return? not found isn't the problem
}
else if (res == -1) //DbUpdateException
{
_logger.LogError(string.Format("{0}.Post({1}) -> DbUpdateException", GetType().Name, value));
return NotFound(); //what to return? not found isn't the problem
}
_logger.LogDebug(string.Format("{0}.Post({1}) -> {2} items changed", GetType().Name, value, res));
return CreatedAtRoute("GetPerson", new { id = value.Id }, value);
}
// DELETE api/values/5
[HttpDelete("{id}")]
public IActionResult Delete(int id)
{
_logger.LogDebug(string.Format("{0}.Delete(id: {1})", GetType().Name, id));
int res = _repo.Delete(id);
if (res == 0) // zero entries changed
{
_logger.LogError(string.Format("{0}.Delete({1}) -> zero items changed", GetType().Name, id));
//what to return in that case, its a different error than not found???
return NotFound();
}
else if (res == -1) // id not found
{
_logger.LogError(string.Format("{0}.Delete({1}) -> not found item by id", GetType().Name, id));
return NotFound(id);
}
return Ok();
}
// PUT api/values/5
[HttpPut("{id}")]
public void Put(int id, [FromBody]Person value)
{
//example for full update / complete replace with logging and error handling
// how to implement, what to return?
// _repo.Replace(id, value);
}
// PATCH api/values/5
[HttpPatch("{id}")]
public void Patch(int id, [FromBody]Person value)
{
//example for partial update with logging and error handling
// how to implement, what to return?
//_repo.Modify(id, lastname);
}
}
}
Мои вопросы
В целом:
Каковы правильные (и соответствующие стандарту REST) реализации контроллера и репозитория, включая обработку исключений, проверку данных (необходимо?) и регистрацию ошибок (при возникновении)