Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Using OData in webapi for properties known only at runtime

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!

like image 333
Gordon Avatar asked Jun 17 '13 04:06

Gordon


1 Answers

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.

like image 151
RaghuRam Nadiminti Avatar answered Oct 05 '22 12:10

RaghuRam Nadiminti