從頭編寫 asp.net core 2.0 web api 基礎框架 (5) EF CRUD
Github原始碼地址:https://github.com/solenovex/Building-asp.net-core-2-web-api-starter-template-from-scratch
這是第一大部分的最後一小部分。要完成CRUD的操作。
Repository Pattern
我們可以直接在Controller訪問DbContext,但是可能會有一些問題:
1.相關的一些程式碼到處重複,有可能在程式中很多地方我都會更新Product,那樣的話我可能就會在多個Action裡面寫同樣的程式碼,而比較好的做法是隻在一個地方寫更新Product的程式碼。
2.到處寫重複程式碼還會導致另外一個問題,那就是容易出錯。
3.還有就是難以測試,如果想對Controller的Action進行單元測試,但是這些Action還包含著持久化相關的邏輯,這就很難的精確的找出到底是邏輯出錯還是持久化部分出錯了。
所以如果能有一種方法可以mock持久化相關的程式碼,然後再測試,就會知道錯誤不是發生在持久化部分了,這就可以用Repository Pattern了。
Repository Pattern是一種抽象,它減少了複雜性,目標是使程式碼對repository的實現更安全,並且與持久化要無關。
其中持久化無關這點我要明確一下,有時候是指可以隨意切換持久化的技術,但這實際上並不是repository pattern的目的,其真正的目的是可以為repository挑選一個最好的持久化技術。例如:建立一個Product最好的方式可能是使用entity framework,而查詢product最好的方式可能是使用dapper,也有可能會呼叫外部服務,而對呼叫repository的消費者來說,它不關心這些具體的實現細節。
首先再建立一個Material entity,然後和Product做成多對一的關係:
namespace CoreBackend.Api.Entities { public class Material { public int Id { get; set; } public int ProductId { get; set; } public string Name { get; set; } public Product Product { get; set; } } public class MaterialConfiguration : IEntityTypeConfiguration<Material> { public void Configure(EntityTypeBuilder<Material> builder) { builder.HasKey(x => x.Id); builder.Property(x => x.Name).IsRequired().HasMaxLength(50); builder.HasOne(x => x.Product).WithMany(x => x.Materials).HasForeignKey(x => x.ProductId) .OnDelete(DeleteBehavior.Cascade); } } }
修改Product.cs:
namespace CoreBackend.Api.Entities
{
public class Product
{
public int Id { get; set; }
public string Name { get; set; }
public float Price { get; set; }
public string Description { get; set; }
public ICollection<Material> Materials { get; set; }
}
public class ProductConfiguration : IEntityTypeConfiguration<Product>
{
public void Configure(EntityTypeBuilder<Product> builder)
{
builder.HasKey(x => x.Id);
builder.Property(x => x.Name).IsRequired().HasMaxLength(50);
builder.Property(x => x.Price).HasColumnType("decimal(8,2)");
builder.Property(x => x.Description).HasMaxLength(200);
}
}
}
然後別忘了在Context裡面註冊Material的Configuration並新增DbSet屬性:
namespace CoreBackend.Api.Entities
{
public class MyContext : DbContext
{
public MyContext(DbContextOptions<MyContext> options)
: base(options)
{
Database.Migrate();
}
public DbSet<Product> Products { get; set; }
public DbSet<Material> Materials { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.ApplyConfiguration(new ProductConfiguration());
modelBuilder.ApplyConfiguration(new MaterialConfiguration());
}
}
}
然後新增一個遷移 Add-Migration AddMaterial:
然後資料庫直接進行遷移操作了,無需再做update-database。
建立一個Repositories資料夾,新增一個IProductRepository:
namespace CoreBackend.Api.Repositories
{
public interface IProductRepository
{
IEnumerable<Product> GetProducts();
Product GetProduct(int productId, bool includeMaterials);
IEnumerable<Material> GetMaterialsForProduct(int productId);
Material GetMaterialForProduct(int productId, int materialId);
}
}
這個是ProductRepository將要實現的介面,裡面定義了一些必要的方法:查詢Products,查詢單個Product,查詢Product的Materials和查詢Product下的一個Material。
其中類似GetProducts()這樣的方法返回型別還是有爭議的,IQueryable<T>還是IEnumerable<T>。
如果返回的是IQueryable,那麼呼叫repository的地方還可以繼續構建IQueryable,例如在真正的查詢執行之前附加一個OrderBy或者Where方法。但是這樣做的話,也意味著你把持久化相關的程式碼給洩露出去了,這看起來是違反了repository pattern的目的。
如果是IEnumerable,為了返回各種各樣情況的查詢結果,需要編寫幾十個上百個查詢方法,那也是相當繁瑣的,幾乎是不可能的。
目前看來,兩種返回方式都有人在用,所以根據情況定吧。我們的程式需求比較簡單,所以使用IEnumerable。
然後建立具體的實現類 ProductRepository:
namespace CoreBackend.Api.Repositories
{
public class ProductRepository : IProductRepository
{
private readonly MyContext _myContext;
public ProductRepository(MyContext myContext)
{
_myContext = myContext;
}
public IEnumerable<Product> GetProducts()
{
return _myContext.Products.OrderBy(x => x.Name).ToList();
}
public Product GetProduct(int productId, bool includeMaterials)
{
if (includeMaterials)
{
return _myContext.Products
.Include(x => x.Materials).FirstOrDefault(x => x.Id == productId);
}
return _myContext.Products.Find(productId);
}
public IEnumerable<Material> GetMaterialsForProduct(int productId)
{
return _myContext.Materials.Where(x => x.ProductId == productId).ToList();
}
public Material GetMaterialForProduct(int productId, int materialId)
{
return _myContext.Materials.FirstOrDefault(x => x.ProductId == productId && x.Id == materialId);
}
}
}
這裡面要包含吃就會的邏輯,所以我們需要MyContext(也有可能需要其他的Service)那就在Constructor裡面注入一個。重要的是呼叫的程式不關心這些細節。
這裡也是編寫額外的持久化邏輯的地方,比如說查詢之後做個排序之類的。
(具體的Entity Framework Core的方法請查閱EF Core官方文件:https://docs.microsoft.com/en-us/ef/core/)
GetProducts,查詢所有的產品並按照名稱排序並返回查詢結果。這裡注意一定要加上ToList(),它保證了對資料庫的查詢就在此時此刻發生。
GetProduct,查詢單個產品,判斷一下是否需要把產品下面的原料都一起查詢出來,如果需要的話就使用Include這個extension method。查詢條件可以放在FirstOrDefault()方法裡面。
GetMaterialsForProduct,查詢某個產品下所有的原料。
GetMaterialForProduct,查詢某個產品下的某種原料。
建立好Repository之後,需要在Startup裡面進行註冊:
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc();
#if DEBUG
services.AddTransient<IMailService, LocalMailService>();
#else
services.AddTransient<IMailService, CloudMailService>();
#endif
var connectionString = Configuration["connectionStrings:productionInfoDbConnectionString"];
services.AddDbContext<MyContext>(o => o.UseSqlServer(connectionString));
services.AddScoped<IProductRepository, ProductRepository>();
}
針對Repository,最好的生命週期是Scoped(每個請求生成一個例項)。<>裡面前邊是它的合約介面,後邊是具體實現。
使用Repository
先為ProductDto新增一個屬性:
namespace CoreBackend.Api.Dtos
{
public class ProductDto
{
public ProductDto()
{
Materials = new List<MaterialDto>();
}
public int Id { get; set; }
public string Name { get; set; }
public float Price { get; set; }
public string Description { get; set; }
public ICollection<MaterialDto> Materials { get; set; }
public int MaterialCount => Materials.Count;
}
}
就是返回該產品所用的原料個數。
再建立一個ProductWithoutMaterialDto:
namespace CoreBackend.Api.Dtos
{
public class ProductWithoutMaterialDto
{
public int Id { get; set; }
public string Name { get; set; }
public float Price { get; set; }
public string Description { get; set; }
}
}
這個Dto不帶原料相關的導航屬性。
然後修改controller。
現在我們可以使用ProductRepository替代原來的記憶體資料了,首先在ProductController裡面注入ProductRepository:
public class ProductController : Controller
{
private readonly ILogger<ProductController> _logger;
private readonly IMailService _mailService;
private readonly IProductRepository _productRepository;
public ProductController(
ILogger<ProductController> logger,
IMailService mailService,
IProductRepository productRepository)
{
_logger = logger;
_mailService = mailService;
_productRepository = productRepository;
}
1.修改GetProducts這個Action:
[HttpGet]
public IActionResult GetProducts()
{
var products = _productRepository.GetProducts();
var results = new List<ProductWithoutMaterialDto>();
foreach (var product in products)
{
results.Add(new ProductWithoutMaterialDto
{
Id = product.Id,
Name = product.Name,
Price = product.Price,
Description = product.Description
});
}
return Ok(results);
}
注意,其中的Product型別是DbContext和repository操作的型別,而不是Action應該返回的型別,而且我們的查詢結果是不帶Material的,所以需要把Product的list對映成ProductWithoutMaterialDto的list。
然後試試:
查詢的時候報錯,是因為Product的屬性Price,在fluentapi裡面設定的型別是decimal(8, 2),而Price的型別是float,那麼我們把所有的Price的型別都改成decimal:
public class Product
{
public int Id { get; set; }
public string Name { get; set; }
public decimal Price { get; set; }
public string Description { get; set; }
public ICollection<Material> Materials { get; set; }
}
public class ProductCreation
{
[Display(Name = "產品名稱")]
[Required(ErrorMessage = "{0}是必填項")]
[StringLength(10, MinimumLength = 2, ErrorMessage = "{0}的長度應該不小於{2}, 不大於{1}")]
public string Name { get; set; }
[Display(Name = "價格")]
[Range(0, Double.MaxValue, ErrorMessage = "{0}的值必須大於{1}")]
public decimal Price { get; set; }
[Display(Name = "描述")]
[MaxLength(100, ErrorMessage = "{0}的長度不可以超過{1}")]
public string Description { get; set; }
}
public class ProductDto
{
public ProductDto()
{
Materials = new List<MaterialDto>();
}
public int Id { get; set; }
public string Name { get; set; }
public decimal Price { get; set; }
public string Description { get; set; }
public ICollection<MaterialDto> Materials { get; set; }
public int MaterialCount => Materials.Count;
}
public class ProductModification
{
[Display(Name = "產品名稱")]
[Required(ErrorMessage = "{0}是必填項")]
[StringLength(10, MinimumLength = 2, ErrorMessage = "{0}的長度應該不小於{2}, 不大於{1}")]
public string Name { get; set; }
[Display(Name = "價格")]
[Range(0, Double.MaxValue, ErrorMessage = "{0}的值必須大於{1}")]
public decimal Price { get; set; }
[Display(Name = "描述")]
[MaxLength(100, ErrorMessage = "{0}的長度不可以超過{1}")]
public string Description { get; set; }
}
public class ProductWithoutMaterialDto
{
public int Id { get; set; }
public string Name { get; set; }
public decimal Price { get; set; }
public string Description { get; set; }
}
還有SeedData裡面和即將廢棄的ProductService:
namespace CoreBackend.Api.Entities
{
public static class MyContextExtensions
{
public static void EnsureSeedDataForContext(this MyContext context)
{
if (context.Products.Any())
{
return;
}
var products = new List<Product>
{
new Product
{
Name = "牛奶",
Price = new decimal(2.5),
Description = "這是牛奶啊"
},
new Product
{
Name = "麵包",
Price = new decimal(4.5),
Description = "這是麵包啊"
},
new Product
{
Name = "啤酒",
Price = new decimal(7.5),
Description = "這是啤酒啊"
}
};
context.Products.AddRange(products);
context.SaveChanges();
}
}
}
namespace CoreBackend.Api.Services
{
public class ProductService
{
public static ProductService Current { get; } = new ProductService();
public List<ProductDto> Products { get; }
private ProductService()
{
Products = new List<ProductDto>
{
new ProductDto
{
Id = 1,
Name = "牛奶",
Price = new decimal(2.5),
Materials = new List<MaterialDto>
{
new MaterialDto
{
Id = 1,
Name = "水"
},
new MaterialDto
{
Id = 2,
Name = "奶粉"
}
},
Description = "這是牛奶啊"
},
new ProductDto
{
Id = 2,
Name = "麵包",
Price = new decimal(4.5),
Materials = new List<MaterialDto>
{
new MaterialDto
{
Id = 3,
Name = "麵粉"
},
new MaterialDto
{
Id = 4,
Name = "糖"
}
},
Description = "這是麵包啊"
},
new ProductDto
{
Id = 3,
Name = "啤酒",
Price = new decimal(7.5),
Materials = new List<MaterialDto>
{
new MaterialDto
{
Id = 5,
Name = "麥芽"
},
new MaterialDto
{
Id = 6,
Name = "地下水"
}
},
Description = "這是啤酒啊"
}
};
}
}
}
然後在執行試試:
結果正確。
然後修改GetProduct:
[Route("{id}", Name = "GetProduct")]
public IActionResult GetProduct(int id, bool includeMaterial = false)
{
var product = _productRepository.GetProduct(id, includeMaterial);
if (product == null)
{
return NotFound();
}
if (includeMaterial)
{
var productWithMaterialResult = new ProductDto
{
Id = product.Id,
Name = product.Name,
Price = product.Price,
Description = product.Description
};
foreach (var material in product.Materials)
{
productWithMaterialResult.Materials.Add(new MaterialDto
{
Id = material.Id,
Name = material.Name
});
}
return Ok(productWithMaterialResult);
}
var onlyProductResult = new ProductDto
{
Id = product.Id,
Name = product.Name,
Price = product.Price,
Description = product.Description
};
return Ok(onlyProductResult);
}
首先再新增一個引數includeMaterial表示是否帶著Material表的資料一起查詢出來,該引數有一個預設值是false,就是請求的時候如果不帶這個引數,那麼這個引數的值就是false。
通過repository查詢之後把Product和Material分別對映成ProductDto和MaterialDot。
試試,首先不包含Material:
目前資料庫的Material表沒有資料,可以手動新增幾個,也可以把資料庫的Product資料刪了,改一下種子資料那部分程式碼:
namespace CoreBackend.Api.Entities
{
public static class MyContextExtensions
{
public static void EnsureSeedDataForContext(this MyContext context)
{
if (context.Products.Any())
{
return;
}
var products = new List<Product>
{
new Product
{
Name = "牛奶",
Price = new decimal(2.5),
Description = "這是牛奶啊",
Materials = new List<Material>
{
new Material
{
Name = "水"
},
new Material
{
Name = "奶粉"
}
}
},
new Product
{
Name = "麵包",
Price = new decimal(4.5),
Description = "這是麵包啊",
Materials = new List<Material>
{
new Material
{
Name = "麵粉"
},
new Material
{
Name = "糖"
}
}
},
new Product
{
Name = "啤酒",
Price = new decimal(7.5),
Description = "這是啤酒啊",
Materials = new List<Material>
{
new Material
{
Name = "麥芽"
},
new Material
{
Name = "地下水"
}
}
}
};
context.Products.AddRange(products);
context.SaveChanges();
}
}
}
然後再試試GetProduct帶有material的查詢:
其中inludeMaterail這個引數需要使用query string的方式,也就是在uri後邊加一個問號,問號後邊跟著引數名,然後是等號,然後是它的值。如果有多個query string的引數,那麼每組引數之間用&分開。
然後再修改一下MaterialController:
namespace CoreBackend.Api.Controllers
{
[Route("api/product")] // 和主Model的Controller字首一樣
public class MaterialController : Controller
{
private readonly IProductRepository _productRepository;
public MaterialController(IProductRepository productRepository)
{
_productRepository = productRepository;
}
[HttpGet("{productId}/materials")]
public IActionResult GetMaterials(int productId)
{
var materials = _productRepository.GetMaterialsForProduct(productId);
var results = materials.Select(material => new MaterialDto
{
Id = material.Id,
Name = material.Name
})
.ToList();
return Ok(results);
}
[HttpGet("{productId}/materials/{id}")]
public IActionResult GetMaterial(int productId, int id)
{
var material = _productRepository.GetMaterialForProduct(productId, id);
if (material == null)
{
return NotFound();
}
var result = new MaterialDto
{
Id = material.Id,
Name = material.Name
};
return Ok(result);
}
}
}
注意GetMaterials方法內,我們往productRepository的GetMaterialsForProduct傳進去一個productId,如果repository返回的是空list可能會有兩種情況:1 product不存在,2 product存在,而它沒有下屬的material。如果是第一種情況,那麼應該返回的是404 NotFound,而第二種action應該返回一個空list。所以我們需要一個方法判斷product是否存在,所以開啟ProductRepository,新增方法:
public bool ProductExist(int productId)
{
return _myContext.Products.Any(x => x.Id == productId);
}
並在pull up member(右鍵點選方法程式碼--重構裡面有)到接口裡面:
namespace CoreBackend.Api.Repositories
{
public interface IProductRepository
{
IEnumerable<Product> GetProducts();
Product GetProduct(int productId, bool includeMaterials);
IEnumerable<Material> GetMaterialsForProduct(int productId);
Material GetMaterialForProduct(int productId, int materialId);
bool ProductExist(int productId);
}
}
然後再改一下Controller:
namespace CoreBackend.Api.Controllers
{
[Route("api/product")] // 和主Model的Controller字首一樣
public class MaterialController : Controller
{
private readonly IProductRepository _productRepository;
public MaterialController(IProductRepository productRepository)
{
_productRepository = productRepository;
}
[HttpGet("{productId}/materials")]
public IActionResult GetMaterials(int productId)
{
var product = _productRepository.ProductExist(productId);
if (!product)
{
return NotFound();
}
var materials = _productRepository.GetMaterialsForProduct(productId);
var results = materials.Select(material => new MaterialDto
{
Id = material.Id,
Name = material.Name
})
.ToList();
return Ok(results);
}
[HttpGet("{productId}/materials/{id}")]
public IActionResult GetMaterial(int productId, int id)
{
var product = _productRepository.ProductExist(productId);
if (!product)
{
return NotFound();
}
var material = _productRepository.GetMaterialForProduct(productId, id);
if (material == null)
{
return NotFound();
}
var result = new MaterialDto
{
Id = material.Id,
Name = material.Name
};
return Ok(result);
}
}
}
試試:
結果都沒有問題!!!
但是看看上面controller裡面的程式碼,到處都是對映,這種手寫的對映很容易出錯,如果entity有幾十個屬性,然後在多個地方需要進行對映,那麼這麼寫實在太糟糕了。
所以需要使用一個對映的庫:
AutoMapper
autoMapper是最主流的.net對映庫,所以我們用它。
通過nuget安裝automapper:
安裝完之後,首先要配置automapper。我們要告訴automapper哪些entity和dto之間有對映關係。這個配置應該只建立一次,並且在startup的時候進行初始化。
在Startup的Configure方法新增:
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory,
MyContext myContext)
{
loggerFactory.AddNLog();
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler();
}
myContext.EnsureSeedDataForContext();
app.UseStatusCodePages();
AutoMapper.Mapper.Initialize(cfg =>
{
cfg.CreateMap<Product, ProductWithoutMaterialDto>();
});
app.UseMvc();
}
建立對映關係,我們需要使用AutoMapper.Mapper.Initialize方法,其引數是一個Action,這個Action的引數是一個Mapping Configuration。
cfg.CreateMap<Product, ProductWithoutMaterialDto>(),意思就是建立一個從Product到ProductWIthoutMaterialDto的對映關係。
AutoMapper是基於約定的,原物件的屬性值會被對映到目標物件相同屬性名的屬性上。如果屬性不存在,那麼就忽略它。
偶爾我們可能需要對AutoMapper的對映進行一些微調,但是對於大多數情況來說,上面這一句話就夠用了。
現在可以在controller裡面使用這個映射了。
開啟controller首先改一下GetProducts:
[HttpGet]
public IActionResult GetProducts()
{
var products = _productRepository.GetProducts();
var results = Mapper.Map<IEnumerable<ProductWithoutMaterialDto>>(products);
return Ok(results);
}
使用Mapper.Map進行對映,<T>其中T是目標型別,可以是一個model也可以是一個集合,括號裡面的引數是原物件們。
執行試試:
沒問題,結果和之前是一樣的。
然後針對GetProduct,首先再建立一對對映:
AutoMapper.Mapper.Initialize(cfg =>
{
cfg.CreateMap<Product, ProductWithoutMaterialDto>();
cfg.CreateMap<Product, ProductDto>();
});
然後GetProduct:
[Route("{id}", Name = "GetProduct")]
public IActionResult GetProduct(int id, bool includeMaterial = false)
{
var product = _productRepository.GetProduct(id, includeMaterial);
if (product == null)
{
return NotFound();
}
if (includeMaterial)
{
var productWithMaterialResult = Mapper.Map<ProductDto>(product);
return Ok(productWithMaterialResult);
}
var onlyProductResult = Mapper.Map<ProductWithoutMaterialDto>(product);
return Ok(onlyProductResult);
}
執行,查詢包含Material,報錯:
這是因為ProductDto裡面有一個屬性 ICollection<Material> Materials,automapper不知道應該怎麼去對映它,所以我們需要再新增一對Material到MaterialDto的對映關係。
AutoMapper.Mapper.Initialize(cfg =>
{
cfg.CreateMap<Product, ProductWithoutMaterialDto>();
cfg.CreateMap<Product, ProductDto>();
cfg.CreateMap<Material, MaterialDto>();
});
執行:
沒問題。
然後把MaterailController裡面也改一下:
namespace CoreBackend.Api.Controllers
{
[Route("api/product")] // 和主Model的Controller字首一樣
public class MaterialController : Controller
{
private readonly IProductRepository _productRepository;
public MaterialController(IProductRepository productRepository)
{
_productRepository = productRepository;
}
[HttpGet("{productId}/materials")]
public IActionResult GetMaterials(int productId)
{
var product = _productRepository.ProductExist(productId);
if (!product)
{
return NotFound();
}
var materials = _productRepository.GetMaterialsForProduct(productId);
var results = Mapper.Map<IEnumerable<MaterialDto>>(materials);
return Ok(results);
}
[HttpGet("{productId}/materials/{id}")]
public IActionResult GetMaterial(int productId, int id)
{
var product = _productRepository.ProductExist(productId);
if (!product)
{
return NotFound();
}
var material = _productRepository.GetMaterialForProduct(productId, id);
if (material == null)
{
return NotFound();
}
var result = Mapper.Map<MaterialDto>(material);
return Ok(result);
}
}
}
執行一下都應該沒有什麼問題。
上面都是查詢的Actions。
下面開始做CUD的對映更改。
新增:
修改ProductRepository,新增以下方法:
public void AddProduct(Product product)
{
_myContext.Products.Add(product);
}
public bool Save()
{
return _myContext.SaveChanges() >= 0;
}
AddProduct會把傳進來的product新增到context的記憶體中(姑且這麼說),但是還沒有更新到資料庫。
Save方法裡面是把context所追蹤的實體變化(CUD)更新到資料庫。
然後把這兩個方法提取到IProductRepository接口裡:
public interface IProductRepository
{
IEnumerable<Product> GetProducts();
Product GetProduct(int productId, bool includeMaterials);
IEnumerable<Material> GetMaterialsForProduct(int productId);
Material GetMaterialForProduct(int productId, int materialId);
bool ProductExist(int productId);
void AddProduct(Product product);
bool Save();
}
修改Controller的Post:
[HttpPost]
public IActionResult Post([FromBody] ProductCreation product)
{
if (product == null)
{
return BadRequest();
}
if (product.Name == "產品")
{
ModelState.AddModelError("Name", "產品的名稱不可以是'產品'二字");
}
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
var newProduct = Mapper.Map<Product>(product);
_productRepository.AddProduct(newProduct);
if (!_productRepository.Save())
{
return StatusCode(500, "儲存產品的時候出錯");
}
var dto = Mapper.Map<ProductWithoutMaterialDto>(newProduct);
return CreatedAtRoute("GetProduct", new { id = dto.Id }, dto);
}
注意別忘了要返回的是Dto。
執行:
沒問題。
Put
cfg.CreateMap<ProductModification, Product>();
[HttpPut("{id}")]
public IActionResult Put(int id, [FromBody] ProductModification productModificationDto)
{
if (productModificationDto == null)
{
return BadRequest();
}
if (productModificationDto.Name == "產品")
{
ModelState.AddModelError("Name", "產品的名稱不可以是'產品'二字");
}
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
var product = _productRepository.GetProduct(id);
if (product == null)
{
return NotFound();
}
Mapper.Map(productModificationDto, product);
if (!_productRepository.Save())
{
return StatusCode(500, "儲存產品的時候出錯");
}
return NoContent();
}
這裡我們使用了Mapper.Map的另一個overload的方法,它有兩個引數。這個方法會把第一個物件相應的值賦給第二個物件上。這時候product的state就變成了modified了。
然後儲存即可。
試試:
Partial Update
cfg.CreateMap<Product, ProductModification>();
[HttpPatch("{id}")]
public IActionResult Patch(int id, [FromBody] JsonPatchDocument<ProductModification> patchDoc)
{
if (patchDoc == null)
{
return BadRequest();
}
var productEntity = _productRepository.GetProduct(id);
if (productEntity == null)
{
return NotFound();
}
var toPatch = Mapper.Map<ProductModification>(productEntity);
patchDoc.ApplyTo(toPatch, ModelState);
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
if (toPatch.Name == "產品")
{
ModelState.AddModelError("Name", "產品的名稱不可以是'產品'二字");
}
TryValidateModel(toPatch);
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
Mapper.Map(toPatch, productEntity);
if (!_productRepository.Save())
{
return StatusCode(500, "更新的時候出錯");
}
return NoContent();
}
試試:
沒問題。
Delete
只是替換成repository,不涉及mapping。
在Repository新增一個Delete方法:
public void DeleteProduct(Product product)
{
_myContext.Products.Remove(product);
}
提取到IProductRepository:
void DeleteProduct(Product product);
然後Controller:
[HttpDelete("{id}")]
public IActionResult Delete(int id)
{
var model = _productRepository.GetProduct(id);
if (model == null)
{
return NotFound();
}
_productRepository.DeleteProduct(model);
if (!_productRepository.Save())
{
return StatusCode(500, "刪除的時候出錯");
}
_mailService.Send("Product Deleted",$"Id為{id}的產品被刪除了");
return NoContent();
}
執行:
Ok。
第一大部分先寫到這。。。。。。。。。。。。
接下來幾天比較忙,然後我再編寫第二大部分。我會直接弄一個已經重構好的模板,簡單講一下,然後重點是Identity Server 4.
到目前為止可以進行CRUD操作了,接下來需要把專案重構一下,然後再簡單用一下Identity Server4。