Creating Web API With ASP.NET Core Using Visual Studio Code

In this article, we are going to learn how to create an API with ASP.NET Core.

Introduction

Web API is a framework that allows us to build web or http based endpoints. If you are building services today for mobile, HTML 5, and even client-server desktop applications, all these different types of applications need services in the back-end to perform operations on data. In this post, we will create Web APIs to  perfrom CRUD (create, read, update and delete) operations.

Agenda

  • Designing the Resource URIs
  • Creating Web API Project
  • Adding Entities
  • Adding DbContext
  • Creating the repository
  • Dependency Injection
  • Setting up connection string
  • Enabling migrations
  • Adding view models and mapping with an automapper
  • Creating the Controllers
  • Adding Security
  • Conclusion

Designing Resource URls

We are going to design the outer layer contract; first a resource name in URI should be a noun. We have "dishes" resource and if we use GET method, we should get a collection of dishes. To get a specific dish or dish category, we will include the dishId or category. The last thing we need is "comments" resource. It has its own Controller but in order to post a comment, we have to get specific dish.

ASP.NET Core
Perquisites

  • .NET Core SDK version 1.1
  • Visual Studio Code
  • Postman tool

Step 1

Open command prompt, type "mkdir confusionrestaurant", and again type "dotnet new webapi", to create an ASP.NET Core Web API template. You might see that there are already some files generated by template; we will discuss these files later. Right now, let’s restore packages by typing "dotnet restore".

Program.cs

The program.cs file is in the root of project, and if you look closely, it starts with void main and that’s the starting point of console app. So, what the file actually doing is that it creates a new webhost builder and then runs the webhost to start listening to the requests. It also instantiates a class called startup, and the startup class is where to setup how to answer the request.

ASP.NET Core

Startup.cs

In startup.cs file, there are two main methods - configureServices and Configure. ConfigureServices is going to setup the dependency injection in ASP.NET Core, and Configure method is used to specify how ASP.NET Core will respond to individual HTTP requests. First, we are going to change the public configuration object to private field config, and register it as Singleton in the Configureservices.

ASP.NET Core

.csproj

If you were using .NET Core before, you might see csproj file in the root of project. Microsoft has depreciated the project.json and adopted leaner version of csproj.

Step 2 Adding Entities

First, add entities folder and next, add a new class and name it as Dish Class.

  1. public class Dish   
  2.  {  
  3.      public int DishId { get; set; }  
  4.      public string DishName { get; set; }  
  5.      public string DishLabel { get; set; }  
  6.      public string Category { get; set; }  
  7.      public decimal Price { get; set; }  
  8.      public string Description { get; set; }  
  9.      public string ImageUrl { get; set; }  
  10.  }  

Add new class and name it as Comment.

  1. public class Comment  
  2.   {  
  3.       public int CommentId { get; set; }  
  4.       public int Rating { get; set; }  
  5.       public string DishComment { get; set; }  
  6.       public DateTime date { get; set; }  
  7.       public CrUser User { get; set; }  
  8.       public Dish Dish { get; set; }  
  9.   }  

Adding a User Class

We are going to extend the asp.netUsers table to include the first name and last name. Add new class and name it CrUser. Make sure you have included Microsoft.ASP.NET.Core.Identity in csproj file.

  1. public class CrUser : IdentityUser  
  2.     {  
  3.         public string FirstName { get; set; }  
  4.         public string LastName { get; set; }  
  5.     }  

Step 3

Next, we are going to add a database context class which will be responsible to communicate with database. So, add new class and name it as CrContext. This class is derived from IdentityDbContext.

  1. public class CrContext : IdentityDbContext  
  2.     {      
  3.         private IConfigurationRoot _config;  
  4.         public CrContext(DbContextOptions<CrContext> options, IConfigurationRoot config)  
  5.         :base(options)  
  6.         {   
  7.             _config = config;       
  8.         }  
  9.         public DbSet<Dish> Dishes {get; set;}  
  10.         public DbSet<Comment> Comments {get; set;}  
  11.           
  12.         protected override void  OnConfiguring(DbContextOptionsBuilder optionsBuilder)  
  13.         {  
  14.             base.OnConfiguring(optionsBuilder);  
  15.             optionsBuilder.UseSqlServer(_config["Data:ConnectionString"]);  
  16.         }  
  17.     }  

Creating the Repository

Step 4

Adding Repository interface.

  1. public class CrRepository : ICrRepository  
  2.     {  
  3.         private readonly CrContext _context;  
  4.         public CrRepository(CrContext context)  
  5.         {  
  6.             _context = context;  
  7.         }  
  8.         public void Add<T>(T entity) where T : class  
  9.         {  
  10.             _context.Add(entity);  
  11.         }  
  12.         public void Delete<T>(T entity) where T : class  
  13.         {  
  14.             _context.Remove(entity);  
  15.         }  
  16.         public IEnumerable<Dish> GetDishes()  
  17.         {  
  18.             return _context.Dishes.ToList();  
  19.         }  
  20.         public Dish GetDishWithComments(int id)  
  21.         {  
  22.             return _context.Dishes  
  23.                 .Include(d => d.Comments)  
  24.                 .Where(d => d.DishId == id)  
  25.                 .FirstOrDefault();  
  26.         }  
  27.         public Dish GetDish(int id)  
  28.         {  
  29.             return _context.Dishes  
  30.                 .Where(d => d.DishId == id)  
  31.                 .FirstOrDefault();  
  32.         }  
  33.         public async Task<bool> SaveAllAsync()  
  34.         {  
  35.             return (await _context.SaveChangesAsync()) > 0;  
  36.         }  
  37.         public IEnumerable<Dish> GetDishByCategory(string category)  
  38.         {  
  39.             return _context.Dishes  
  40.                   .Where(c => c.Category.Equals(category,    StringComparison.CurrentCultureIgnoreCase))  
  41.                   .OrderBy(d => d.DishName)  
  42.                  .ToList();  
  43.         }  
  44.   
  45.         public CrUser GetUser(string userName)  
  46.         {  
  47.             return _context.Users  
  48.                  .Include(u => u.Claims)  
  49.                  .Include(u => u.Roles)  
  50.                  .Where(u => u.UserName == userName)  
  51.                  .Cast<CrUser>()  
  52.                  .FirstOrDefault();  
  53.         }  
  54.     }  

Step 5

Adding the concrete class that implements the interface.

  1. public class CrRepository : ICrRepository  
  2.     {  
  3.         private readonly CrContext _context;  
  4.         public CrRepository(CrContext context)  
  5.         {  
  6.             _context = context;  
  7.         }  
  8.         public void Add<T>(T entity) where T : class  
  9.         {  
  10.             _context.Add(entity);  
  11.         }  
  12.         public void Delete<T>(T entity) where T : class  
  13.         {  
  14.             _context.Remove(entity);  
  15.         }  
  16.         public IEnumerable<Dish> GetDishes()  
  17.         {  
  18.             return _context.Dishes.ToList();  
  19.         }  
  20.         public Dish GetDishWithComments(int id)  
  21.         {  
  22.             return _context.Dishes  
  23.                 .Include(d => d.Comments)  
  24.                 .Where(d => d.DishId == id)  
  25.                 .FirstOrDefault();  
  26.         }  
  27.         public Dish GetDish(int id)  
  28.         {  
  29.             return _context.Dishes  
  30.                 .Where(d => d.DishId == id)  
  31.                 .FirstOrDefault();  
  32.         }  
  33.         public async Task<bool> SaveAllAsync()  
  34.         {  
  35.             return (await _context.SaveChangesAsync()) > 0;  
  36.         }  
  37.         public IEnumerable<Dish> GetDishByCategory(string category)  
  38.         {  
  39.             return _context.Dishes  
  40.                   .Where(c => c.Category.Equals(category,    StringComparison.CurrentCultureIgnoreCase))  
  41.                   .OrderBy(d => d.DishName)  
  42.                  .ToList();  
  43.         }  
  44.   
  45.         public CrUser GetUser(string userName)  
  46.         {  
  47.             return _context.Users  
  48.                  .Include(u => u.Claims)  
  49.                  .Include(u => u.Roles)  
  50.                  .Where(u => u.UserName == userName)  
  51.                  .Cast<CrUser>()  
  52.                  .FirstOrDefault();  
  53.         }  
  54.     }  

Step 6

We are going to seed the database when we first fire up the application. Let’s create CR Initializer class, and what it actually does is that it does a quick query to the database, and if there is no data in the database, it assumes it’s empty and it will seed the database.

  1. public class CRInitializer  
  2.   {  
  3.       private CrContext _ctx;  
  4.       private UserManager<CrUser> _userMgr;  
  5.       private RoleManager<IdentityRole> _roleMgr;  
  6.   
  7.       public CRInitializer(UserManager<CrUser> userMgr,  
  8.                             RoleManager<IdentityRole> roleMgr,  
  9.                             CrContext ctx)  
  10.       {  
  11.           _ctx = ctx;  
  12.           _userMgr = userMgr;  
  13.           _roleMgr = roleMgr;  
  14.       }  
  15.   
  16.       public async Task Seed()  
  17.       {  
  18.           
  19.           var user = await _userMgr.FindByNameAsync("ahmedabdi");  
  20.   
  21.           if (user == null)  
  22.           {  
  23.               if (!(await _roleMgr.RoleExistsAsync("Admin")))  
  24.               {  
  25.                   var role = new IdentityRole("Admin");  
  26.                   role.Claims.Add(new IdentityRoleClaim<string>()  
  27.                   {  
  28.                       ClaimType = "IsAdmin",  
  29.                       ClaimValue = "True"  
  30.                   });  
  31.                   await _roleMgr.CreateAsync(role);  
  32.               }  
  33.               user = new CrUser()  
  34.               {  
  35.                   UserName = "ahmedabdi",  
  36.                   FirstName = "Ahmed",  
  37.                   LastName = "Abdi",  
  38.                   Email = "[email protected]"  
  39.               };  
  40.   
  41.               var userResult = await _userMgr.CreateAsync(user, "Ahm3dia@!");  
  42.               var roleResult = await _userMgr.AddToRoleAsync(user, "Admin");  
  43.               var claimResult = await _userMgr.AddClaimAsync(user, new Claim("SuperUser""True"));  
  44.   
  45.               if (!userResult.Succeeded || !roleResult.Succeeded || !claimResult.Succeeded)  
  46.               {  
  47.                   throw new InvalidOperationException("Failed to build user or role");  
  48.               }  
  49.           }  
  50.   
  51.           if (!_ctx.Dishes.Any())  
  52.           {  
  53.               _ctx.AddRange(_sample);  
  54.               await _ctx.SaveChangesAsync();  
  55.           }  
  56.       }  
  57.       List<Dish> _sample = new List<Dish>  
  58.       {  
  59.                   new Dish()  
  60.                   {  
  61.                       DishName = "Vadonut",  
  62.                       DishLabel = "Hot",  
  63.                       Category = "appetizer",  
  64.                       Price = 1,  
  65.                       Description = "A quintessential ConFusion experience, is it a vada or is it a donut?",  
  66.                       ImageUrl = "/images/vadonut.jpg",  
  67.                   },  
  68.   
  69.  //Code ommited  

I have omitted some of the Dish Objects for brevity but you can download the project at the top of the post.

Step 7 Dependency Injection

Next, we are going to register the repository in startup.cs. Inside the configureservices method, we added the interface and concrete class as scoped dependency, and if you look the concrete class CrRepository, you can see that it needs CrContext which is dbcontext object from Entity framework. Let’s register it. The next thing we are going to do is to register CRInitializer as transient. However, to run CRInitializer, we need to configure it in ConfigureMethod. First, we added it to the list of parameters and called Seed which is an awaitable task. Next, we added the Identity in service collection and then configure it in ConfigureMethod by using UseIdentity before the UseMVC because we want to protect the MVC request from unauthorized or unauthenticated users. For more information about DI, see here.

  1. // This method gets called by the runtime. Use this method to add services to the container.  
  2.      public void ConfigureServices(IServiceCollection services)  
  3.      {  
  4.          services.AddSingleton(_config);  
  5.          services.AddDbContext<CrContext>(ServiceLifetime.Scoped);  
  6.          services.AddScoped<ICrRepository, CrRepository>();  
  7.          services.AddTransient<CRInitializer>();  
  8.          services.AddIdentity<CrUser, IdentityRole>()  
  9.                  .AddEntityFrameworkStores<CrContext>();  
  10.          services.AddAutoMapper();  
  11.          // Add framework services.  
  12.          services.AddMvc();  
  13.      }  
  14.     // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.  
  15.      public void Configure(IApplicationBuilder app,   
  16.                           IHostingEnvironment env,  
  17.                           ILoggerFactory loggerFactory,  
  18.                           CRInitializer seeder)  
  19.      {  
  20.          loggerFactory.AddConsole(_config.GetSection("Logging"));  
  21.          loggerFactory.AddDebug();  
  22.          app.UseIdentity();  
  23.          app.UseMvc();  
  24.          seeder.Seed().Wait();  
  25.      }  

Step 8

Next, we are going to add the connection string in appsetting.json file. I’m using SQL Server Express 2014, so you may need to change the name of the server if you haven’t already installed it.

  1. {  
  2.   "Data": {  
  3.     "ConnectionString""Data Source=.\\SQLEXPRESS;Initial   Catalog=confusionresturantDb;Integrated Security=True;Connect Timeout=30;Encrypt=False;TrustServerCertificate=True;ApplicationIntent=ReadWrite;MultiSubnetFailover=False"  
  4.   },  
  5.   "Logging": {  
  6.     "IncludeScopes"false,  
  7.     "LogLevel": {  
  8.       "Default""Warning"  
  9.     }  
  10.   }  
  11. }  

Step 9

To work with migrations in CLI, let’s first add Microsoft.EntityFramworkCore.Tools.DotNet Package in csproj file.

  1. <ItemGroup>  
  2.     <DotNetCliToolReference Include="Microsoft.EntityFrameworkCore.Tools.DotNet" Version="1.0.0" />  
  3.   </ItemGroup>  

In the terminal type,

  • dotnet ef migrations add init
  • dotnet ef database update

Step 10 Adding a View Models

If we send GET request later when we Create DishesController and CommentController classes, the response returns all the fields of the dish and the comment. I want to get rid of unnecessary fields, so how do I do this? We will add new classes and name as a DishModel and CommentModel, so that the Controller will return the DishModel or CommentModel rather than entities. The next thing we’re going to do is to map the entities to models, we will first add an extension to automapper in csproj file

  1. <PackageReference Include="AutoMapper.Extensions.Microsoft.DependencyInjection" Version="1.2.0"/> 

Now In the startup.cs file under the configureMethod we will add new service

services.AddAutoMapper();

In the models folder add new class and name it as DishModel .

  1. public class DishModel  
  2.   {  
  3.       public string DishName { get; set; }  
  4.       public string DishLabel { get; set; }  
  5.       public decimal Price { get; set; }  
  6.       public string ImageUrl { get; set; }  
  7.   }  

Add new class and name it as CommentModel.

  1. public class CommentModel  
  2.   {    
  3.       public int CommentId { get; set; }  
  4.       public string Rating { get; set; }  
  5.       public string DishComment { get; set; }  
  6.       public DateTime date { get; set; }  
  7.       public string Author { get; set; }  
  8.   }  

Add new class and name it DishMappingProfile

  1. public DishMappingProfile()  
  2. {  
  3.     CreateMap<Dish, DishModel>()  
  4.     .ReverseMap();  
  5.       
  6.     CreateMap<Comment, CommentModel>()  
  7.      .ForMember(m => m.Author, opt => opt.ResolveUsing(u => u.User.UserName.ToString()))  
  8.      .ReverseMap();  
  9. }  

HTTP Methods

First, reading a resource, we use Get method. For creating a new resource we use POST method and if we want to update resource two options are available, the first one is PUT which is used for full updates, for example a put request to /api/dishes/dishId would update dish with that ID and if field is missing it will be empty in the response, but if you need to partially update to resource then we will use PATCH method. The last http method we need is DELETE method and it’s used to delete an existing resource.

Step 11 Creating the Controllers

Now we are going to add new class “DishesController” and pass the dependencies in the constructor parameters.

  1. [Route("api/[controller]")]  
  2.      public class DishesController : Controller  
  3.      {  
  4.         private readonly ICrRepository _repo;  
  5.         private readonly IMapper _mapper;  
  6.         public DishesController(ICrRepository repo , IMapper mapper)  
  7.         {  
  8.             _repo= repo;  
  9.             _mapper = mapper;  

Step 12

Get Dishes

  1. [HttpGet("")]  
  2.        public IActionResult Get()  
  3.        {  
  4.         var dishes = _repo.GetDishes();  
  5.         return Ok(Mapper.Map<IEnumerable<DishModel>>(dishes));  
  6.        }  

Step 13 

Get Dish

  1. [HttpGet("{id:int}", Name = "DishGet")]  
  2.        public IActionResult Get(int id)  
  3.        {  
  4.            try  
  5.            {  
  6.             var dish = _repo.GetDishWithComments(id);  
  7.             if(dish == nullreturn NotFound($"Dish of {id} was not found");  
  8.             return Ok(dish);  
  9.            }  
  10.            catch(Exception)  
  11.            {}  
  12.            return BadRequest("Could not found Dish");  
  13.        }  

Step 14

Dish by Category

  1. [HttpGet("{category}")]  
  2.     public IActionResult GetCategory(string category)  
  3.     {  
  4.         try  
  5.         {  
  6.           var dish = _repo.GetDishByCategory(category);  
  7.           return Ok(Mapper.Map<IEnumerable<DishModel>>(dish));  
  8.         }  
  9.         catch(Exception)  
  10.         {}  
  11.         return BadRequest("Couldn't found dish");  
  12.     }  

Step 15

Inserting a Dish

  1. [HttpPost]  
  2.   public async Task<IActionResult> Post([FromBody] Dish model)  
  3.   {  
  4.      try  
  5.      {  
  6.          if(!ModelState.IsValid) return BadRequest(ModelState);  
  7.          _repo.Add(model);  
  8.          if( await _repo.SaveAllAsync())  
  9.          {  
  10.            var newUri = Url.Link("DishGet"new{id = model.DishId});  
  11.           return Created(newUri, model);  
  12.          }  
  13.      }  
  14.      catch(Exception)  
  15.      {}  
  16.      return BadRequest("Could not post Dish");  
  17.   }  

Step 16

Updating a Dish

  1. [HttpPut("{id}")]  
  2.  public async Task<IActionResult> Put (int id, [FromBody] DishModel model)  
  3.  {  
  4.      try  
  5.      {  
  6.       if(!ModelState.IsValid) return BadRequest(ModelState);  
  7.       var oldDish = _repo.GetDish(id);  
  8.       if(oldDish == nullreturn NotFound($"Couldn't find a dish of {id}");  
  9.       _mapper.Map(model, oldDish);  
  10.   
  11.       if (await _repo.SaveAllAsync())  
  12.       {  
  13.           return Ok(_mapper.Map<DishModel>(oldDish));  
  14.       }  
  15.      }  
  16.      catch(Exception)  
  17.      {}  
  18.      return BadRequest("Could not update dish");  
  19.  }  

Step 17

Deleting a Dish

  1. [HttpDelete("{id}")]  
  2.        public async Task<IActionResult> Delete(int id)  
  3.        {  
  4.         try  
  5.         {  
  6.           var oldDish = _repo.GetDish(id);  
  7.           if(oldDish == nullreturn NotFound($"Couldn’t found Dish of id {id}");  
  8.           _repo.Delete(oldDish);  
  9.           if(await _repo.SaveAllAsync())  
  10.           {  
  11.               return Ok();  
  12.           }  
  13.         }  
  14.         catch(Exception)  
  15.         {}  
  16.         return BadRequest("Couldn’t Delete Dish");  
  17.        }  

Step 18

Now we are going to add CommentsController and in this controller class, we are only working with post method in order to add comments. We also added an authorize attribute because we want to check that user is authenticated.

  1. [Route("api/dishes/{id}/comments", Name = "CommentGet")]  
  2.     public class CommentController : Controller  
  3.     {  
  4.         private ICrRepository _repo;  
  5.         private IMapper _mapper;  
  6.         private UserManager<CrUser> _userMgr;  
  7.   
  8.         public CommentController(ICrRepository repo,  
  9.                                 IMapper mapper,  
  10.                                 UserManager<CrUser> userMgr)  
  11.         {  
  12.             _repo = repo;  
  13.             _mapper = mapper;  
  14.             _userMgr = userMgr;  
  15.         }  
  16.   
  17.         [HttpPost]  
  18.         [Authorize]  
  19.         public async Task<IActionResult> Post(int id, [FromBody] CommentModel model)  
  20.         {  
  21.             try  
  22.             {  
  23.                 var dish = _repo.GetDish(id);  
  24.                 var comment = _mapper.Map<Comment>(model);  
  25.                 comment.Dish = dish;  
  26.                 var crUser = await _userMgr.FindByNameAsync(this.User.Identity.Name);  
  27.   
  28.                 if (crUser != null)  
  29.                 {  
  30.                     comment.User = crUser;  
  31.                     _repo.Add(comment);  
  32.                     if (await _repo.SaveAllAsync())  
  33.                     {  
  34.                         var url = Url.Link("CommentGet"new { id = model.CommentId });  
  35.                         return Created(url, _mapper.Map<CommentModel>(comment));  
  36.                     }  
  37.                 }  
  38.             }  
  39.             catch (Exception ex)  
  40.             {  
  41.                 _logger.LogError($"an error accured while posting comment {ex}");  
  42.             }  
  43.             return BadRequest("Could not post comment");  
  44.         }  
  45.     }  

Step 19 Adding Security

If we run the application and access the actions we added to the authorize attribute, we should get 401 (Unauthorized), so that in here we are going to add login functionality. Add new class in models folder and name it a Loginmodel

  1. public class LoginModel  
  2.  {  
  3.      [Required]  
  4.      public string UserName { get; set; }  
  5.      [Required]  
  6.      public string Password { get; set; }  
  7.  }  

Now we are going to add new controller, add new class, named auth controller .

  1. public class AuthController : Controller  
  2.     {  
  3.         private CrContext _context;  
  4.         private SignInManager<CrUser> _signInMgr;  
  5.         private ILogger<AuthController> _logger;  
  6.   
  7.         public AuthController(CrContext context, SignInManager<CrUser> signInMgr, ILogger<AuthController> logger)  
  8.        {  
  9.            _context = context;  
  10.            _signInMgr = signInMgr;  
  11.            _logger = logger;  
  12.        }  
  13.          
  14.        [HttpPost("api/Auth/login")]  
  15.        public async Task <IActionResult> Login ([FromBody] LoginModel model)  
  16.        {  
  17.          try  
  18.          {  
  19.         var result = await _signInMgr.PasswordSignInAsync(model.UserName , model.Password, falsefalse);  
  20.         if(result.Succeeded)  
  21.         {  
  22.             return Ok();  
  23.         }  
  24.          }  
  25.          catch(Exception ex)  
  26.          {  
  27.            _logger.LogError($"an Exception is thrown while logging in {ex}");  
  28.          }  
  29.          return BadRequest("Failed to login");  
  30.        }  
  31.     }  

Now let’s run the application and open the postman, copy http://localhost:5000/api/Dishes to Postman,

ASP.NET Core

  • Get Dishes by category

    ASP.NET Core

  • Get Dish

    ASP.NET Core

  • Insert Dish

    ASP.NET Core

  • Update Dish

Now we are going to update the price from 4 to 5,

ASP.NET Core

  • Delete Dish

    ASP.NET Core

  • Add Comment to a Dish

    ASP.NET Core

Conclusion

In this article, we learned how to create a web API with ASP.NET Core. First, we started with data layer and then created simple Web API project that has the CRUD functionality. I hope this article is very useful for all readers.