I am new to ASP.Net, ASP.NET MVC and EntityFrameWork (generally) and with the .NET Core variants (specifically). Currently I am trying to get my first example/test project running (in Visuals Studio 2015) but having a couple of problems I couldn't find solutions for on Google.
Part of the tutorials & instructions I followed so far:
- https://dzone.com/articles/how-to-create-rest-apiweb-api-with-aspnet-core-10 (for the first introduction)
- http://www.restapitutorial.com/lessons/httpmethods.html (for what the web api should return)
- https://docs.efproject.net/en/latest/platforms/aspnetcore/existing-db.html (create the DB and scaffold-dbcontext)
- https://docs.asp.net/en/latest/fundamentals/logging.html (for the general use of loggers)
- https://github.com/NLog/NLog.Extensions.Logging (for configuring Logging with NLog)
- https://docs.asp.net/en/latest/tutorials/web-api-help-pages-using-swagger.html (for setting up and using swagger)
Those tutorials & instructions only describe a snippet of the solution each but those snippets do not fit together and cause problems. So I am trying to get the missing pieces together.
What I want to achieve is a (as simple as possible) example project
- ASP.NET Core WEB API demo/example project (in Visual Studio 2015)
- which stores data in a (SQL) Database (not some handwritten repository) using the EntityFramework Core (just 1 table Person holding 3 columns: id as primary key identity, 2 columns firstname and lastname as nvarchar(30))
- where one can
- request (GET) all persons (WORKS in the code below)
- (GET) a specific person by id or by lastname (works in the code below)
- create (POST) a new person (works in the code below)
- (DELETE) a person by id (works in the code below)
- full replace (PUT) by id (HOW TO DO?)
- modify (PATCH) the last name (people still marry) only sending id and new last name (HOW TO DO?)
- 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)
- have working exception handling
My implementations in question
- 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);
}
}
}
My Questions
In general:
What are the correct (and REST standard conform) implementations of the controller and the repository including exception handling, data validation (necessary?) and logging of errors (when occure)