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(注意虚拟 TagsTagIds):

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 但当它没有被注释时,它让我相信 ValueComparerValueConverter正在按照 Entity Framework Core 的预期工作。

这会导致标签数据库列存储此序列化值: