Estoy tratando de construir una API con EntityFramework y OData v4.

Problema: necesito algunos datos adicionales, extraProperty, que no están en mi base de datos para crear un nuevo Item, pero oData no reconocerá esto como Item si agrego algunos datos a mi objeto JSON en mi llamada POST.

Yo uso EntityFrameWork, así que, de acuerdo con esta pregunta Traté de usar la anotación de datos [NotMapped] o .Ignore(t => t.extraProperty); en mi modelo. Pero oData parece ignorarlo.

Todo lo que obtengo de esta POST, con esto extraProperty, es:

No admite valores sin tipo en tipo no abierto.

Código

JSON envío mi llamada POST:

{
  "name": "John Doe",
  "extraProperty": "Random string"
}

$ metadatos:

<?xml version="1.0" encoding="utf-8"?>
<edmx:Edmx Version="4.0" xmlns:edmx="http://docs.oasis-open.org/odata/ns/edmx">
    <edmx:DataServices>
        <Schema Namespace="MyApi.Models" xmlns="http://docs.oasis-open.org/odata/ns/edm">
            <EntityType Name="Items">
                <Key>
                    <PropertyRef Name="id" />
                </Key>
                <Property Name="id" Type="Edm.Int32" Nullable="false" />
                <Property Name="name" Type="Edm.String" Nullable="false" />                
            </EntityType>           
        </Schema>
    </edmx:DataServices>
</edmx:Edmx>

ODataConfig.cs

namespace MyApi.App_Start
{
    public class OdataConfig
    {
        public static void Register(HttpConfiguration config)
        {
            config.MapHttpAttributeRoutes();
            ODataConventionModelBuilder builder = new ODataConventionModelBuilder();
            builder.EntitySet<Items>("Items");
            config.Count().Filter().OrderBy().Expand().Select().MaxTop(null);
            config.MapODataServiceRoute("odata", "odata", builder.GetEdmModel());
        }
    }
}

Items.cs

[Table("Item.Items")]
public partial class Items
{
    [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2214:DoNotCallOverridableMethodsInConstructors")]
    public Items(){}

    public int id { get; set; }

    public string name { get; set; }

    [NotMapped] // I already tried this, it's not working
    public string extraProperty{ get; set; }
 }

MyModel.cs

public partial class MyModel: DbContext
{
    public MyModel()
        : base("name=MyModel")
    {

        Database.SetInitializer<MyModel>(null);
    }

    public virtual DbSet<Items> Items{ get; set; }

    protected override void OnModelCreating(DbModelBuilder modelBuilder)
    {
        // I also tried this but not working
        modelBuilder.Entity<Items>()
            .Ignore(e => e.extraProperty);
    }
}

MyController.cs

public class ItemsController : ODataController
{
    private MyModeldb = new MyModel();

    // POST: odata/Items 
    public async Task<IHttpActionResult> Post(Items items)
    {
        // items is always null when enterring here
        // and this condition is always triggered
        if (!ModelState.IsValid)
        {
            return BadRequest(ModelState);
        }

        // Do some stuff with extraProperty here

        db.Items.Add(items);
        await db.SaveChangesAsync();

        return Created(items);
    }
}

Paquete parcial.config

<package id="EntityFramework" version="6.2.0" targetFramework="net461" />
<package id="Microsoft.Data.Edm" version="5.8.3" targetFramework="net461" />
<package id="Microsoft.AspNet.OData" version="6.1.0" targetFramework="net45" />
<package id="Microsoft.Data.OData" version="5.8.3" targetFramework="net461" />
<package id="Microsoft.OData.Core" version="7.4.1" targetFramework="net45" />
<package id="Microsoft.OData.Edm" version="7.4.1" targetFramework="net45" />

También pensé en hacer un interceptor, para purgar mi json antes de llamar a la publicación, pero de acuerdo con esta pregunta, Web API OData no admite interceptor de consultas ...

¿Cómo puedo lidiar con este error y evitarlo? Realmente necesito procesar extraProperty en el método POST, o al menos, justo antes.

2
Toodoo 27 feb. 2018 a las 20:43

4 respuestas

La mejor respuesta

En su clase Items, elimine el atributo [NotMapped] para

public string extraProperty{ get; set; }

Y deja el siguiente código en tu clase MyModel

modelBuilder.Entity<Items>()
            .Ignore(e => e.extraProperty);

El atributo [NotMapped] le dice a OData que ignore el extraProperty al serializar y deserializar la clase Items. Pero dado que desea usarlo en la solicitud POST de ItemsController, no puede usar el atributo [NotMapped] en este escenario, por lo que el Enlace de modelo se lleva a cabo como lo desee.

2
hem 28 mar. 2018 a las 17:32

Dependiendo de para qué desea usar sus "Datos adicionales", ¿no sería más simple usar un modelo de entrada para su método de publicación, hacer lo que quiera con los datos y luego llenar las propiedades apropiadas del Modelo EF. Esa sería la solución más simple si Annotations y FluentAPI no funcionan para usted.

I.e.

public partial class ItemsInput
{
    public int id { get; set; }
    public string name { get; set; }
    public string extraProperty{ get; set; }
 }
 
 public async Task<IHttpActionResult> Post(ItemsInput itemsInput)
    {
        // This shouldn't be triggered anymore unless it's a valid error
        if (!ModelState.IsValid)
        {
            return BadRequest(ModelState);
        }

        // Do some stuff with extraProperty here
        
        //Convert the input object to json string
        var itemsInputJson = JsonConvert.SerializeObject(itemsInput);
        //Load json string to the EF Model, this will fill up all compatible
        //properties and ignore non-matching ones
        Items items = JsonConvert.DeserializeObject<Items>(itemsInputJson);
        db.Items.Add(items);
        await db.SaveChangesAsync();

        return Created(items);
    }
1
johnny 5 28 mar. 2018 a las 18:20

Puede asignar todo a un DTO usando AutoMapper y luego aplicar las QueryOptions manualmente en su controlador.

Nota: Recuerde incluir

usando AutoMapper;

usando AutoMapper.QueryableExtensions;

public class ItemDTO 
{
     public int Id { get; set;}
     public string Name { get; set;}
     public string CustomProperty { get; set; }
}

public class ItemsController : ApiController
{
    MyCustomContext _context;
    public ItemsController(MyCustomContext context)
    {
        _context = context;
    }

    public IEnumerable<ItemDTO> Get(ODataQueryOptions<Item> q)
    {
       var itemsQuery = _context.Items.AsQueryable();
       itemsQuery = q.ApplyTo(itemsQuery , new ODataQuerySettings()) as IQueryable<Item>;

       var mapperConfiguration = this.GetMapperConfiguration();
       return itemsQuery.ProjectTo<ItemDTO>(mapperConfiguration);
    }

    public IConfigurationProvider GetMapperConfiguration()
    {
        return new MapperConfiguration(x => x.CreateMap<Item, ItemDTO>().ForMember(m => m.CustomProperty, o => o.MapFrom(d => d.Id + "Custom")));
    }
}

Nota: Debe asignar usando el método MapFrom que no puede usar ResolveUsing

0
johnny 5 28 mar. 2018 a las 03:40

La forma FluentAPI simplemente funciona (probado muchas veces). ¿Puedes proporcionar tus $ metadatos? Intente nuevamente con la eliminación del atributo NotMapped y agregue Ignorar en un generador de modelos.

Alternativamente, puede agregar esta propiedad al IEdmModel en el método GetEdmModel:

model.StructuralTypes.First(t => t.ClrType == typeof(Items)).AddProperty(typeof(Items).GetProperty("extraProperty"));
0
donMateo 27 feb. 2018 a las 19:20