Problems implementing a simple ASP.NET Core Web AP

2019-09-11 02:40发布

问题:

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

  1. 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);

        }
    }

  1. 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();
            }

        }

    }

  1. 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)