22
votes

I have a WebAPI 2.2 application with OData V4. Also I'm using EF 6.1.

In one of my entities I have a calculated property:

public class Person {
  public string FirstName { get; set; }
  public string LastName { get; set; }
  // Calculated Property - No setter
  public string FullName { 
    get {
      return FirstName + " " + LastName;
    }
} 

In order to provide the calculated property to my clients, I need to register in the OData Model

    public static IEdmModel GetModel()
    {
        ODataModelBuilder builder = new ODataConventionModelBuilder();
        builder.Namespace = "NavigationServices";
        builder.EntityType<Person>;   
        builder.EntityType<Person>().Property(a => a.FullName); // Calculated Property
        ....

        return builder.GetEdmModel();
    }

So, when I obtain my data in the client side, every object has the Calculated property.

However, when I try to Create (POST) a new element or Update (PUT) a existing one, my action don't recognize the element and generates an error saying that it doesn't find the "set method" for the property.

I read a couple of posts about read only properties in OData (apparently not supported) but I don't find a way to use OData with calculated properties.

Some advice of how to overcome this situation?

4

4 Answers

9
votes

Now there is a soft way for doing this which is to build a contract between the client and server using annotations.

In the Core vocabulary of the V4 standard, there is a such term:

<Term Name="Computed" Type="Core.Tag" DefaultValue="true" AppliesTo="Property">
    <Annotation Term="Core.Description" String="A value for this property is generated on both insert and update"/>
</Term>

In Web API OData, in WebConfig.cs, you write such code to add such annotation to your property:

ODataConventionModelBuilder builder = new ODataConventionModelBuilder();
var model = builder.GetEdmModel() as EdmModel;
model.SetVocabularyAnnotation(
    new EdmAnnotation(model.EntityContainer.FindEntitySet("People").EntityType().FindProperty("FullName"),
    new EdmTerm("Org.OData.Core.V1", "Computed", EdmPrimitiveTypeKind.Boolean),
    new EdmBooleanConstant(true)));

Then in your data, it'll look something like this:

<Annotations Target="V4Service.Models.Person/FullName">
    <Annotation Term="Org.OData.Core.V1.Computed" Bool="true"/>
</Annotations>

Through the steps above the service advertises that the FullName property on Person entity is computed by the service. Then in the controller methods for POST and PATCH requests you can have you own logic of ignoring any value sent by the client for the FullName property and compute your own.

I'm not sure which client you are using. If you're using OData Client for .NET, our support for getting annotation values will be in our next release. If you don't mind using EdmLib directly, the annotation value retrieval support has already been added.

6
votes

You are right, OData doesn't support read-only properties at this time.

However, it supports read-only entities.

Or, you can trick OData by adding a setter which does nothing to your property.

public string FullName
{ 
    get
    {
        return FirstName + " " + LastName;
    }
    set
    {
        // do nothing
    }
}

This is how you set an entity as read-only:

public class Northwind : DataService<NorthwindEntities>
{
    // This method is called only once to initialize service-wide policies.
    public static void InitializeService(DataServiceConfiguration config)
    {
        config.SetEntitySetAccessRule("Customers", EntitySetRights.AllRead);
    }
}
1
votes

In your entity you need a [NotMapped] data annotation:

[NotMapped]
public string FullName => $"{FirstName} - {LastName}";

in your OData configuration:

builder.EntityType<Person>;
builder.StructuralTypes.First(t => t.ClrType == typeof(Person))
                .AddProperty(typeof(Person).GetProperty(nameof(Person.FullName)));

this way you don't need an empty setter.

-1
votes

You could use a database computed property instead of a class computed property.

This article describes EF and database computed properties. One difference that I see in your sample code compared to the article is that you don't have any setter in your property. If you set your property to use Auto Accessors with a private set

public string FullName { 
    get;
    private set; 
}

Then create the column in the database as a computed column.

ALTER TABLE dbo.Users ADD FullName AS FirstName + ' ' + LastName

As an additional plus you'll be able to query against this property using odata and Linq.