2

I'm trying to build an API with EntityFramework and OData v4.

Issue : I need some extra data, extraProperty, that are not in my DB to create a new Item, but oData won't recognize this as an Item if I add some data to my JSON object in my POST call.

I use EntityFrameWork so, according to this question I tried to use the data annotation [NotMapped] or .Ignore(t => t.extraProperty); in my model. But oData seems to ignore it.

All I get from this POST, with this extraProperty, is :

Does not support untyped value in non-open type.

Code

JSON I send in my POST call :

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

$metadata :

<?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);
    }
}

Partial package.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" />

I also thought to make an interceptor, to purge my json before post is called, but according to this question, Web API OData does not support query interceptor...

How can I deal with this error and avoid it ? I really need to process extraProperty in POST method, or at least, just before.

Community
  • 1
  • 1
Toodoo
  • 8,570
  • 6
  • 35
  • 58
  • The exception message is a hint that you should use [Open Types](https://learn.microsoft.com/en-us/aspnet/web-api/overview/odata-support-in-aspnet-web-api/odata-v4/use-open-types-in-odata-v4) to handle additional data. – Gert Arnold Mar 27 '18 at 11:59
  • Have you tried just the modelBuilder.Entity().Ignore(e => e.extraProperty); without [NotMapped] annotation? – hem Mar 27 '18 at 15:30
  • Also, can you capture the Post request in Fiddler and see if Items are actually being sent in the Raw request? – hem Mar 27 '18 at 15:30
  • @hem, raw is fine and I already tried modelBuilder.Entity().Ignore(e => e.extraProperty); with and without NotMapped. Thanks for your comment. – Toodoo Mar 27 '18 at 15:53
  • 1
    @hem, I retried modelBuilder.Entity().Ignore(e => e.extraProperty); without NotMapped and it finally worked. Can you post this as an answer, so i'll give you the bounty. – Toodoo Mar 28 '18 at 11:56

4 Answers4

2

In your Items class, remove the [NotMapped] attribute for

public string extraProperty{ get; set; }

and leave the following code in your MyModel class

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

[NotMapped] attribute is telling OData to ignore the extraProperty when serialising and deserialising Items class. But since you want to use it in the POST request of ItemsController, you can't use the [NotMapped] attribute in this scenario, so the Model Binding takes place as you want it.

hem
  • 2,100
  • 1
  • 10
  • 11
  • 1
    I found that this still doesn't work, when I do this and you do a POST you get the following model validation error: `The property 'x' does not exist on type 'model'. Make sure to only use property names that are defined by the type.`. `.Ignore` seems to remove it completely from the model too. – Roger Far Oct 26 '20 at 16:54
1

Depending on what you want to use your "Extra Data" for, wouldn't it be simpler to use an input model for your post method, do what you want with the data, and then fill up the proper EF Model properties. That would be the simplest solution if the Annotations and FluentAPI aren't working for you

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);
}
Chris Schaller
  • 13,704
  • 3
  • 43
  • 81
Kaizer69
  • 373
  • 4
  • 18
  • Hi, thanks for your answer. I considered this solution for a while but i've got a lots of property and rebuilding proper EF Model will be ugly and dirty... – Toodoo Mar 27 '18 at 15:51
  • No prob just want to help. By rebuilding you mean transferring the properties from the input model to the EF Model? you can also use the newtonsoft json library to just serialize and deserialize the object. If the property doesn't exist in the destination object it's just ignored. that could make the rebuilding of the EF model "cleaner" – Kaizer69 Mar 27 '18 at 15:58
  • I edited my answer to show how you might do the rebuild via newtonsoft. The processing overhead for this should be negligible – Kaizer69 Mar 27 '18 at 16:27
0

FluentAPI way just works (tested many times). Can you provide your $metadata? Try again with removing the NotMapped attribute and adding Ignore on a model builder.

Alternatively, you can add this property to the IEdmModel in GetEdmModel method:

model.StructuralTypes.First(t => t.ClrType == typeof(Items)).AddProperty(typeof(Items).GetProperty("extraProperty"));
donMateo
  • 2,334
  • 1
  • 13
  • 14
  • Thanks for your answer. I tried again with both method, annotation and fluent API but i still get the error. I also added my $metadata and my OdataConfig.cs to my post. Can't find where's the IEdModel you're talking about. – Toodoo Feb 28 '18 at 09:24
  • Its builder in OdataConfig->Register :) – donMateo Feb 28 '18 at 12:06
0

You can just map everything to a DTO using AutoMapper, and then apply the QueryOptions manually in your controller.

Note: Remember to include

using AutoMapper;

using 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")));
    }
}

Note: You must map using the MapFrom method you cannot use ResolveUsing

Community
  • 1
  • 1
johnny 5
  • 19,893
  • 50
  • 121
  • 195