第1部分:https://www.yuque.com/net.core/framework/hlvsrm
第2部分:https://www.yuque.com/net.core/framework/hxwrrq
第3部分:https://www.yuque.com/net.core/framework/kitvxp
第4部分:http://www.cnblogs.com/cgzl/p/7661805.html

Github源码地址:
这是第一大部分的最后一小部分。要完成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做成多对一的关系:

  1. namespace CoreBackend.Api.Entities
  2. {
  3. public class Material
  4. {
  5. public int Id { get; set; }
  6. public int ProductId { get; set; }
  7. public string Name { get; set; }
  8. public Product Product { get; set; }
  9. }
  10. public class MaterialConfiguration : IEntityTypeConfiguration<Material> //configure entity material
  11. {
  12. public void Configure(EntityTypeBuilder<Material> builder)
  13. {
  14. builder.HasKey(x => x.Id); //set main key
  15. builder.Property(x => x.Name).IsRequired().HasMaxLength(50);
  16. builder.HasOne(x => x.Product).WithMany(x => x.Materials).HasForeignKey(x => x.ProductId)
  17. .OnDelete(DeleteBehavior.Cascade);
  18. }
  19. }
  20. }

修改Product.cs:

  1. namespace CoreBackend.Api.Entities
  2. {
  3. public class Product
  4. {
  5. public int Id { get; set; }
  6. public string Name { get; set; }
  7. public float Price { get; set; }
  8. public string Description { get; set; }
  9. public ICollection<Material> Materials { get; set; }
  10. }
  11. public class ProductConfiguration : IEntityTypeConfiguration<Product> //confiure entity product
  12. {
  13. public void Configure(EntityTypeBuilder<Product> builder)
  14. {
  15. builder.HasKey(x => x.Id); //set main key
  16. builder.Property(x => x.Name).IsRequired().HasMaxLength(50);
  17. builder.Property(x => x.Price).HasColumnType("decimal(8,2)");
  18. builder.Property(x => x.Description).HasMaxLength(200);
  19. }
  20. }
  21. }

然后别忘了在Context里面注册Material的Configuration并添加DbSet属性

  1. namespace CoreBackend.Api.Entities
  2. {
  3. public class MyContext : DbContext
  4. {
  5. public MyContext(DbContextOptions<MyContext> options)
  6. : base(options)
  7. {
  8. Database.Migrate();
  9. }
  10. public DbSet<Product> Products { get; set; }
  11. public DbSet<Material> Materials { get; set; }
  12. //即将在entities中的配置运行到context,并添加Dbset属性
  13. protected override void OnModelCreating(ModelBuilder modelBuilder)
  14. {
  15. modelBuilder.ApplyConfiguration(new ProductConfiguration());
  16. modelBuilder.ApplyConfiguration(new MaterialConfiguration());
  17. }
  18. }
  19. }


然后添加一个迁移 Add-Migration AddMaterial:
1-05.从头编写 asp.net core 2.0 web api 基础框架 (5) EF CRUD - 图1
然后数据库直接进行迁移操作了,无需再做update-database。
1-05.从头编写 asp.net core 2.0 web api 基础框架 (5) EF CRUD - 图2

建立一个Repositories文件夹,添加一个IProductRepository:

  1. namespace CoreBackend.Api.Repositories
  2. {
  3. public interface IProductRepository
  4. {
  5. IEnumerable<Product> GetProducts();
  6. Product GetProduct(int productId, bool includeMaterials);
  7. IEnumerable<Material> GetMaterialsForProduct(int productId);
  8. Material GetMaterialForProduct(int productId, int materialId);
  9. }
  10. }

这个是ProductRepository将要实现的接口,里面定义了一些必要的方法:查询Products,查询单个Product,查询Product的Materials和查询Product下的一个Material。
其中类似GetProducts()这样的方法返回类型还是有争议的,IQueryable还是IEnumerable。(IQueryable一般是动态数据查询,IEumerable静态的)
如果返回的是IQueryable,那么调用repository的地方还可以继续构建IQueryable,例如在真正的查询执行之前附加一个OrderBy或者Where方法。但是这样做的话,也意味着你把持久化相关的代码给泄露出去了,这看起来是违反了repository pattern的目的。
如果是IEnumerable,为了返回各种各样情况的查询结果,需要编写几十个上百个查询方法,那也是相当繁琐的,几乎是不可能的。
目前看来,两种返回方式都有人在用,所以根据情况定吧。我们的程序需求比较简单,所以使用IEnumerable。

然后建立具体的实现类 ProductRepository:

  1. //实现接口IRepository
  2. namespace CoreBackend.Api.Repositories
  3. {
  4. public class ProductRepository : IProductRepository
  5. {
  6. private readonly MyContext _myContext;
  7. public ProductRepository(MyContext myContext)
  8. {
  9. _myContext = myContext;
  10. }
  11. //获取所有的product
  12. public IEnumerable<Product> GetProducts()
  13. {
  14. return _myContext.Products.OrderBy(x => x.Name).ToList();
  15. }
  16. //获取单个product的所有信息,如果有material则返回,相反则不返回
  17. public Product GetProduct(int productId, bool includeMaterials)
  18. {
  19. if (includeMaterials)
  20. {
  21. return _myContext.Products
  22. .Include(x => x.Materials).FirstOrDefault(x => x.Id == productId);
  23. }
  24. return _myContext.Products.Find(productId);
  25. }
  26. //根据product查询所有的materials
  27. public IEnumerable<Material> GetMaterialsForProduct(int productId)
  28. {
  29. return _myContext.Materials.Where(x => x.ProductId == productId).ToList();
  30. }
  31. //根据productId,materialId精确查询
  32. public Material GetMaterialForProduct(int productId, int materialId)
  33. {
  34. return _myContext.Materials.FirstOrDefault(x => x.ProductId == productId && x.Id == materialId);
  35. }
  36. }
  37. }

这里面要包含吃就会的逻辑,所以我们需要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里面进行注册:

  1. //将Repository注册到configureServices
  2. public void ConfigureServices(IServiceCollection services)
  3. {
  4. services.AddMvc();
  5. #if DEBUG
  6. services.AddTransient<IMailService, LocalMailService>();
  7. #else
  8. services.AddTransient<IMailService, CloudMailService>();
  9. #endif
  10. var connectionString = Configuration["connectionStrings:productionInfoDbConnectionString"];
  11. services.AddDbContext<MyContext>(o => o.UseSqlServer(connectionString));
  12. services.AddScoped<IProductRepository, ProductRepository>(); //注册IProductRepository
  13. }

针对Repository,最好的生命周期是Scoped(每个请求生成一个实例)。<>里面前边是它的合约接口,后边是具体实现。

使用Repository

先为ProductDto添加一个属性(MaterialCount):

  1. namespace CoreBackend.Api.Dtos
  2. {
  3. public class ProductDto
  4. {
  5. public ProductDto()
  6. {
  7. Materials = new List<MaterialDto>();
  8. }
  9. public int Id { get; set; }
  10. public string Name { get; set; }
  11. public float Price { get; set; }
  12. public string Description { get; set; }
  13. public ICollection<MaterialDto> Materials { get; set; }
  14. public int MaterialCount => Materials.Count;
  15. }
  16. }

就是返回该产品所用的原料个数。
再建立一个ProductWithoutMaterialDto->**没有原料**:

  1. namespace CoreBackend.Api.Dtos
  2. {
  3. public class ProductWithoutMaterialDto
  4. {
  5. public int Id { get; set; }
  6. public string Name { get; set; }
  7. public float Price { get; set; }
  8. public string Description { get; set; }
  9. }
  10. }

这个Dto不带原料相关的导航属性。
然后修改controller。
现在我们可以使用ProductRepository替代原来的内存数据了,首先在ProductController里面注入ProductRepository:

  1. public class ProductController : Controller
  2. {
  3. private readonly ILogger<ProductController> _logger;
  4. private readonly IMailService _mailService;
  5. private readonly IProductRepository _productRepository; //将数据model注入到controller中
  6. public ProductController(
  7. ILogger<ProductController> logger,
  8. IMailService mailService,
  9. IProductRepository productRepository)
  10. {
  11. _logger = logger;
  12. _mailService = mailService;
  13. _productRepository = productRepository;
  14. }
  15. }

1.修改GetProducts这个Action:

  1. [HttpGet]
  2. public IActionResult GetProducts()
  3. {
  4. var products = _productRepository.GetProducts();
  5. //var product = ProductService.Current.Products.SingleOrDefault(x => x.Id == id); --old
  6. var results = new List<ProductWithoutMaterialDto>();
  7. foreach (var product in products)
  8. {
  9. results.Add(new ProductWithoutMaterialDto
  10. {
  11. Id = product.Id,
  12. Name = product.Name,
  13. Price = product.Price,
  14. Description = product.Description
  15. });
  16. }
  17. return Ok(results);
  18. }

注意,其中的Product类型是DbContext和repository操作的类型,而不是Action应该返回的类型,而且我们的查询结果是不带Material的,所以需要把Product的list映射成ProductWithoutMaterialDto的list。
然后试试:
查询的时候报错,是因为Product的属性Price,在fluentapi里面设置的类型是decimal(8, 2),而Price的类型是float,那么我们把所有的Price的类型都改成decimal:

  1. //原始数据模型
  2. public class Product
  3. {
  4. public int Id { get; set; }
  5. public string Name { get; set; }
  6. public decimal Price { get; set; } //float->decimal
  7. public string Description { get; set; }
  8. public ICollection<Material> Materials { get; set; }
  9. }
  10. //创建一笔新的数据
  11. public class ProductCreation
  12. {
  13. [Display(Name = "产品名称")]
  14. [Required(ErrorMessage = "{0}是必填项")]
  15. [StringLength(10, MinimumLength = 2, ErrorMessage = "{0}的长度应该不小于{2}, 不大于{1}")]
  16. public string Name { get; set; }
  17. [Display(Name = "价格")]
  18. [Range(0, Double.MaxValue, ErrorMessage = "{0}的值必须大于{1}")]
  19. public decimal Price { get; set; } //float->decimal
  20. [Display(Name = "描述")]
  21. [MaxLength(100, ErrorMessage = "{0}的长度不可以超过{1}")]
  22. public string Description { get; set; }
  23. }
  24. //在数据模型中新增一个属性MaterialCount
  25. public class ProductDto
  26. {
  27. public ProductDto()
  28. {
  29. Materials = new List<MaterialDto>();
  30. }
  31. public int Id { get; set; }
  32. public string Name { get; set; }
  33. public decimal Price { get; set; } //float->decimal
  34. public string Description { get; set; }
  35. public ICollection<MaterialDto> Materials { get; set; }
  36. public int MaterialCount => Materials.Count;
  37. }
  38. //完全更新
  39. public class ProductModification
  40. {
  41. [Display(Name = "产品名称")]
  42. [Required(ErrorMessage = "{0}是必填项")]
  43. [StringLength(10, MinimumLength = 2, ErrorMessage = "{0}的长度应该不小于{2}, 不大于{1}")]
  44. public string Name { get; set; }
  45. [Display(Name = "价格")]
  46. [Range(0, Double.MaxValue, ErrorMessage = "{0}的值必须大于{1}")]
  47. public decimal Price { get; set; } //float->decimal
  48. [Display(Name = "描述")]
  49. [MaxLength(100, ErrorMessage = "{0}的长度不可以超过{1}")]
  50. public string Description { get; set; }
  51. }
  52. //新数据模型与旧数据模型同步
  53. public class ProductWithoutMaterialDto
  54. {
  55. public int Id { get; set; }
  56. public string Name { get; set; }
  57. public decimal Price { get; set; } //float->decimal
  58. public string Description { get; set; }
  59. }

还有SeedData里面和即将废弃的ProductService:

  1. namespace CoreBackend.Api.Entities
  2. {
  3. public static class MyContextExtensions
  4. {
  5. public static void EnsureSeedDataForContext(this MyContext context)
  6. {
  7. if (context.Products.Any())
  8. {
  9. return;
  10. }
  11. var products = new List<Product>
  12. {
  13. new Product
  14. {
  15. Name = "牛奶",
  16. Price = new decimal(2.5),
  17. Description = "这是牛奶啊"
  18. },
  19. new Product
  20. {
  21. Name = "面包",
  22. Price = new decimal(4.5),
  23. Description = "这是面包啊"
  24. },
  25. new Product
  26. {
  27. Name = "啤酒",
  28. Price = new decimal(7.5),
  29. Description = "这是啤酒啊"
  30. }
  31. };
  32. context.Products.AddRange(products);
  33. context.SaveChanges();
  34. }
  35. }
  36. }
  37. namespace CoreBackend.Api.Services
  38. {
  39. public class ProductService
  40. {
  41. public static ProductService Current { get; } = new ProductService();
  42. public List<ProductDto> Products { get; }
  43. private ProductService()
  44. {
  45. Products = new List<ProductDto>
  46. {
  47. new ProductDto
  48. {
  49. Id = 1,
  50. Name = "牛奶",
  51. Price = new decimal(2.5),
  52. Materials = new List<MaterialDto>
  53. {
  54. new MaterialDto
  55. {
  56. Id = 1,
  57. Name = "水"
  58. },
  59. new MaterialDto
  60. {
  61. Id = 2,
  62. Name = "奶粉"
  63. }
  64. },
  65. Description = "这是牛奶啊"
  66. },
  67. new ProductDto
  68. {
  69. Id = 2,
  70. Name = "面包",
  71. Price = new decimal(4.5),
  72. Materials = new List<MaterialDto>
  73. {
  74. new MaterialDto
  75. {
  76. Id = 3,
  77. Name = "面粉"
  78. },
  79. new MaterialDto
  80. {
  81. Id = 4,
  82. Name = "糖"
  83. }
  84. },
  85. Description = "这是面包啊"
  86. },
  87. new ProductDto
  88. {
  89. Id = 3,
  90. Name = "啤酒",
  91. Price = new decimal(7.5),
  92. Materials = new List<MaterialDto>
  93. {
  94. new MaterialDto
  95. {
  96. Id = 5,
  97. Name = "麦芽"
  98. },
  99. new MaterialDto
  100. {
  101. Id = 6,
  102. Name = "地下水"
  103. }
  104. },
  105. Description = "这是啤酒啊"
  106. }
  107. };
  108. }
  109. }
  110. }

然后在运行试试:
1-05.从头编写 asp.net core 2.0 web api 基础框架 (5) EF CRUD - 图3
结果正确。
然后修改GetProduct:

  1. [Route("{id}", Name = "GetProduct")]
  2. public IActionResult GetProduct(int id, bool includeMaterial = false) //默认没有对应的材料
  3. {
  4. var product = _productRepository.GetProduct(id, includeMaterial); //替换之前的ProductService
  5. if (product == null)
  6. {
  7. return NotFound();
  8. }
  9. if (includeMaterial)
  10. {
  11. //赋值到对应的productDto
  12. var productWithMaterialResult = new ProductDto
  13. {
  14. Id = product.Id,
  15. Name = product.Name,
  16. Price = product.Price,
  17. Description = product.Description
  18. };
  19. foreach (var material in product.Materials)
  20. {
  21. productWithMaterialResult.Materials.Add(new MaterialDto
  22. {
  23. Id = material.Id,
  24. Name = material.Name
  25. });
  26. }
  27. return Ok(productWithMaterialResult);
  28. }
  29. var onlyProductResult = new ProductDto
  30. {
  31. Id = product.Id,
  32. Name = product.Name,
  33. Price = product.Price,
  34. Description = product.Description
  35. };
  36. return Ok(onlyProductResult);
  37. }

首先再添加一个参数includeMaterial表示是否带着Material表的数据一起查询出来,该参数有一个默认值是false,就是请求的时候如果不带这个参数,那么这个参数的值就是false。
通过repository查询之后把Product和Material分别映射成ProductDto和MaterialDot。
试试,首先不包含Material:
1-05.从头编写 asp.net core 2.0 web api 基础框架 (5) EF CRUD - 图4
目前数据库的Material表没有数据,可以手动添加几个,也可以把数据库的Product数据删了,改一下种子数据那部分代码:

  1. namespace CoreBackend.Api.Entities
  2. {
  3. public static class MyContextExtensions
  4. {
  5. public static void EnsureSeedDataForContext(this MyContext context)
  6. {
  7. if (context.Products.Any())
  8. {
  9. return;
  10. }
  11. var products = new List<Product>
  12. {
  13. new Product
  14. {
  15. Name = "牛奶",
  16. Price = new decimal(2.5),
  17. Description = "这是牛奶啊",
  18. Materials = new List<Material> //add material
  19. {
  20. new Material
  21. {
  22. Name = "水"
  23. },
  24. new Material
  25. {
  26. Name = "奶粉"
  27. }
  28. }
  29. },
  30. new Product
  31. {
  32. Name = "面包",
  33. Price = new decimal(4.5),
  34. Description = "这是面包啊",
  35. Materials = new List<Material>
  36. {
  37. new Material
  38. {
  39. Name = "面粉"
  40. },
  41. new Material
  42. {
  43. Name = "糖"
  44. }
  45. }
  46. },
  47. new Product
  48. {
  49. Name = "啤酒",
  50. Price = new decimal(7.5),
  51. Description = "这是啤酒啊",
  52. Materials = new List<Material>
  53. {
  54. new Material
  55. {
  56. Name = "麦芽"
  57. },
  58. new Material
  59. {
  60. Name = "地下水"
  61. }
  62. }
  63. }
  64. };
  65. context.Products.AddRange(products);
  66. context.SaveChanges();
  67. }
  68. }
  69. }

然后再试试GetProduct带有material的查询:(?includeMaterial=true)
1-05.从头编写 asp.net core 2.0 web api 基础框架 (5) EF CRUD - 图5

其中**inludeMaterail这个参数需要使用query string**的方式,也就是在uri后边加一个问号,问号后边跟着参数名,然后是等号,然后是它的值。如果有多个query string的参数,那么每组参数之间用&分开。
然后再修改一下MaterialController:

  1. namespace CoreBackend.Api.Controllers
  2. {
  3. [Route("api/product")] // 和主Model的Controller前缀一样
  4. public class MaterialController : Controller
  5. {
  6. private readonly IProductRepository _productRepository;
  7. public MaterialController(IProductRepository productRepository)
  8. {
  9. _productRepository = productRepository;
  10. }
  11. [HttpGet("{productId}/materials")]
  12. public IActionResult GetMaterials(int productId)
  13. {
  14. var materials = _productRepository.GetMaterialsForProduct(productId);
  15. var results = materials.Select(material => new MaterialDto
  16. {
  17. Id = material.Id,
  18. Name = material.Name
  19. })
  20. .ToList();
  21. return Ok(results);
  22. }
  23. [HttpGet("{productId}/materials/{id}")]
  24. public IActionResult GetMaterial(int productId, int id)
  25. {
  26. var material = _productRepository.GetMaterialForProduct(productId, id);
  27. if (material == null)
  28. {
  29. return NotFound(); //2 product存在,而它没有下属的
  30. }
  31. var result = new MaterialDto
  32. {
  33. Id = material.Id,
  34. Name = material.Name
  35. };
  36. return Ok(result);
  37. }
  38. }
  39. }

注意GetMaterials方法内,我们往productRepository的GetMaterialsForProduct传进去一个productId,如果repository返回的是空list可能会有两种情况:1 product不存在,2 product存在,而它没有下属的material。如果是第一种情况,那么应该返回的是404 NotFound,而第二种action应该返回一个空list。所以我们需要一个方法判断product是否存在,所以打开ProductRepository,添加方法:

并在pull up member(右键点击方法代码—重构里面有)到接口里面:

  1. namespace CoreBackend.Api.Repositories
  2. {
  3. public interface IProductRepository
  4. {
  5. IEnumerable<Product> GetProducts();
  6. Product GetProduct(int productId, bool includeMaterials);
  7. IEnumerable<Material> GetMaterialsForProduct(int productId);
  8. Material GetMaterialForProduct(int productId, int materialId);
  9. bool ProductExist(int productId);
  10. }
  11. }

然后再改一下Controller:

  1. namespace CoreBackend.Api.Controllers
  2. {
  3. [Route("api/product")] //和主Model的Controller前缀一样
  4. public class MaterialController : Controller
  5. {
  6. private readonly IProductRepository _productRepository;
  7. public MaterialController(IProductRepository productRepository)
  8. {
  9. _productRepository = productRepository;
  10. }
  11. [HttpGet("{productId}/materials")]
  12. public IActionResult GetMaterials(int productId)
  13. {
  14. var product = _productRepository.ProductExist(productId);
  15. if (!product)
  16. {
  17. return NotFound();
  18. }
  19. var materials = _productRepository.GetMaterialsForProduct(productId);
  20. var results = materials.Select(material => new MaterialDto
  21. {
  22. Id = material.Id,
  23. Name = material.Name
  24. })
  25. .ToList();
  26. return Ok(results);
  27. }
  28. [HttpGet("{productId}/materials/{id}")]
  29. public IActionResult GetMaterial(int productId, int id)
  30. {
  31. var product = _productRepository.ProductExist(productId);
  32. if (!product)
  33. {
  34. return NotFound();
  35. }
  36. var material = _productRepository.GetMaterialForProduct(productId, id);
  37. if (material == null)
  38. {
  39. return NotFound();
  40. }
  41. var result = new MaterialDto
  42. {
  43. Id = material.Id,
  44. Name = material.Name
  45. };
  46. return Ok(result);
  47. }
  48. }
  49. }

试试:
1-05.从头编写 asp.net core 2.0 web api 基础框架 (5) EF CRUD - 图6
1-05.从头编写 asp.net core 2.0 web api 基础框架 (5) EF CRUD - 图7
1-05.从头编写 asp.net core 2.0 web api 基础框架 (5) EF CRUD - 图8
1-05.从头编写 asp.net core 2.0 web api 基础框架 (5) EF CRUD - 图9
1-05.从头编写 asp.net core 2.0 web api 基础框架 (5) EF CRUD - 图10
结果都没有问题!!!
但是看看上面controller里面的代码,到处都是映射,这种手写的映射很容易出错,如果entity有几十个属性,然后在多个地方需要进行映射,那么这么写实在太糟糕了。
所以需要使用一个映射的库:

AutoMapper

autoMapper是最主流的.net映射库,所以我们用它。
通过nuget安装automapper:
1-05.从头编写 asp.net core 2.0 web api 基础框架 (5) EF CRUD - 图11
安装完之后,首先要配置automapper。我们要告诉automapper哪些entity和dto之间有映射关系。这个配置应该只创建一次,并且在startup的时候进行初始化。
在Startup的Configure方法添加:

  1. public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory,
  2. MyContext myContext)
  3. {
  4. loggerFactory.AddNLog();
  5. if (env.IsDevelopment())
  6. {
  7. app.UseDeveloperExceptionPage();
  8. }
  9. else
  10. {
  11. app.UseExceptionHandler();
  12. }
  13. myContext.EnsureSeedDataForContext();
  14. app.UseStatusCodePages();
  15. //添加autoMap
  16. AutoMapper.Mapper.Initialize(cfg =>
  17. {
  18. cfg.CreateMap<Product, ProductWithoutMaterialDto>();
  19. });
  20. app.UseMvc();
  21. }

创建映射关系,我们需要使用AutoMapper.Mapper.Initialize方法,其参数是一个Action,这个Action的参数是一个Mapping Configuration。
cfg.CreateMap(),意思就是创建一个从Product到ProductWIthoutMaterialDto的映射关系。
AutoMapper是基于约定的,原对象的属性值会被映射到目标对象相同属性名的属性上。如果属性不存在,那么就忽略它。
偶尔我们可能需要对AutoMapper的映射进行一些微调,但是对于大多数情况来说,上面这一句话就够用了。
现在可以在controller里面使用这个映射了。
打开controller首先改一下GetProducts:

  1. [HttpGet]
  2. public IActionResult GetProducts()
  3. {
  4. var products = _productRepository.GetProducts();
  5. var results = Mapper.Map<IEnumerable<ProductWithoutMaterialDto>>(products);
  6. return Ok(results);
  7. }

使用Mapper.Map进行映射,其中T是目标类型,可以是一个model也可以是一个集合,括号里面的参数是原对象们。
运行试试:
1-05.从头编写 asp.net core 2.0 web api 基础框架 (5) EF CRUD - 图12
没问题,结果和之前是一样的。
然后针对GetProduct,首先再建立一对映射:

  1. AutoMapper.Mapper.Initialize(cfg =>
  2. {
  3. cfg.CreateMap<Product, ProductWithoutMaterialDto>();
  4. cfg.CreateMap<Product, ProductDto>();
  5. });

然后GetProduct:

  1. [Route("{id}", Name = "GetProduct")]
  2. public IActionResult GetProduct(int id, bool includeMaterial = false)
  3. {
  4. var product = _productRepository.GetProduct(id, includeMaterial);
  5. if (product == null)
  6. {
  7. return NotFound();
  8. }
  9. if (includeMaterial)
  10. {
  11. var productWithMaterialResult = Mapper.Map<ProductDto>(product);
  12. return Ok(productWithMaterialResult);
  13. }
  14. var onlyProductResult = Mapper.Map<ProductWithoutMaterialDto>(product);
  15. return Ok(onlyProductResult);
  16. }

运行,查询包含Material,报错:
1-05.从头编写 asp.net core 2.0 web api 基础框架 (5) EF CRUD - 图13
这是因为ProductDto里面有一个属性 ICollection Materials,automapper不知道应该怎么去映射它,所以我们需要再添加一对Material到MaterialDto的映射关系。

  1. AutoMapper.Mapper.Initialize(cfg =>
  2. {
  3. cfg.CreateMap<Product, ProductWithoutMaterialDto>();
  4. cfg.CreateMap<Product, ProductDto>();
  5. cfg.CreateMap<Material, MaterialDto>();
  6. });

运行:
1-05.从头编写 asp.net core 2.0 web api 基础框架 (5) EF CRUD - 图14
没问题。
然后把MaterailController里面也改一下:

  1. AutoMapper.Mapper.Initialize(cfg =>
  2. {
  3. cfg.CreateMap<Product, ProductWithoutMaterialDto>();
  4. cfg.CreateMap<Product, ProductDto>();
  5. });

运行一下都应该没有什么问题。
上面都是查询的Actions。
下面开始做CUD的映射更改。

添加:

修改ProductRepository,添加以下方法:

  1. public void AddProduct(Product product)
  2. {
  3. _myContext.Products.Add(product);
  4. }
  5. public bool Save()
  6. {
  7. return _myContext.SaveChanges() >= 0;
  8. }

AddProduct会把传进来的product添加到context的内存中(姑且这么说),但是还没有更新到数据库。
Save方法里面是把context所追踪的实体变化(CUD)更新到数据库。
然后把这两个方法提取到IProductRepository接口里:

  1. public interface IProductRepository
  2. {
  3. IEnumerable<Product> GetProducts();
  4. Product GetProduct(int productId, bool includeMaterials);
  5. IEnumerable<Material> GetMaterialsForProduct(int productId);
  6. Material GetMaterialForProduct(int productId, int materialId);
  7. bool ProductExist(int productId);
  8. void AddProduct(Product product);
  9. bool Save();
  10. }

修改Controller的Post:

  1. [HttpPost]
  2. public IActionResult Post([FromBody] ProductCreation product)
  3. {
  4. if (product == null)
  5. {
  6. return BadRequest();
  7. }
  8. if (product.Name == "产品")
  9. {
  10. ModelState.AddModelError("Name", "产品的名称不可以是'产品'二字");
  11. }
  12. if (!ModelState.IsValid)
  13. {
  14. return BadRequest(ModelState);
  15. }
  16. var newProduct = Mapper.Map<Product>(product);
  17. _productRepository.AddProduct(newProduct);
  18. if (!_productRepository.Save())
  19. {
  20. return StatusCode(500, "保存产品的时候出错");
  21. }
  22. var dto = Mapper.Map<ProductWithoutMaterialDto>(newProduct);
  23. return CreatedAtRoute("GetProduct", new { id = dto.Id }, dto);
  24. }

注意别忘了要返回的是Dto。
运行:
1-05.从头编写 asp.net core 2.0 web api 基础框架 (5) EF CRUD - 图15
没问题。

Put

  1. cfg.CreateMap<ProductModification, Product>();
  1. [HttpPut("{id}")]
  2. public IActionResult Put(int id, [FromBody] ProductModification productModificationDto)
  3. {
  4. if (productModificationDto == null)
  5. {
  6. return BadRequest();
  7. }
  8. if (productModificationDto.Name == "产品")
  9. {
  10. ModelState.AddModelError("Name", "产品的名称不可以是'产品'二字");
  11. }
  12. if (!ModelState.IsValid)
  13. {
  14. return BadRequest(ModelState);
  15. }
  16. var product = _productRepository.GetProduct(id);
  17. if (product == null)
  18. {
  19. return NotFound();
  20. }
  21. Mapper.Map(productModificationDto, product);
  22. if (!_productRepository.Save())
  23. {
  24. return StatusCode(500, "保存产品的时候出错");
  25. }
  26. return NoContent();
  27. }

这里我们使用了Mapper.Map的另一个overload的方法,它有两个参数。这个方法会把第一个对象相应的值赋给第二个对象上。这时候product的state就变成了modified了。
然后保存即可。
试试:
1-05.从头编写 asp.net core 2.0 web api 基础框架 (5) EF CRUD - 图16

Partial Update

cfg.CreateMap();

  1. [HttpPatch("{id}")]
  2. public IActionResult Patch(int id, [FromBody] JsonPatchDocument<ProductModification> patchDoc)
  3. {
  4. if (patchDoc == null)
  5. {
  6. return BadRequest();
  7. }
  8. var productEntity = _productRepository.GetProduct(id);
  9. if (productEntity == null)
  10. {
  11. return NotFound();
  12. }
  13. var toPatch = Mapper.Map<ProductModification>(productEntity);
  14. patchDoc.ApplyTo(toPatch, ModelState);
  15. if (!ModelState.IsValid)
  16. {
  17. return BadRequest(ModelState);
  18. }
  19. if (toPatch.Name == "产品")
  20. {
  21. ModelState.AddModelError("Name", "产品的名称不可以是'产品'二字");
  22. }
  23. TryValidateModel(toPatch);
  24. if (!ModelState.IsValid)
  25. {
  26. return BadRequest(ModelState);
  27. }
  28. Mapper.Map(toPatch, productEntity);
  29. if (!_productRepository.Save())
  30. {
  31. return StatusCode(500, "更新的时候出错");
  32. }
  33. return NoContent();
  34. }

试试:
1-05.从头编写 asp.net core 2.0 web api 基础框架 (5) EF CRUD - 图17
没问题。

Delete

只是替换成repository,不涉及mapping。
在Repository添加一个Delete方法:

  1. public void DeleteProduct(Product product)
  2. {
  3. _myContext.Products.Remove(product);
  4. }
  5. 提取到IProductRepository
  6. void DeleteProduct(Product product);

然后Controller:

  1. [HttpDelete("{id}")]
  2. public IActionResult Delete(int id)
  3. {
  4. var model = _productRepository.GetProduct(id);
  5. if (model == null)
  6. {
  7. return NotFound();
  8. }
  9. _productRepository.DeleteProduct(model);
  10. if (!_productRepository.Save())
  11. {
  12. return StatusCode(500, "删除的时候出错");
  13. }
  14. _mailService.Send("Product Deleted",$"Id为{id}的产品被删除了");
  15. return NoContent();
  16. }

运行:
1-05.从头编写 asp.net core 2.0 web api 基础框架 (5) EF CRUD - 图18
Ok。
第一大部分先写到这。。。。。。。。。。。。
接下来几天比较忙,然后我再编写第二大部分。我会直接弄一个已经重构好的模板,简单讲一下,然后重点是Identity Server 4.
到目前为止可以进行CRUD操作了,接下来需要把项目重构一下,然后再简单用一下Identity Server4。