Angluar + WebAPI CRUD演練

Angluar

最近在熟悉Angluar的開發,CRUD其實是最好入手的例子與範例,在此用一個Student的單一TABLE來做CRUD做一個前後端的串聯學習。

在Visual Studio 2022建立一個WebDemo專案(新增ASP.net Core WebAPI),進行建立Model資料夾,並新增Student。

    [Table("Student")]
    public class Student
    {
        [Key]
        public int Id { get; set; }
        public string? Name { get; set; }
        public int Age { get; set; }
        public string? Gender { get; set; }
    }

新增一個Repository資料夾,並在底下新增一個Interface資料夾,並新增IGenericReposiotry.cs

    public interface IGenericReposiotry<TEntity> where TEntity : class
    {

        IEnumerable<TEntity> GetAll();

        IEnumerable<TEntity> Get(Expression<Func<TEntity, bool>> filter);

        IEnumerable<TEntity> Get<TKey>(Expression<Func<TEntity, bool>> filter, int pageIndex, int pageSize, Expression<Func<TEntity, TKey>> sortKeySelector, bool isAsc = true);

        void Insert(TEntity entity);

        void Delete(TEntity entityToDelete);

        void Update(TEntity entityToUpdate);

        int SaveChanges();

    }

最後新增套件

  1. Microsoft.EntityFrameworkCore.SqlServer
  2. Microsoft.EntityFrameworkCore.Tools

新增StudentContext.cs

    public class StudentContext:DbContext
    {
        public StudentContext(DbContextOptions<StudentContext> options):base(options) 
        {
        }

        public DbSet<Student> Students { get; set; }
    }

在appsettings.json

{
  "ConnectionStrings": {
    "DefaultDatabase": "server=(LocalDb)\\MSSQLLocalDB;database=myDb;trusted_connection=true;"
    //  "DefaultDatabase": "server=localhost;database=myDb;trusted_connection=true;TrustServerCertificate=true;"

  },
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*"
}

 

在Repository新增GenericReposiotry.cs

   public class GenericRepository<TEntity> : IGenericReposiotry<TEntity> where TEntity : class
    {
        public readonly StudentContext _context;
        public DbSet<TEntity> _dbSet;

        public GenericRepository(StudentContext context)
        {
            this._context = context;
            _dbSet = _context.Set<TEntity>();
            //  印出EF 產生SQL語法監看效能
            //this._context.Database.Log = (log) => Debug.WriteLine(log); 
        }


        public IEnumerable<TEntity> GetAll()
        {
            return _dbSet.AsQueryable();
        }


        public IEnumerable<TEntity> Get(Expression<Func<TEntity, bool>> filter)
        {
            return _dbSet.Where(filter).AsQueryable();
        }
        public IEnumerable<TEntity> Get<TKey>(Expression<Func<TEntity, bool>> filter, int pageIndex, int pageSize, Expression<Func<TEntity, TKey>> sortKeySelector, bool isAsc = true)
        {

            if (isAsc)
            {
                return _dbSet
                    .Where(filter)
                    .OrderBy(sortKeySelector)
                    .Skip(pageSize * (pageIndex - 1))
                    .Take(pageSize).AsQueryable();
            }
            else
            {
                return _dbSet
                    .Where(filter)
                    .OrderByDescending(sortKeySelector)
                    .Skip(pageSize * (pageIndex - 1))
                    .Take(pageSize).AsQueryable();
            }
        }

        public void Insert(TEntity entity)
        {
            _dbSet.Add(entity);
        }

        public void Delete(object id)
        {
            TEntity entityToDelete = _dbSet.Find(id);
            Delete(entityToDelete);
        }

        public void Delete(TEntity entityToDelete)
        {
            if (_context.Entry(entityToDelete).State == EntityState.Deleted)
            {
                _dbSet.Attach(entityToDelete);
            }
            _dbSet.Remove(entityToDelete);
        }

        public void Update(TEntity entityToUpdate)
        {
            _dbSet.Attach(entityToUpdate);
            _context.Entry(entityToUpdate).State = EntityState.Modified;
        }

        public int SaveChanges()
        {

            //try
            //{
            return _context.SaveChanges();
            //}

            //catch (DbEntityValidationException ex)
            //{
            //    // Retrieve the error messages as a list of strings.
            //    var errorMessages = ex.EntityValidationErrors
            //            .SelectMany(x => x.ValidationErrors)
            //            .Select(x => x.ErrorMessage);

            //    // Join the list to a single string.
            //    var fullErrorMessage = string.Join("; ", errorMessages);

            //    // Combine the original exception message with the new one.
            //    var exceptionMessage =
            //              string.Concat(ex.Message, " The validation errors are: ", fullErrorMessage);

            //    // Throw a new DbEntityValidationException with the improved exception message.
            //    throw new DbEntityValidationException(exceptionMessage, ex.EntityValidationErrors);
            //}
        }

    }

 

補充知識:為什麼要定義GenericRepository。例如,如果我們有Employee、Product和Customer等三個實體,那麼我們需要建立EmployeeRepository、ProductRepository和CustomerRepository等三個Repository。

這實際上是在做重複的工作,特別是如果所有Repository都將執行相同類型的工作( CRUD 操作)。這違反了 DRY(不要重複自己)原則,因為我們在每個Repository中一次又一次地重複相同的Code。為了解決上述問題,Generic Repository Pattern才會誕生。

新增IStudentRepository並繼承IGenericRepository

    public interface IStudentRepository : IGenericReposiotry<Student>
    {
    }

新增StudentRepository

    public class StudentRepository : GenericRepository<Student>, IStudentRepository
    {
        public StudentRepository(StudentContext dbContext) : base(dbContext)
        {

        }
    }

進行Entity Framework Core

add-migration init
update-database

注入部分

using Microsoft.EntityFrameworkCore;
using WebDemoAPI.Model;
using WebDemoAPI.Repository.Interface;
using WebDemoAPI.Repository;

var builder = WebApplication.CreateBuilder(args);

string connString = builder.Configuration.GetConnectionString("DefaultDatabase");
// Add services to the container.

builder.Services.AddDbContext<StudentContext>(options => options.UseSqlServer(connString));
builder.Services.AddScoped(typeof(IGenericReposiotry<>), typeof(GenericRepository<>));
builder.Services.AddScoped<IStudentRepository, StudentRepository>();

builder.Services.AddControllers();
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

var app = builder.Build();

// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}
//加入CORS
app.UseCors(builder => builder
       .AllowAnyHeader()
       .AllowAnyMethod()
       .AllowAnyOrigin()
    );
app.UseAuthorization();

app.MapControllers();

app.Run();

 

Controller部分

    [Route("api/[controller]/[action]")]
    [ApiController]
    public class StudentController : ControllerBase
    {
        private readonly IStudentRepository _studentRepository;
        public StudentController(IStudentRepository studentRepository)
        {
            _studentRepository = studentRepository;
        }

        [HttpGet]
        public IActionResult GetStudents()
        {
            var students = _studentRepository.GetAll().ToList();
            return Ok(students);
        }
        [HttpPost]
        public IActionResult Post(Student payload)
        {
            _studentRepository.Insert(payload);
            _studentRepository.SaveChanges();
            return Ok(payload);
        }

        [HttpPut]
        public IActionResult Put(Student payload)
        {
            _studentRepository.Update(payload);
            _studentRepository.SaveChanges();
            return Ok(payload);
        }

        [HttpDelete]
        public IActionResult Delete(int id)
        {

            var result = _studentRepository.Get(x => x.Id == id).FirstOrDefault();
            if (result == null)
            {
                return NotFound();
            }
            _studentRepository.Delete(result);
            _studentRepository.SaveChanges();
            return Ok();

        }

    }

 

API開發完之後,接者開始寫前端的Angluar畫面!

ng new name-of-your-project


npm install bootstrap

 

到angluar.json部分設定

            "styles": [
              "src/styles.css",
              "node_modules/bootstrap/dist/css/bootstrap.min.css"
            ],
            "scripts":
            [
               "node_modules/bootstrap/dist/js/bootstrap.min.js"
            ]
ng g component student --skip-tests

ng g class student/student --skip-tests
export interface Student {
  id: number;
  name: string;
  age: number;
  gender: string;
}
		

 

ng g service student/student --skip-tests
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http'
import { Student } from './student';
@Injectable({
  providedIn: 'root'
})
export class StudentService {

  constructor(private httpClient:HttpClient) { }

   get(){
    return this.httpClient.get<Student[]>("http://localhost:5069/api/Student/GetStudents")
  }
}

student.component.ts

import { Component, OnInit } from '@angular/core';
import {Student} from './student';
import { StudentService } from './student.service';
@Component({
  selector: 'app-student',
  templateUrl: './student.component.html',
  styleUrls: ['./student.component.css']
})
export class StudentComponent implements OnInit {
   
  students:Student[] = [];
   constructor(private studentService:StudentService) { }
   ngOnInit(): void {
       this.get();
   }
   get()
   {
    this.studentService.get()
    .subscribe({
      next:(data) => {
        this.students = data;
      },
      error:(err) => {
        console.log(err);
      }
    })

   }
}

student.component.html

<table class="table">
  <thead>
    <tr>
      <th scope="col">編號</th>
      <th scope="col">姓名</th>
      <th scope="col">年齡</th>
      <th scope="col">性別</th>
    </tr>
  </thead>
  <tbody>
    <tr *ngFor="let sd of students">
     <th scope="row">{{sd.id}}</th>
     <td>{{sd.name}}</td>
     <td>{{sd.age}}</td>
     <td>{{sd.gender}}</td>
    </tr>
  </tbody>
</table>

<nav class="navbar navbar-expand-lg navbar-light bg-primary">
  <div class="container-fluid ">
    <a class="navbar-brand" href="#">範例程式</a>
    <div class="collapse navbar-collapse" id="navbarSupportedContent">
      <ul class="navbar-nav me-auto mb-2 mb-lg-0"></ul>
    </div>
  </div>
</nav>
<app-student></app-student>

app.module.ts如下

import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';

import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { HttpClientModule } from '@angular/common/http';
import { StudentComponent } from './student/student.component';


@NgModule({
  declarations: [
    AppComponent,
    StudentComponent
  ],
  imports: [
    BrowserModule,
    AppRoutingModule,
    HttpClientModule
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

新增功能串接在StudentService追加post method

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http'
import { Student } from './student';

@Injectable({
  providedIn: 'root'
})
export class StudentService {
  url = 'http://localhost:5141/api/Student/';
  constructor(private httpClient:HttpClient) { }
  get(){
   return this.httpClient.get<Student[]>(this.url + "GetStudents")
  }
  post(payload: Student) {
    return this.httpClient.post<Student>(
    this.url + 'Post',
    payload
    );
  }
}

student.component.ts

import { Component ,OnInit} from '@angular/core';
import { Student} from './student';
import { StudentService } from './student.service';
declare var window: any;
@Component({
  selector: 'app-student',
  templateUrl: './student.component.html',
  styleUrls: ['./student.component.css']
})
export class StudentComponent implements OnInit  {

  students:Student[] = [];
  addorupdatemodal: any;
  studentForm: Student = {
    age: 0,
    gender: 'Male',
    id: 0,
    name: '',
  };
  addorupdatemodalTitle: string = '';
   constructor(private studentService:StudentService) { }
   ngOnInit(): void {
       this.get();
        this.addorupdatemodal = new window.bootstrap.Modal(
        document.getElementById('addorupdatemodal')
      );
   }
   get()
   {
    this.studentService.get()
    .subscribe({
      next:(data) => {
        this.students = data;
      },
      error:(err) => {
        console.log(err);
      }
    })
   }

   openAddOrUpdateModal(studentId:number)
   {
        if(studentId ===0)
        {
            this.addorupdatemodalTitle = '新增';
            this.studentForm = {
              age: 0,
              gender: 'Male',
              id: 0,
              name: '',
        };
        this.addorupdatemodal.show();
        }
   }
    createorUpdateStudent() {
    if (this.studentForm.id == 0) {
      this.studentService.post(this.studentForm).subscribe({
        next: (data) => {
          this.students.unshift(data);
           this.addorupdatemodal.hide();
        },
        error: (error) => {
          console.log(error);
        },
      });
    }
  }
}

加入新增按鈕

<table class="table">
  <thead>
    <tr>
      <th scope="col">編號</th>
      <th scope="col">姓名</th>
      <th scope="col">年齡</th>
      <th scope="col">性別</th>
    </tr>
  </thead>
  <tbody>
    <tr *ngFor="let sd of students">
     <th scope="row">{{sd.id}}</th>
     <td>{{sd.name}}</td>
     <td>{{sd.age}}</td>
     <td>{{sd.gender}}</td>
    </tr>
  </tbody>
</table>

<!-- Modal -->
<div class="container">
  <div class="row">
    <div class="col col-md-4 offset-md-4">
      <button type="button" (click)='openAddOrUpdateModal(0)' class="btn btn-primary">新增</button>
    </div>
  </div>
</div>
<div class="modal fade" id="addorupdatemodal" tabindex="-1" aria-labelledby="exampleModalLabel" aria-hidden="true">
  <div class="modal-dialog">
    <div class="modal-content">
      <div class="modal-header">
        <h5 class="modal-title" id="exampleModalLabel">{{addorupdatemodalTitle}}</h5>
        <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
      </div>
      <div class="modal-body">
        <div class="container">
          <div class="row">
            <input type="hidden" id="studentId" [(ngModel)]="studentForm.id">
            <div class="mb-3">
              <label for="name" class="form-label">姓名</label>
              <input type="text" class="form-control" id="name" [(ngModel)]="studentForm.name" />
            </div>
            <div class="mb-3">
              <label for="age" class="form-label">年齡</label>
              <input type="number" class="form-control" id="age" [(ngModel)]="studentForm.age" />
            </div>
            <div class="mb-3">
              <label for="age" class="form-label">性別</label>
              <select class="form-select" [(ngModel)]="studentForm.gender" aria-label="Default select example">
                <option selected>Open this select menu</option>
                <option value="M">男</option>
                <option value="F">女</option>
              </select>
            </div>
          </div>
        </div>
      </div>
      <div class="modal-footer">
        <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
          關閉
        </button>
        <button type="button"  (click)="createorUpdateStudent()"  class="btn btn-primary">儲存</button>
      </div>
    </div>
  </div>
</div>

若無法使用ngModel問題,則要匯入FormsModule

import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { BrowserModule } from '@angular/platform-browser';

import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { StudentComponent } from './student/student.component';
import { HttpClientModule } from '@angular/common/http';
@NgModule({
  declarations: [
    AppComponent,
    StudentComponent
  ],
  imports: [
    BrowserModule,
    AppRoutingModule,
    HttpClientModule,
    FormsModule
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

Student.service.ts底下追加update

  update(payload: Student) {
    return this.httpClient.put<Student>(
        this.url + 'Put',
        payload
    );
  }

Student.component.ts

   openAddOrUpdateModal(studentId:number)
   {
        if(studentId ===0)
        {
            this.addorupdatemodalTitle = '新增';
            this.studentForm = {
              age: 0,
              gender: 'Male',
              id: 0,
              name: '',
        };
        this.addorupdatemodal.show();
        }
        else
        {
          
          this.addorupdatemodalTitle = '修改';
          this.studentForm = this.students.filter(s => s.id === studentId)[0];
          this.addorupdatemodal.show();

        }
   }
   createorUpdateStudent()
   {
    if (this.studentForm.id == 0) {
        this.studentService.post(this.studentForm).subscribe({
          next: (data) => {
            this.students.unshift(data);
             this.addorupdatemodal.hide();
          },
          error: (error) => {
            console.log(error);
          },
      });
    }else{
      this.studentService.update(this.studentForm).subscribe({
        next:(data) => {
            this.students = this.students.filter(_ => _.id !== data.id);
            this.students.unshift(data);
            this.addorupdatemodal.hide();
        }
      })
    }
  }
  <tbody>
    <tr *ngFor="let sd of students">
     <th scope="row">{{sd.id}}</th>
     <td>{{sd.name}}</td>
     <td>{{sd.age}}</td>
     <td>{{sd.gender}}</td>
     <td>
      <button class="btn btn-primary" (click)="openAddOrUpdateModal(sd.id)">編輯</button>

     </td>

    </tr>
  </tbody>

Student.service.ts底下追加Delete功能

  delete(studetId: number) {
    return this.httpClient.delete(
      this.url  + '/Delete?id=${studetId}'
    );
  }
import { Component ,OnInit} from '@angular/core';
import { Student} from './student';
import { StudentService } from './student.service';
declare var window: any;
@Component({
  selector: 'app-student',
  templateUrl: './student.component.html',
  styleUrls: ['./student.component.css']
})
export class StudentComponent implements OnInit  {

  students:Student[] = [];
  addorupdatemodal: any;
  studentForm: Student = {
    age: 0,
    gender: 'Male',
    id: 0,
    name: '',
  };
  addorupdatemodalTitle: string = '';

  deleteModal: any;
  studentIdToDelete: number = 0;

  constructor(private studentService:StudentService) { }
   ngOnInit(): void {
       this.get();
        this.addorupdatemodal = new window.bootstrap.Modal(
        document.getElementById('addorupdatemodal')
       );
       this.deleteModal = new window.bootstrap.Modal(
        document.getElementById('deleteModal')
      );
   }
   get()
   {
    this.studentService.get()
    .subscribe({
      next:(data) => {
        this.students = data;
      },
      error:(err) => {
        console.log(err);
      }
    })
   }

   openAddOrUpdateModal(studentId:number)
   {
        if(studentId ===0)
        {
            this.addorupdatemodalTitle = '新增';
            this.studentForm = {
              age: 0,
              gender: 'Male',
              id: 0,
              name: '',
        };
        this.addorupdatemodal.show();
        }
         else
        {

          this.addorupdatemodalTitle = '修改';
          this.studentForm = this.students.filter(s => s.id === studentId)[0];
          this.addorupdatemodal.show();

        }

   }
   createorUpdateStudent() {
    if (this.studentForm.id == 0) {
        this.studentService.post(this.studentForm).subscribe({
          next: (data) => {
            this.students.unshift(data);
             this.addorupdatemodal.hide();
          },
          error: (error) => {
            console.log(error);
          },
      });
    }else{
      this.studentService.update(this.studentForm).subscribe({
        next:(data) => {
            this.students = this.students.filter(_ => _.id !== data.id);
            this.students.unshift(data);
            this.addorupdatemodal.hide();
        }
      })
    }
  }

  openDeleteModal(studentId: number) {
    this.studentIdToDelete = studentId;
    this.deleteModal.show();
  }
  confirmDelete(){
    this.studentService.delete(this.studentIdToDelete)
    .subscribe({
      next:(data) => {
        this.students = this.students.filter(_ => _.id !== this.studentIdToDelete);
        this.deleteModal.hide();
      },
      error:(error) => {
        console.log(error);
      }
    })
  }


}
<table class="table">
  <thead>
    <tr>
      <th scope="col">編號</th>
      <th scope="col">姓名</th>
      <th scope="col">年齡</th>
      <th scope="col">性別</th>
      <th scope="col">操作</th>
    </tr>
  </thead>
  <tbody>
    <tr *ngFor="let sd of students">
     <th scope="row">{{sd.id}}</th>
     <td>{{sd.name}}</td>
     <td>{{sd.age}}</td>
     <td>{{sd.gender}}</td>
     <td>
      <button class="btn btn-primary" (click)="openAddOrUpdateModal(sd.id)">編輯</button>
      <button class="btn btn-primary" (click)="openDeleteModal(sd.id)">刪除</button>
     </td>

    </tr>
  </tbody>
</table>

<!-- Modal -->
<div class="container">
  <div class="row">
    <div class="col col-md-4 offset-md-4">
      <button type="button" (click)='openAddOrUpdateModal(0)' class="btn btn-primary">新增</button>
    </div>
  </div>
</div>
<div class="modal fade" id="addorupdatemodal" tabindex="-1" aria-labelledby="exampleModalLabel" aria-hidden="true">
  <div class="modal-dialog">
    <div class="modal-content">
      <div class="modal-header">
        <h5 class="modal-title" id="exampleModalLabel">{{addorupdatemodalTitle}}</h5>
        <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
      </div>
      <div class="modal-body">
        <div class="container">
          <div class="row">
            <input type="hidden" id="studentId" [(ngModel)]="studentForm.id">
            <div class="mb-3">
              <label for="name" class="form-label">姓名</label>
              <input type="text" class="form-control" id="name" [(ngModel)]="studentForm.name" />
            </div>
            <div class="mb-3">
              <label for="age" class="form-label">年齡</label>
              <input type="number" class="form-control" id="age" [(ngModel)]="studentForm.age" />
            </div>
            <div class="mb-3">
              <label for="age" class="form-label">性別</label>
              <select class="form-select" [(ngModel)]="studentForm.gender" aria-label="Default select example">
                <option selected>Open this select menu</option>
                <option value="M">男</option>
                <option value="F">女</option>
              </select>
            </div>
          </div>
        </div>
      </div>
      <div class="modal-footer">
        <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
          關閉
        </button>
        <button type="button"  (click)="createorUpdateStudent()"  class="btn btn-primary">儲存</button>
      </div>
    </div>
  </div>
</div>
<div class="modal fade" id="deleteModal" tabindex="-1" aria-labelledby="exampleModalLabel" aria-hidden="true">
  <div class="modal-dialog">
    <div class="modal-content">
      <div class="modal-header">
        <h5 class="modal-title" id="exampleModalLabel">刪除</h5>
        <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="關閉"></button>
      </div>
      <div class="modal-body">
        <h1>是否確認刪除 </h1>
      </div>
      <div class="modal-footer">
        <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">關閉</button>
        <button type="button" (click)="confirmDelete()" class="btn btn-primary">刪除</button>
      </div>
    </div>
  </div>
</div>

完成如下

 

 

 

元哥的筆記