10

Say I have a very simple type which I'd like to expose on an OData feed as part of a collection using a .NET C# webapi controller:

public class Image
{
    /// <summary>
    /// Get the name of the image.
    /// </summary>
    public string Name { get; set; }

    public int Id { get; set; }

    internal System.IO.Stream GetProperty(string p)
    {
        throw new System.NotImplementedException();
    }

    private Dictionary<string, string> propBag = new Dictionary<string, string>();
    internal string GetIt(string p)
    {
        return propBag[p];
    }
}

In my WebApiConfig.cs I do the standard thing to configure it:

        ODataModelBuilder modelBuilder = new ODataConventionModelBuilder();
        var imagesES = modelBuilder.EntitySet<Image>("Images");

And according to Excel, this is a great feed. But in my collection, that propBag contains a finite list of other data (say "a", "b", and "c" or similar). I'd like them as extra properties in my OData feed. My first thought was to try something like this when the configuration happened:

        ODataModelBuilder modelBuilder = new ODataConventionModelBuilder();
        var imagesES = modelBuilder.EntitySet<Image>("Images");
        images.EntityType.Property(c => c.GetIt("a"))

This fails completely because that is actually an expression tree that is being passed in, not a lambda function, and this method makes an attempt to parse it. And expects a property de-reference.

What direction should I be going in here? For some context: I'm trying to create an odata read-only source with a single simple flat object. Getting the simple version working was easy following tutorials found on the web.

Update:

cellik, below, pointed me in one direction. I just followed it as far as I could go, and I got very close.

First, I created a property info class to represent the dynamic properties:

public class LookupInfoProperty : PropertyInfo
{
    private Image _image;
    private string _propName;
    public LookupInfoProperty(string pname)
    {
        _propName = pname;
    }

    public override PropertyAttributes Attributes
    {
        get { throw new NotImplementedException(); }
    }

    public override bool CanRead
    {
        get { return true; }
    }

    public override bool CanWrite
    {
        get { return false; }
    }

    public override MethodInfo[] GetAccessors(bool nonPublic)
    {
        throw new NotImplementedException();
    }

    public override MethodInfo GetGetMethod(bool nonPublic)
    {
        throw new NotImplementedException();
    }

    public override ParameterInfo[] GetIndexParameters()
    {
        throw new NotImplementedException();
    }

    public override MethodInfo GetSetMethod(bool nonPublic)
    {
        throw new NotImplementedException();
    }

    public override object GetValue(object obj, BindingFlags invokeAttr, Binder binder, object[] index, System.Globalization.CultureInfo culture)
    {
        throw new NotImplementedException();
    }

    public override Type PropertyType
    {
        get { return typeof(string); }
    }

    public override void SetValue(object obj, object value, BindingFlags invokeAttr, Binder binder, object[] index, System.Globalization.CultureInfo culture)
    {
        throw new NotImplementedException();
    }

    public override Type DeclaringType
    {
        get { throw new NotImplementedException(); }
    }

    public override object[] GetCustomAttributes(Type attributeType, bool inherit)
    {
        throw new NotImplementedException();
    }

    public override object[] GetCustomAttributes(bool inherit)
    {
        return new object[0];
    }

    public override bool IsDefined(Type attributeType, bool inherit)
    {
        throw new NotImplementedException();
    }

    public override string Name
    {
        get { return _propName; }
    }

    public override Type ReflectedType
    {
        get { return typeof(Image); }
    }
}

As you can see, very few of the methods need to be implemented. I then created a custom serializer:

public class CustomSerializerProvider : DefaultODataSerializerProvider
{
    public override ODataEdmTypeSerializer CreateEdmTypeSerializer(IEdmTypeReference edmType)
    {
        if (edmType.IsEntity())
        {
            // entity type serializer
            return new CustomEntityTypeSerializer(edmType.AsEntity(), this);
        }
        return base.CreateEdmTypeSerializer(edmType);
    }
}

public class CustomEntityTypeSerializer : ODataEntityTypeSerializer
{
    public CustomEntityTypeSerializer(IEdmEntityTypeReference edmType, ODataSerializerProvider serializerProvider)
        : base(edmType, serializerProvider)
    {
    }

    /// <summary>
    /// If we are looking at the proper type, try to do a prop bag lookup first.
    /// </summary>
    /// <param name="structuralProperty"></param>
    /// <param name="entityInstanceContext"></param>
    /// <returns></returns>
    public override ODataProperty CreateStructuralProperty(IEdmStructuralProperty structuralProperty, EntityInstanceContext entityInstanceContext)
    {
        if ((structuralProperty.DeclaringType as IEdmEntityType).Name == "Image")
        {
            var r = (entityInstanceContext.EntityInstance as Image).GetIt(structuralProperty.Name);
            if (r != null)
                return new ODataProperty() { Name = structuralProperty.Name, Value = r };
        }
        return base.CreateStructuralProperty(structuralProperty, entityInstanceContext);
    }
}

Which are configured in my WebApiConfig Register method:

config.Formatters.InsertRange(0, ODataMediaTypeFormatters.Create(new CustomSerializerProvider(), new DefaultODataDeserializerProvider()));

And, finally, I create the Image class, and add the "a" property to it:

        ODataModelBuilder modelBuilder = new ODataConventionModelBuilder();
        var imagesES = modelBuilder.EntitySet<Image>("Images");
        var iST = modelBuilder.StructuralTypes.Where(t => t.Name == "Image").FirstOrDefault();
        iST.AddProperty(new LookupInfoProperty("a"));
        Microsoft.Data.Edm.IEdmModel model = modelBuilder.GetEdmModel();
        config.Routes.MapODataRoute("ODataRoute", "odata", model);

There is only one problem - in most of the test queries coming from a client like Excel, the EntityInstance is null. Indeed, it is a depreciated property - you are to use EdmObject instead. And that does have a reference to the actual object Instance. However, in the current nightly builds (which you must have for any of this to work) the EdmObject's access is internal - and so one can't use it.

Update 2: There is some minimal documentation on this on the asp CodePlex site.

So very close!

marc_s
  • 732,580
  • 175
  • 1,330
  • 1,459
Gordon
  • 3,012
  • 2
  • 26
  • 35
  • I spent a small amount of time looking at the source code. Came up with the idea of using the model builder to look for the StructuralType, and then do an AddProperty with my own custom version of PropertyInfo (which is an abstract class). However, looks like the member reference is hard-coded. It isn't trying to use the method that could be returned by PropertyInfo. :( – Gordon Jun 18 '13 at 08:02
  • Ok, it looks like the actual property lookup is done by TryGetValue in EdmStructuredObject - where it uses the type's GetProperty to do the work, so PropertyInfo, above, is just used for the name here. So, perhaps the question now is how do I inject a IEdmStructuredObject into my model, where I configure it myself, somehow? – Gordon Jun 18 '13 at 08:22
  • Was this issue solved (EdmObject's access is internal) in the latest release? – lnaie Nov 23 '13 at 20:59

2 Answers2

4

Not really a solution to your problem but hope this helps.

This is one of the top features in our backlog. We tend to call it 'Typeless support' internally in our team while referring it.

The problem with web API is that it requires a strong CLR type for each and every EDM type that the service is exposing. Also, the mapping between the CLR type and the EDM type is one-to-one and not configurable. This is also how most IQueryable implementations work too.

The idea with typeless support is to break that requirement and provide support for having EDM types without a backing strong CLR type. For example, all your EDM entities can be backed by a key-value dictionary.

RaghuRam Nadiminti
  • 6,718
  • 1
  • 33
  • 30
  • Indeed, this along with two other requirements are starting to make me think I should roll my own. One is these are all read-only. Another is that my web service will have an arbitrary number of collections (added at runtime), and each collection has a set of OData tables associated with them (similar to the above, in that they are dynamic, but each one having different properties). So, I have dynamic along two axes here. :( – Gordon Jun 19 '13 at 06:18
  • @RaghuRam Nadiminti YES PLEASE. Would love more flexibility in the ODataConventionModelBuilder. I imagine the use of more advanced Automapper style conventions + customization syntax to easily control the mapping process). Would give service developers full control over the server entities vs EDM types. Seems like the odata + webapi team is a bit too concerned with trying to maintain referential integrity / keep client-server very tightly bound, but lots of good cases where actually developers need the flexibility more than the scaffolding / out of box ease of use. Thanks. – MemeDeveloper Apr 17 '14 at 11:35
  • I just found this on the web - http://blogs.msdn.com/b/leohu/archive/2013/11/05/typeless-entity-object-support-in-webapi.aspx it might be exactly a solution. Once I get back to this (not right away!) perhaps we can marked this answered! – Gordon Jul 28 '14 at 14:40
  • 3
    Looks like this was added in the August 2014 release. https://aspnet.codeplex.com/SourceControl/latest#Samples/WebApi/OData/v4/ODataOpenTypeSample/ – Chris Sep 05 '14 at 20:13
1

There are extension points on how the serialization is done in Web API Odata

Here is an example.

customizing odata output from asp.net web api

Though the question was different, I guess what you want could be done using the same approach (i.e. overriding how the entries are serialized.)

Especially, in the overridden CreateEntry you may change entry.Properties

(Note that this version is not released yet AFAIK but could be downloaded as a prerelease version.)

Community
  • 1
  • 1
cellik
  • 2,116
  • 2
  • 19
  • 29
  • Thanks! This looks like a good avenue to explore. I'll try it out this evening. – Gordon Jun 18 '13 at 17:20
  • So very very close - see the update above. Thanks; regardless, I learned a lot about the OData infrastructure because of this! – Gordon Jun 19 '13 at 06:18
  • This does not work, I was able to get to the point when I injected the properties, But the writer - which writes requested format to the output - does the check whether the injected property really exists on the type So dont try to do this :) unless you also inject your writer somehow – Kirill Chilingarashvili Mar 14 '14 at 18:07