实体框架核心 - ValueConverter 与 ValueComparer 将枚举转换为类不起作用

问题描述

我正在尝试使用 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."
        ]
    }
}

帖子中的 GroceryItemTag 字段(“标签”)是一个枚举,但使用查找表作为 GroceryItemTag 对象,具有枚举 ID、名称和 iconCodePoint 字段。

邮递员请求:

enter image description here

这是我的实体框架核心模型:

杂货:

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 的基类(注意虚拟 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 类:

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 查找表提供种子:

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,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; }
    }
}

我如何从邮递员那里发送能够将标签 ID 转换为 GroceryItemTag 的帖子

我知道我需要这样做 (See official Microsoft docs):

    builder.Entity<GroceryItem>()
    .Property(e => e.Tags)
    .HasConversion(
        v => JsonSerializer.Serialize(v,new ValueComparer<IList<GroceryItemTag>>(
            (c1,c => (IList<GroceryItemTag>)c.ToList()));

我只是无法让它工作,例如,它可以编译,但我得到了完全相同的错误。完全没有变化。

解决方法

我相信我误解了 ValueConverter/ValueComparer 来存储整个对象,而不仅仅是一个枚举 ID。我认为发送的 JSON 需要具有完整的对象值,而不仅仅是枚举 (id)。我认为 ValueConverter/ValueComparer 会使查找表过时。

我删除了 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,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。

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

enter image description here