ContosoPets – ตัวอย่าง WebApi

  1. สร้างโปรเจ็กส์แบบ Web API
  2. Add a data store
  3. Add a controller
  4. Implement CRUD operations
  5. Test web API actions

1.สร้างโปรเจ็กส์แบบ Web API

สร้างโปรเจ็กส์แบบ Web API ชื่อ ContosoPets

2.Add a data store

สร้างโฟลเดอร์ Models และไฟล์ Models/Product.cs

ไฟล์ Models/Product.cs

using System.ComponentModel.DataAnnotations;

namespace ContosoPets.Models
{
    public class Product
    {
        public long Id { get; set; }

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

        [Required]
        [Range(minimum: 0.01, maximum: (double)decimal.MaxValue)]
        public decimal Price { get; set; }
    }
}

สร้างโฟลเดอร์ Data และไฟล์ Data/ContosoPetsContext.cs ไฟล์ Data/SeedData.cs

ไฟล์ Data/ContosoPetsContext.cs

using ContosoPets.Models;
using Microsoft.EntityFrameworkCore;

namespace ContosoPets.Data
{
    public class ContosoPetsContext : DbContext
    {
        public ContosoPetsContext(DbContextOptions<ContosoPetsContext> options)
            : base(options)
        {
        }

        public DbSet<Product> Products { get; set; }
    }
}

แก้ไขไฟล์ Startup.cs 

using ContosoPets.Data;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;

namespace ContosoPets
{
    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)
        {
            services.AddDbContext<ContosoPetsContext>(options =>
                options.UseInMemoryDatabase("ContosoPets"));
            services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }
            else
            {
                // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
                app.UseHsts();
            }

            app.UseHttpsRedirection();
            app.UseMvc();
        }
    }
}

ไฟล์ Data/SeedData.cs

using ContosoPets.Models;
using System.Linq;

namespace ContosoPets.Data
{
    public static class SeedData
    {
        public static void Initialize(ContosoPetsContext context)
        {
            if (!context.Products.Any())
            {
                context.Products.AddRange(
                    new Product
                    {
                        Name = "Squeaky Bone",
                        Price = 20.99m
                    },
                    new Product
                    {
                        Name = "Knotted Rope",
                        Price = 12.99m
                    }
                );

                context.SaveChanges();
            }
        }
    }
}

แก้ไขไฟล์ Program.cs

using ContosoPets.Data;
using Microsoft.AspNetCore;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using System;

namespace ContosoPets
{
    public class Program
    {
        public static void Main(string[] args)
        {
            var host = CreateWebHostBuilder(args).Build();
            SeedDatabase(host);
            host.Run();
        }

        public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
            WebHost.CreateDefaultBuilder(args)
                .UseStartup<Startup>();

        private static void SeedDatabase(IWebHost host)
        {
            var scopeFactory = host.Services.GetRequiredService<IServiceScopeFactory>();

            using (var scope = scopeFactory.CreateScope())
            {
                var context = scope.ServiceProvider.GetRequiredService<ContosoPetsContext>();

                if (context.Database.EnsureCreated())
                {
                    try
                    {
                        SeedData.Initialize(context);
                    }
                    catch (Exception ex)
                    {
                        var logger = scope.ServiceProvider.GetRequiredService<ILogger<Program>>();
                        logger.LogError(ex, "A database seeding error occurred.");
                    }
                }
            }
        }
    }
}

3.Add a controller

สร้างไฟล์ Controllers/ProductsController.cs

ถ้าสร้างแบบ API Controller – Empty

ไฟล์ Controllers/ProductsController.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;

namespace ContosoPets.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class ProductsController : ControllerBase
    {
    }
}

ถ้าสร้างแบบ API Controller with actions, using Entity Framework

ไฟล์ Controllers/ProductsController.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using ContosoPets.Data;
using ContosoPets.Models;

namespace ContosoPets.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class ProductsController : ControllerBase
    {
        private readonly ContosoPetsContext _context;

        public ProductsController(ContosoPetsContext context)
        {
            _context = context;
        }

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

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

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

            return product;
        }

        // PUT: api/Products/5
        [HttpPut("{id}")]
        public async Task<IActionResult> PutProduct(long id, Product product)
        {
            if (id != product.Id)
            {
                return BadRequest();
            }

            _context.Entry(product).State = EntityState.Modified;

            try
            {
                await _context.SaveChangesAsync();
            }
            catch (DbUpdateConcurrencyException)
            {
                if (!ProductExists(id))
                {
                    return NotFound();
                }
                else
                {
                    throw;
                }
            }

            return NoContent();
        }

        // POST: api/Products
        [HttpPost]
        public async Task<ActionResult<Product>> PostProduct(Product product)
        {
            _context.Products.Add(product);
            await _context.SaveChangesAsync();

            return CreatedAtAction("GetProduct", new { id = product.Id }, product);
        }

        // DELETE: api/Products/5
        [HttpDelete("{id}")]
        public async Task<ActionResult<Product>> DeleteProduct(long id)
        {
            var product = await _context.Products.FindAsync(id);
            if (product == null)
            {
                return NotFound();
            }

            _context.Products.Remove(product);
            await _context.SaveChangesAsync();

            return product;
        }

        private bool ProductExists(long id)
        {
            return _context.Products.Any(e => e.Id == id);
        }
    }
}

แก้ไขไฟล์ Controllers/ProductsController.cs

using ContosoPets.Data;
using ContosoPets.Models;
using Microsoft.AspNetCore.Mvc;
using System.Collections.Generic;
using System.Linq;

namespace ContosoPets.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class ProductsController : ControllerBase
    {
        private readonly ContosoPetsContext _context;

        public ProductsController(ContosoPetsContext context)
        {
            _context = context;
        }

        [HttpGet]
        public ActionResult<List<Product>> GetAll() =>
            _context.Products.ToList();

        // GET by ID action

        // POST action

        // PUT action

        // DELETE action
    }
}

ลองรันดู

> curl -k -s https://localhost:44361/api/values
["value1","value2"]

4.Implement CRUD operations

HTTP action verb CRUD operation ASP.NET Core attribute
POSTCreate[HttpPost]
GETRead[HttpGet]
PUTUpdate[HttpPut]
DELETEDelete[HttpDelete]

Retrieve a product

[HttpGet("{id}")]
public async Task<ActionResult<Product>> GetById(long id)
{
    var product = await _context.Products.FindAsync(id);

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

    return product;
}

ถ้าไม่เจอโปรดัก (product == null) จะ return 404 ด้วย NotFound()

แต่ถ้าเจอโปรดักจะ return 200

Add a product

[HttpPost]
public async Task<ActionResult<Product>> Create(Product product)
{
    _context.Products.Add(product);
    await _context.SaveChangesAsync();

    // CreatedAtAction uses the action name to generate 
    // a Location HTTP response header with a URL to the newly created product.
    return CreatedAtAction(nameof(GetById), new { id = product.Id }, product);
}

ถ้าเพิ่มโปรดักสำเร็จจะ return 201

แต่ถ้าไม่สำเร็จจะ return 400

Modify a product

[HttpPut("{id}")]
public async Task<IActionResult> Update(long id, Product product)
{
    if (id != product.Id)
    {
        return BadRequest();
    }

    _context.Entry(product).State = EntityState.Modified;
    await _context.SaveChangesAsync();

    return NoContent();
}

ถ้าไม่เจอโปรดักจะ return 400 ด้วย BadRequest()

ถ้าเจอ แต่อัพเดทโปรดักไม่สำเร็จจะ return 400

แต่ถ้าอัพเดทโปรดักสำเร็จจะ return 204 ด้วย NoContent()

Remove a product

[HttpDelete("{id}")]
public async Task<IActionResult> Delete(long id)
{
    var product = await _context.Products.FindAsync(id);

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

    _context.Products.Remove(product);
    await _context.SaveChangesAsync();

    return NoContent();
}

ถ้าไม่เจอโปรดัก (product == null) จะ return 404 ด้วย NotFound()

แต่ถ้าลบโปรดักสำเร็จจะ return 204 ด้วย NoContent()

5.Test web API actions

1.ส่ง invalid HTTP POST request

curl -i -k \
    -H "Content-Type: application/json" \
    -d "{\"name\":\"Plush Squirrel\",\"price\":0.00}" \
    https://localhost:5001/api/Products
  • -i displays the HTTP response headers.
  • -d implies an HTTP POST operation and defines the request body.
  • -H indicates that the request body is in JSON format. The header’s value overrides the default content type of application/x-www-form-urlencoded.

พอส่ง request นี้ออกไปจะ Error 400 เพราะ Price ต่ำกว่า minimum (0.01)

> curl -i -k  -H "Content-Type: application/json" -d "{\"name\":\"Plush Squirrel\",\"price\":0.00}" https://localhost:44361/api/Products
HTTP/1.1 400 Bad Request
Transfer-Encoding: chunked
Content-Type: application/problem+json; charset=utf-8
Server: Microsoft-IIS/10.0
X-SourceFiles: =?UTF-8?B?QzpcVXNlcnNccGhhaXNhcm5zXHNvdXJjZVxyZXBvM1xDb250b3NvUGV0c1xDb250b3NvUGV0c1xhcGlcUHJvZHVjdHM=?=
X-Powered-By: ASP.NET
Date: Tue, 18 Jun 2019 04:51:38 GMT

{"errors":{"Price":["The field Price must be between 0.01 and 7.92281625142643E+28."]},"title":"One or more validation errors occurred.","status":400,"traceId":"80000002-0005-ff00-b63f-84710c7967bb"}

format JSON ให้ดูง่าย

{
    "errors":{
        "Price":["The field Price must be between 0.01 and 7.92281625142643E+28."]
    },
    "title":"One or more validation errors occurred.",
    "status":400,
    "traceId":"80000002-0005-ff00-b63f-84710c7967bb"
}

2.ส่ง valid HTTP POST request

curl -i -k \
    -H "Content-Type: application/json" \
    -d "{\"name\":\"Plush Squirrel\",\"price\":12.99}" \
    https://localhost:5001/api/Products

ส่ง request ออกไปจะได้ 201

> curl -i -k -H "Content-Type: application/json"  -d "{\"name\":\"Plush Squirrel\",\"price\":12.99}"  https://localhost:44361/api/Products
HTTP/1.1 201 Created
Transfer-Encoding: chunked
Content-Type: application/json; charset=utf-8
Location: https://localhost:44361/api/Products/3
Server: Microsoft-IIS/10.0
X-SourceFiles: =?UTF-8?B?QzpcVXNlcnNccGhhaXNhcm5zXHNvdXJjZVxyZXBvM1xDb250b3NvUGV0c1xDb250b3NvUGV0c1xhcGlcUHJvZHVjdHM=?=
X-Powered-By: ASP.NET
Date: Tue, 18 Jun 2019 04:59:24 GMT

{"id":3,"name":"Plush Squirrel","price":12.99}

บรรทัดที่ 5 Location แสดง url ในการเรียกดู item ที่เราสร้างขึ้นมา https://localhost:44361/api/Products/3

3.ส่ง HTTP GET request

curl -k -s https://localhost:5001/api/Products/3 | jq

จะได้

> curl -k -s https://localhost:44361/api/Products/3
{"id":3,"name":"Plush Squirrel","price":12.99}

4.ส่ง HTTP PUT request

curl -i -k \
    -X PUT \
    -H "Content-Type: application/json" \
    -d "{\"id\":2,\"name\":\"Knotted Rope\",\"price\":14.99}" \
    https://localhost:5001/api/Products/2
> curl -i -k  -X PUT -H "Content-Type: application/json" -d "{\"id\":2,\"name\":\"Knotted Rope\",\"price\":14.99}"  https://localhost:44361/api/Products/2
HTTP/1.1 204 No Content
Server: Microsoft-IIS/10.0
X-SourceFiles: =?UTF-8?B?QzpcVXNlcnNccGhhaXNhcm5zXHNvdXJjZVxyZXBvM1xDb250b3NvUGV0c1xDb250b3NvUGV0c1xhcGlcUHJvZHVjdHNcMg==?=
X-Powered-By: ASP.NET
Date: Tue, 18 Jun 2019 05:05:48 GMT

ได้ 204  คืออัพเดทโปรดักสำเร็จ

5.ส่ง HTTP DELETE request

curl -i -k -X DELETE https://localhost:5001/api/Products/1
>curl -i -k -X DELETE https://localhost:44361/api/Products/1
HTTP/1.1 204 No Content
Server: Microsoft-IIS/10.0
X-SourceFiles: =?UTF-8?B?QzpcVXNlcnNccGhhaXNhcm5zXHNvdXJjZVxyZXBvM1xDb250b3NvUGV0c1xDb250b3NvUGV0c1xhcGlcUHJvZHVjdHNcMQ==?=
X-Powered-By: ASP.NET
Date: Tue, 18 Jun 2019 05:11:17 GMT

ได้ 204 คือลบโปรดักสำเร็จ

ตอนนี้ลบ Product id 1 แล้ว ถ้าเรียก id 1 จะได้ 404

> curl -k -s https://localhost:44361/api/Products/1
{
    "type":"https://tools.ietf.org/html/rfc7231#section-6.5.4",
    "title":"Not Found",
    "status":404,
    "traceId":"80000002-0004-ff00-b63f-84710c7967bb"
}

6.ส่ง HTTP GET request

curl -k -s https://localhost:44361/api/Products | jq
> curl -k -s https://localhost:44361/api/Products
[
    {
        "id":2,
        "name":"Knotted Rope",
        "price":14.99
    },
    {
        "id":3,
        "name":"Plush Squirrel",
        "price":12.99
    }
]

ตอนนี้เหลือแค่ Product 2 และ 3