Return to Tech/dotnet

.NET Core - WebAPI

.NET Coreの WEB API実装について

以下は実行イメージ例 API形式でtodoデータを扱う
todoメモサンプル

以下作成例

Modelsディレクトリを作成し TodoItem.cs を配置します。
TodoItem.cs
namespace kshellnet.Models {
  public class TodoItem {
    public long Id { get; set; }
    public string Name { get; set; }
    public bool IsComplete { get; set; }
  }
}

Modelsディレクトリに TodoContext.cs を配置します
TodoContext.cs
using Microsoft.EntityFrameworkCore;

namespace kshellnet.Models {
  public class TodoContext : DbContext {
    public TodoContext(DbContextOptions<> options) : base(options) {
    }

    public DbSet TodoItems { get; set; }
  }
}

Startup.csの ConfigureService()内に以下の要領でコンテキスト情報を追加します。
Startup.cs
public void ConfigureServices(IServiceCollection services) {
  services.AddDbContext(options => options.UseSqlite("Data Source=todo.db"));

  // APIコントローラとして実装するので以下も追加
  services.AddControllers();
}


Startup.csの Configure()内にエンドポイントの設定を追加します。
Startup.cs
public void Configure(IApplicationBuilder app, IWebHostEnvironment env) {
  app.UseEndpoints(endpoints => {
    endpoints.MapRazorPages();

    // 以下を追加します
    endpoints.MapControllers();
  })
}

DbContextを用意した後は以下を実行
dotnet ef migrations add Initial --context TodoContext

結果
Build started...
Build succeeded.
Done. To undo this action, use 'ef migrations remove'

Migration\Todo\下を確認

TIMESTAMP_Initial.csが作成されます
using Microsoft.EntityFrameworkCore.Migrations;

namespace kshell.net.Migrations.Todo {
  public partial class Initial : Migration {
    protected override void Up(MigrationBuilder migrationBuilder) {
      migrationBuilder.CreateTable(
        name: "TodoItems",
        columns: table => new {
          Id = table.Column(nullable: false)
              .Annotation("Sqlite:Autoincrement", true),
          Name = table.Column(nullable: true),
          IsComplete = table.Column(nullable: false)
        },
        constraints: table => {
          table.PrimaryKey("PK_TodoItems", x => x.Id);
        }
      );
    }

    protected override void Down(MigrationBuilder migrationBuilder) {
      migrationBuilder.DropTable(
        migrationBuilder.DropTable( name: "TodoItems" );
      )
    }
  }
}


Updateする

dotnet ef database update --context TodoContext

結果
Build started...
Build succeeded.
Done.

todo.db ※SQLiteのdbが作成されます。


コントローラーをスキャフォールドする

前準備含む
> dotnet add package Microsoft.VisualStudio.Web.CodeGeneration.Design
> dotnet add package Microsoft.EntityFrameworkCore.Design
> dotnet tool install --global dotnet-aspnet-codegenerator
次のコマンドを使用してツールを呼び出せます。dotnet-aspnet-codegenerator
ツール 'dotnet-aspnet-codegenerator' (バージョン '3.1.3')が正常にインストールされました。


コントローラを作成
> dotnet aspnet-codegenerator controller -name TodoItemsController -async -api -m TodoItem -dc TodoContext -outDir Controllers
結果
Building project ...
Finding the generator 'controller'...
Running the generator 'controller'...
Attempting to compile the application in memory.
Attempting to figure out the EntityFramework metadata for the model and DbContext: 'TodoItem'
Added Controller : '\Controllers\TodoItemsController.cs'.
RunTime 00:00:15.14


出来上がったコントローラは以下のような感じ
TodoItemsController.cs
namespace kshellnet.Controllers {
  [Route("api/[controller]")]
  [ApiController]
  public class TodoItemsController : ControllerBase {
    private readonly TodoContext _context;
    public TodoItemsController(TodoContext context) { _context = context; }
  }

  // GET: api/TodoItems
  [HttpGet]
  public async Task>> GetTodoItems() {
    return await _context.TodoItems.ToListAsync();
  }

  // GET: api/TodoItems/5
  [HttpGet("{id}")]
  public async Task> GetTodoItem(long id) {
    var todoItem = await _context.TodoItems.FindAsync(id);
    if (todoItem == null) { return NotFound(); }
    return todoItem;
  }

  // PUT: api/TodoItems/5
  // To protect from overposting attacks, enable the specific properties you want to bind to, 
  // for more details, see https://go.microsoft.com/fwlink/?linkid=2123754. 
  [HttpPut("{id}")]
  public async Task PutTodoItem(long id, TodoItem todoItem) {
    if (id != todoItem.Id) { return BadRequest(); }
    _context.Entry(todoItem).State = EntityState.Modified;

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

  // POST: api/TodoItems
  // 過剰ポストのアタック対処方は上のコメント参照
  [HttpPost]
  public async Task> PostTodoItem(TodoItem todoItem) {
    _context.TodoItems.Add(todoItem);
    await _context.SaveChangesAsync();

    return CreatedAtAction("GetTodoItem", new { id = todoItem.Id }, todoItem);
  }

  // DELETE: api/TodoItems/5
  [HttpDelete("{id}")]
   public async Task> DeleteTodoItem(long id) {
     var todoItem = await _context.TodoItems.FindAsync(id);
     if (todoItem == null) { return NotFound(); }

     _context.TodoItems.Remove(todoItem);
     await _context.SaveChangesAsync();

     return todoItem;
   }

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


wwwroot下に /api/TodoItems/にアクセスするためのhtml, jsを配置します。
file: todo.html
<!DOCTYPE html>

<html>
<head>
    <meta charset="utf-8">
    <title>ToDo</title>
    <link rel="stylesheet" href="css/site.css" />
</head>
<body>
    <h1>ToDo</h1>
    <h3>Add</h3>
    <form action="javascript:void(0);" method="POST" onsubmit="addItem()">
        <input type="text" id="add-name" placeholder="New todo">
        <input type="submit" value="Add">
    </form>

    <div id="editForm">
        <h3>編集</h3>
        <form action="javascript:void(0);" onsubmit="updateItem()">
            <input type="hidden" id="edit-id">
            <input type="checkbox" id="edit-isComplete">
            <input type="text" id="edit-name">
            <input type="submit" value="save">
            <a onclick="closeInput()" aria-label="閉じる">✖</a>
        </form>
    </div>

    <p id="counter"></p>

    <table>
        <tr>
            <th>完了済み?</th>
            <th>Name</th>
            <th><</th>
            <th&ht</th>
        </tr>
        <tbody id="todos"></tbody>
    </table>

    <script src="js/todo.js" asp-append-version="true"></script>
    <script type="text/javascript">
        getItems();
    </script>
</body>
</html>


todo.js
const uri = 'api/TodoItems';
let todos = [];

function getItems() {
    fetch(uri)
        .then(response => response.json())
        .then(data => _displayItems(data))
        .catch(error => console.error('Unable to get items.', error));
}

function addItem() {
    const addNameTextbox = document.getElementById('add-name');

    const item = {
        isComplete: false,
        name: addNameTextbox.value.trim()
    };

    fetch(uri, {
        method: 'POST',
        headers: {
            'Accept': 'application/json',
            'Content-Type': 'application/json'
        },
        body: JSON.stringify(item)
    })
        .then(response => response.json())
        .then(() => {
            getItems();
            addNameTextbox.value = '';
        })
        .catch(error => console.error('Unable to add item.', error));
}

function deleteItem(id) {
    fetch(`${uri}/${id}`, {
        method: 'DELETE'
    })
        .then(() => getItems())
        .catch(error => console.error('Unable to delete item.', error));
}

function displayEditForm(id) {
    const item = todos.find(item => item.id === id);

    document.getElementById('edit-name').value = item.name;
    document.getElementById('edit-id').value = item.id;
    document.getElementById('edit-isComplete').checked = item.isComplete;
    document.getElementById('editForm').style.display = 'block';
}

function updateItem() {
    const itemId = document.getElementById('edit-id').value;
    const item = {
        id: parseInt(itemId, 10),
        isComplete: document.getElementById('edit-isComplete').checked,
        name: document.getElementById('edit-name').value.trim()
    };

    fetch(`${uri}/${itemId}`, {
        method: 'PUT',
        headers: {
            'Accept': 'application/json',
            'Content-Type': 'application/json'
        },
        body: JSON.stringify(item)
    })
        .then(() => getItems())
        .catch(error => console.error('Unable to update item.', error));

    closeInput();

    return false;
}

function closeInput() {
    document.getElementById('editForm').style.display = 'none';
}

function _displayCount(itemCount) {
    const name = (itemCount === 1) ? 'to-do' : 'to-dos';

    document.getElementById('counter').innerText = `${itemCount} ${name}`;
}

function _displayItems(data) {
    const tBody = document.getElementById('todos');
    tBody.innerHTML = '';

    _displayCount(data.length);

    const button = document.createElement('button');

    data.forEach(item => {
        let isCompleteCheckbox = document.createElement('input');
        isCompleteCheckbox.type = 'checkbox';
        isCompleteCheckbox.disabled = true;
        isCompleteCheckbox.checked = item.isComplete;

        let editButton = button.cloneNode(false);
        editButton.innerText = 'Edit';
        editButton.setAttribute('onClick', `displayEditForm(${item.id})`);

        let deleteButton = button.cloneNode(false);
        deleteButton.innerText = 'Delete';
        deleteButton.setAttribute('onclick', `deleteItem(${item.id})`);

        let tr = tBody.insertRow();

        let td1 = tr.insertCell(0);

        td1.appendChild(isCompleteCheckbox);

        let td2 = tr.insertCell(1);
        let textNode = document.createTextNode(item.name);
        td2.appendChild(textNode);

        let td3 = tr.insertCell(2);
        td3.appendChild(editButton);

        let td4 = tr.insertCell(3);
        td4.appendChild(deleteButton);
    });

    todos = data;
}

Return to Tech/dotnet