Entity Framework Core - ValueConverter with ValueComparer 将 Enum 转换为 Class 不工作
Entity Framework Core - ValueConverter with ValueComparer to convert Enum to Class not working
我正在尝试通过 Postman:
将此 Json 发送到我的 API
{
"name": "yummy food",
"brand": "brand",
"tags": [
"1",
"2"
]
}
但是我收到这个错误:
{
"type": "https://tools.ietf.org/html/rfc7231#section-6.5.1",
"title": "One or more validation errors occurred.",
"status": 400,
"traceId": "00-b7a6042817b5124294f5c6d2f6169f05-70d797d0744bfe40-00",
"errors": {
"$.tags[0]": [
"The JSON value could not be converted to GroceryItemTag. Path: $.tags[0] | LineNumber: 4 | BytePositionInLine: 11."
]
}
}
post 中的 GroceryItemTag 字段(“标签”)是枚举,但使用查找 table 成为具有枚举 ID、名称和 iconCodePoint 字段的 GroceryItemTag
对象.
post人工请求:
这是我的 entity framework 核心模型:
杂货商品:
using System.Collections.Generic;
namespace Vepo.Domain
{
public class GroceryItem : VeganItem<GroceryItem, GroceryItemTagEnum, GroceryItemTag>
{
public string Name {get; set;}
public string Brand {get; set;}
public string Description {get; set;}
public string Image {get; set;}
public virtual ICollection<GroceryItemGroceryStore> GroceryItemGroceryStores { get; set; }
}
}
GroceryItem 的基础 class(注意虚拟 Tags
和 TagIds
):
using System.Collections.Generic;
namespace Vepo.Domain
{
public abstract class VeganItem<VeganItemType, VeganItemTagEnumType, VeganItemTagType>
{
public int Id { get; set; }
public int IsNotVeganCount { get; set; }
public int IsVeganCount { get; set; }
public int RatingsCount { get; set; }
public int Rating { get; set; }
public List<VeganItemTagEnumType> TagIds { get; set; }
public virtual List<VeganItemTagType> Tags { get; set; }
public List<Establishment<VeganItemType>> Establishments { get; set; }
public int CurrentRevisionId { get; set; }
}
}
GroceryItemTagEnum:
public enum GroceryItemTagEnum
{
BabyAndChild = 1,
Baking,
Bathroom,
BeerAndWine,
Condiments,
Confectionary,
Cooking,
Dessert,
Drinks,
FauxDairy,
FauxMeat,
FauxSeafood,
FridgeAndDeli,
Frozen,
HealthFood,
HouseHold,
Other,
Pantry,
Pet,
}
GroceryItemTag class 用于查找 table:
public class GroceryItemTag
{
public GroceryItemTagEnum Id { get; set; }
public int IconCodePoint {get; set;}
public string Name { get; set; }
}
控制器,注意PostGroceryItem
:
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 Vepo.Domain;
namespace Vepo.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class GroceryItemsController : ControllerBase
{
private readonly VepoContext _context;
public GroceryItemsController(VepoContext context)
{
_context = context;
}
// GET: api/GroceryItems
[HttpGet]
public async Task<ActionResult<IEnumerable<GroceryItem>>> GetGroceryItems()
{
return await _context.GroceryItems.ToListAsync();
}
// GET: api/GroceryItems/5
[HttpGet("{id}")]
public async Task<ActionResult<GroceryItem>> GetGroceryItem(int id)
{
var groceryItem = await _context.GroceryItems.FindAsync(id);
if (groceryItem == null)
{
return NotFound();
}
return groceryItem;
}
// PUT: api/GroceryItems/5
// To protect from overposting attacks, see https://go.microsoft.com/fwlink/?linkid=2123754
[HttpPut("{id}")]
public async Task<IActionResult> PutGroceryItem(int id, GroceryItem groceryItem)
{
if (id != groceryItem.Id)
{
return BadRequest();
}
_context.Entry(groceryItem).State = EntityState.Modified;
try
{
await _context.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
if (!GroceryItemExists(id))
{
return NotFound();
}
else
{
throw;
}
}
return NoContent();
}
// POST: api/GroceryItems
// To protect from overposting attacks, see https://go.microsoft.com/fwlink/?linkid=2123754
[HttpPost]
public async Task<ActionResult<GroceryItem>> PostGroceryItem(GroceryItem groceryItem)
{
_context.GroceryItems.Add(groceryItem);
await _context.SaveChangesAsync();
return CreatedAtAction("GetGroceryItem", new { id = groceryItem.Id }, groceryItem);
}
// DELETE: api/GroceryItems/5
[HttpDelete("{id}")]
public async Task<IActionResult> DeleteGroceryItem(int id)
{
var groceryItem = await _context.GroceryItems.FindAsync(id);
if (groceryItem == null)
{
return NotFound();
}
_context.GroceryItems.Remove(groceryItem);
await _context.SaveChangesAsync();
return NoContent();
}
private bool GroceryItemExists(int id)
{
return _context.GroceryItems.Any(e => e.Id == id);
}
}
}
我的数据库上下文为 GroceryItemTag 查找设置种子 table:
using Microsoft.EntityFrameworkCore;
namespace Vepo.Domain
{
public class VepoContext : DbContext
{
public VepoContext(DbContextOptions<VepoContext> options)
: base(options)
{
}
protected override void OnModelCreating(ModelBuilder builder)
{
builder.Entity<GroceryItemGroceryStore>().HasKey(table => new
{
table.GroceryItemId,
table.GroceryStoreId
});
builder.Entity<MenuItemRestaurant>().HasKey(table => new
{
table.MenuItemId,
table.RestaurantId
});
builder.Entity<GroceryItemTag>()
.Property(tag => tag.Id)
.ValueGeneratedNever();
builder.Entity<MenuItemTag>()
.Property(tag => tag.Id)
.ValueGeneratedNever();
builder.Entity<GroceryItemTag>().HasData(
new GroceryItemTag[] {
new GroceryItemTag {
Name = "Baby & Child",
Id = GroceryItemTagEnum.BabyAndChild,
IconCodePoint = 0xf77c
},
new GroceryItemTag {
Name = "Baking",
Id = GroceryItemTagEnum.Baking,
IconCodePoint = 0xf563
},
new GroceryItemTag {
Name = "Beer & Wine",
Id = GroceryItemTagEnum.BeerAndWine,
IconCodePoint = 0xf4e3
},
new GroceryItemTag {
Name = "Condiments",
Id = GroceryItemTagEnum.Condiments,
IconCodePoint = 0xf72f
},
new GroceryItemTag {
Name = "Confectionary",
Id = GroceryItemTagEnum.Confectionary,
IconCodePoint = 0xf819
},
new GroceryItemTag {
Name = "Cooking",
Id = GroceryItemTagEnum.Cooking,
IconCodePoint = 0xe01d
},
new GroceryItemTag {
Name = "Dessert",
Id = GroceryItemTagEnum.Dessert,
IconCodePoint = 0xf810
},
new GroceryItemTag {
Name = "Drinks",
Id = GroceryItemTagEnum.Drinks,
IconCodePoint = 0xf804
},
new GroceryItemTag {
Name = "Faux Meat",
Id = GroceryItemTagEnum.FauxMeat,
IconCodePoint = 0xf814
},
new GroceryItemTag {
Name = "Faux Dairy",
Id = GroceryItemTagEnum.FauxDairy,
IconCodePoint = 0xf7f0
},
new GroceryItemTag {
Name = "Faux Seafood",
Id = GroceryItemTagEnum.FauxSeafood,
IconCodePoint = 0xf7fe
},
new GroceryItemTag {
Name = "Fridge & Deli",
Id = GroceryItemTagEnum.FridgeAndDeli,
IconCodePoint = 0xe026
},
new GroceryItemTag {
Name = "Frozen",
Id = GroceryItemTagEnum.Frozen,
IconCodePoint = 0xf7ad
},
new GroceryItemTag {
Name = "Bathroom",
Id = GroceryItemTagEnum.Bathroom,
IconCodePoint = 0xe06b
},
new GroceryItemTag {
Name = "Health Food",
Id = GroceryItemTagEnum.HealthFood,
IconCodePoint = 0xf787
},
new GroceryItemTag {
Name = "Household",
Id = GroceryItemTagEnum.HouseHold,
IconCodePoint = 0xf898
},
new GroceryItemTag {
Name = "Pantry",
Id = GroceryItemTagEnum.Pantry,
IconCodePoint = 0xf7eb
},
new GroceryItemTag {
Name = "Pet",
Id = GroceryItemTagEnum.Pet,
IconCodePoint = 0xf6d3
},
new GroceryItemTag {
Name = "Other",
Id = GroceryItemTagEnum.Other,
IconCodePoint = 0xf39b
}});
builder.Entity<GroceryItem>()
.Property(e => e.Tags)
.HasConversion(
v => JsonSerializer.Serialize(v, null),
v => JsonSerializer.Deserialize<List<GroceryItemTag>>(v, null),
new ValueComparer<IList<GroceryItemTag>>(
(c1, c2) => c1.SequenceEqual(c2),
c => c.Aggregate(0, (a, v) => HashCode.Combine(a, v.GetHashCode())),
c => (IList<GroceryItemTag>)c.ToList()));
}
public DbSet<GroceryItem> GroceryItems { get; set; }
public DbSet<GroceryItemGroceryStore> GroceryItemGroceryStores { get; set; }
public DbSet<MenuItemRestaurant> MenuItemRestaurants { get; set; }
public DbSet<MenuItem> MenuItems { get; set; }
public DbSet<GroceryStore> GroceryStores { get; set; }
public DbSet<GroceryItemTag> GroceryItemTags { get; set; }
public DbSet<MenuItemTag> MenuItemTags { get; set; }
public DbSet<Restaurant> Restaurants { get; set; }
}
}
如何从 postman 发送 post 以便能够将标签 ID 转换为 GroceryItemTag
我知道我需要这样做 (See official Microsoft docs):
builder.Entity<GroceryItem>()
.Property(e => e.Tags)
.HasConversion(
v => JsonSerializer.Serialize(v, null),
v => JsonSerializer.Deserialize<List<GroceryItemTag>>(v, null),
new ValueComparer<IList<GroceryItemTag>>(
(c1, c2) => c1.SequenceEqual(c2),
c => c.Aggregate(0, (a, v) => HashCode.Combine(a, v.GetHashCode())),
c => (IList<GroceryItemTag>)c.ToList()));
我就是无法让它正常工作,例如,它编译了,但我得到了完全相同的错误。完全没有变化。
我相信我误解了 ValueConverter/ValueComparer 用于存储整个对象而不仅仅是一个枚举 ID。我认为发送的 JSON 需要有完整的对象值,而不仅仅是枚举(id)。我认为 ValueConverter/ValueComparer
使查找 table 过时。
我删除了 VeganItem.TagIds
,使 VeganItem.Tags
不是虚拟的,并将 json 负载更改为:
{
"name": "yummy food",
"brand": "brand",
"tags": [
{"name":"Baking", "id":2, "iconCodePoint": 23145}
]
}
它奏效了。我相信这是应该做的,因为我在此处注释掉代码时也尝试过:
builder.Entity<GroceryItem>()
.Property(e => e.Tags)
.HasConversion(
v => JsonSerializer.Serialize(v, null),
v => JsonSerializer.Deserialize<List<GroceryItemTag>>(v, null),
new ValueComparer<IList<GroceryItemTag>>(
(c1, c2) => c1.SequenceEqual(c2),
c => c.Aggregate(0, (a, v) => HashCode.Combine(a, v.GetHashCode())),
c => (IList<GroceryItemTag>)c.ToList()));
当上面的代码被注释掉时,它没有成功转换 JSON 但当它没有被注释时,它让我相信 ValueComparer
和 ValueConverter
正在按照 Entity Framework Core 的预期工作。
这会导致标签数据库列存储此序列化值:
我正在尝试通过 Postman:
将此 Json 发送到我的 API{
"name": "yummy food",
"brand": "brand",
"tags": [
"1",
"2"
]
}
但是我收到这个错误:
{
"type": "https://tools.ietf.org/html/rfc7231#section-6.5.1",
"title": "One or more validation errors occurred.",
"status": 400,
"traceId": "00-b7a6042817b5124294f5c6d2f6169f05-70d797d0744bfe40-00",
"errors": {
"$.tags[0]": [
"The JSON value could not be converted to GroceryItemTag. Path: $.tags[0] | LineNumber: 4 | BytePositionInLine: 11."
]
}
}
post 中的 GroceryItemTag 字段(“标签”)是枚举,但使用查找 table 成为具有枚举 ID、名称和 iconCodePoint 字段的 GroceryItemTag
对象.
post人工请求:
这是我的 entity framework 核心模型:
杂货商品:
using System.Collections.Generic;
namespace Vepo.Domain
{
public class GroceryItem : VeganItem<GroceryItem, GroceryItemTagEnum, GroceryItemTag>
{
public string Name {get; set;}
public string Brand {get; set;}
public string Description {get; set;}
public string Image {get; set;}
public virtual ICollection<GroceryItemGroceryStore> GroceryItemGroceryStores { get; set; }
}
}
GroceryItem 的基础 class(注意虚拟 Tags
和 TagIds
):
using System.Collections.Generic;
namespace Vepo.Domain
{
public abstract class VeganItem<VeganItemType, VeganItemTagEnumType, VeganItemTagType>
{
public int Id { get; set; }
public int IsNotVeganCount { get; set; }
public int IsVeganCount { get; set; }
public int RatingsCount { get; set; }
public int Rating { get; set; }
public List<VeganItemTagEnumType> TagIds { get; set; }
public virtual List<VeganItemTagType> Tags { get; set; }
public List<Establishment<VeganItemType>> Establishments { get; set; }
public int CurrentRevisionId { get; set; }
}
}
GroceryItemTagEnum:
public enum GroceryItemTagEnum
{
BabyAndChild = 1,
Baking,
Bathroom,
BeerAndWine,
Condiments,
Confectionary,
Cooking,
Dessert,
Drinks,
FauxDairy,
FauxMeat,
FauxSeafood,
FridgeAndDeli,
Frozen,
HealthFood,
HouseHold,
Other,
Pantry,
Pet,
}
GroceryItemTag class 用于查找 table:
public class GroceryItemTag
{
public GroceryItemTagEnum Id { get; set; }
public int IconCodePoint {get; set;}
public string Name { get; set; }
}
控制器,注意PostGroceryItem
:
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 Vepo.Domain;
namespace Vepo.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class GroceryItemsController : ControllerBase
{
private readonly VepoContext _context;
public GroceryItemsController(VepoContext context)
{
_context = context;
}
// GET: api/GroceryItems
[HttpGet]
public async Task<ActionResult<IEnumerable<GroceryItem>>> GetGroceryItems()
{
return await _context.GroceryItems.ToListAsync();
}
// GET: api/GroceryItems/5
[HttpGet("{id}")]
public async Task<ActionResult<GroceryItem>> GetGroceryItem(int id)
{
var groceryItem = await _context.GroceryItems.FindAsync(id);
if (groceryItem == null)
{
return NotFound();
}
return groceryItem;
}
// PUT: api/GroceryItems/5
// To protect from overposting attacks, see https://go.microsoft.com/fwlink/?linkid=2123754
[HttpPut("{id}")]
public async Task<IActionResult> PutGroceryItem(int id, GroceryItem groceryItem)
{
if (id != groceryItem.Id)
{
return BadRequest();
}
_context.Entry(groceryItem).State = EntityState.Modified;
try
{
await _context.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
if (!GroceryItemExists(id))
{
return NotFound();
}
else
{
throw;
}
}
return NoContent();
}
// POST: api/GroceryItems
// To protect from overposting attacks, see https://go.microsoft.com/fwlink/?linkid=2123754
[HttpPost]
public async Task<ActionResult<GroceryItem>> PostGroceryItem(GroceryItem groceryItem)
{
_context.GroceryItems.Add(groceryItem);
await _context.SaveChangesAsync();
return CreatedAtAction("GetGroceryItem", new { id = groceryItem.Id }, groceryItem);
}
// DELETE: api/GroceryItems/5
[HttpDelete("{id}")]
public async Task<IActionResult> DeleteGroceryItem(int id)
{
var groceryItem = await _context.GroceryItems.FindAsync(id);
if (groceryItem == null)
{
return NotFound();
}
_context.GroceryItems.Remove(groceryItem);
await _context.SaveChangesAsync();
return NoContent();
}
private bool GroceryItemExists(int id)
{
return _context.GroceryItems.Any(e => e.Id == id);
}
}
}
我的数据库上下文为 GroceryItemTag 查找设置种子 table:
using Microsoft.EntityFrameworkCore;
namespace Vepo.Domain
{
public class VepoContext : DbContext
{
public VepoContext(DbContextOptions<VepoContext> options)
: base(options)
{
}
protected override void OnModelCreating(ModelBuilder builder)
{
builder.Entity<GroceryItemGroceryStore>().HasKey(table => new
{
table.GroceryItemId,
table.GroceryStoreId
});
builder.Entity<MenuItemRestaurant>().HasKey(table => new
{
table.MenuItemId,
table.RestaurantId
});
builder.Entity<GroceryItemTag>()
.Property(tag => tag.Id)
.ValueGeneratedNever();
builder.Entity<MenuItemTag>()
.Property(tag => tag.Id)
.ValueGeneratedNever();
builder.Entity<GroceryItemTag>().HasData(
new GroceryItemTag[] {
new GroceryItemTag {
Name = "Baby & Child",
Id = GroceryItemTagEnum.BabyAndChild,
IconCodePoint = 0xf77c
},
new GroceryItemTag {
Name = "Baking",
Id = GroceryItemTagEnum.Baking,
IconCodePoint = 0xf563
},
new GroceryItemTag {
Name = "Beer & Wine",
Id = GroceryItemTagEnum.BeerAndWine,
IconCodePoint = 0xf4e3
},
new GroceryItemTag {
Name = "Condiments",
Id = GroceryItemTagEnum.Condiments,
IconCodePoint = 0xf72f
},
new GroceryItemTag {
Name = "Confectionary",
Id = GroceryItemTagEnum.Confectionary,
IconCodePoint = 0xf819
},
new GroceryItemTag {
Name = "Cooking",
Id = GroceryItemTagEnum.Cooking,
IconCodePoint = 0xe01d
},
new GroceryItemTag {
Name = "Dessert",
Id = GroceryItemTagEnum.Dessert,
IconCodePoint = 0xf810
},
new GroceryItemTag {
Name = "Drinks",
Id = GroceryItemTagEnum.Drinks,
IconCodePoint = 0xf804
},
new GroceryItemTag {
Name = "Faux Meat",
Id = GroceryItemTagEnum.FauxMeat,
IconCodePoint = 0xf814
},
new GroceryItemTag {
Name = "Faux Dairy",
Id = GroceryItemTagEnum.FauxDairy,
IconCodePoint = 0xf7f0
},
new GroceryItemTag {
Name = "Faux Seafood",
Id = GroceryItemTagEnum.FauxSeafood,
IconCodePoint = 0xf7fe
},
new GroceryItemTag {
Name = "Fridge & Deli",
Id = GroceryItemTagEnum.FridgeAndDeli,
IconCodePoint = 0xe026
},
new GroceryItemTag {
Name = "Frozen",
Id = GroceryItemTagEnum.Frozen,
IconCodePoint = 0xf7ad
},
new GroceryItemTag {
Name = "Bathroom",
Id = GroceryItemTagEnum.Bathroom,
IconCodePoint = 0xe06b
},
new GroceryItemTag {
Name = "Health Food",
Id = GroceryItemTagEnum.HealthFood,
IconCodePoint = 0xf787
},
new GroceryItemTag {
Name = "Household",
Id = GroceryItemTagEnum.HouseHold,
IconCodePoint = 0xf898
},
new GroceryItemTag {
Name = "Pantry",
Id = GroceryItemTagEnum.Pantry,
IconCodePoint = 0xf7eb
},
new GroceryItemTag {
Name = "Pet",
Id = GroceryItemTagEnum.Pet,
IconCodePoint = 0xf6d3
},
new GroceryItemTag {
Name = "Other",
Id = GroceryItemTagEnum.Other,
IconCodePoint = 0xf39b
}});
builder.Entity<GroceryItem>()
.Property(e => e.Tags)
.HasConversion(
v => JsonSerializer.Serialize(v, null),
v => JsonSerializer.Deserialize<List<GroceryItemTag>>(v, null),
new ValueComparer<IList<GroceryItemTag>>(
(c1, c2) => c1.SequenceEqual(c2),
c => c.Aggregate(0, (a, v) => HashCode.Combine(a, v.GetHashCode())),
c => (IList<GroceryItemTag>)c.ToList()));
}
public DbSet<GroceryItem> GroceryItems { get; set; }
public DbSet<GroceryItemGroceryStore> GroceryItemGroceryStores { get; set; }
public DbSet<MenuItemRestaurant> MenuItemRestaurants { get; set; }
public DbSet<MenuItem> MenuItems { get; set; }
public DbSet<GroceryStore> GroceryStores { get; set; }
public DbSet<GroceryItemTag> GroceryItemTags { get; set; }
public DbSet<MenuItemTag> MenuItemTags { get; set; }
public DbSet<Restaurant> Restaurants { get; set; }
}
}
如何从 postman 发送 post 以便能够将标签 ID 转换为 GroceryItemTag
我知道我需要这样做 (See official Microsoft docs):
builder.Entity<GroceryItem>()
.Property(e => e.Tags)
.HasConversion(
v => JsonSerializer.Serialize(v, null),
v => JsonSerializer.Deserialize<List<GroceryItemTag>>(v, null),
new ValueComparer<IList<GroceryItemTag>>(
(c1, c2) => c1.SequenceEqual(c2),
c => c.Aggregate(0, (a, v) => HashCode.Combine(a, v.GetHashCode())),
c => (IList<GroceryItemTag>)c.ToList()));
我就是无法让它正常工作,例如,它编译了,但我得到了完全相同的错误。完全没有变化。
我相信我误解了 ValueConverter/ValueComparer 用于存储整个对象而不仅仅是一个枚举 ID。我认为发送的 JSON 需要有完整的对象值,而不仅仅是枚举(id)。我认为 ValueConverter/ValueComparer
使查找 table 过时。
我删除了 VeganItem.TagIds
,使 VeganItem.Tags
不是虚拟的,并将 json 负载更改为:
{
"name": "yummy food",
"brand": "brand",
"tags": [
{"name":"Baking", "id":2, "iconCodePoint": 23145}
]
}
它奏效了。我相信这是应该做的,因为我在此处注释掉代码时也尝试过:
builder.Entity<GroceryItem>()
.Property(e => e.Tags)
.HasConversion(
v => JsonSerializer.Serialize(v, null),
v => JsonSerializer.Deserialize<List<GroceryItemTag>>(v, null),
new ValueComparer<IList<GroceryItemTag>>(
(c1, c2) => c1.SequenceEqual(c2),
c => c.Aggregate(0, (a, v) => HashCode.Combine(a, v.GetHashCode())),
c => (IList<GroceryItemTag>)c.ToList()));
当上面的代码被注释掉时,它没有成功转换 JSON 但当它没有被注释时,它让我相信 ValueComparer
和 ValueConverter
正在按照 Entity Framework Core 的预期工作。
这会导致标签数据库列存储此序列化值: