OData third look

ODataLogo-96This is a follow up on my OData first look and OData second look post. The sample code builds on the previous post and shows projection, data formats, batch processing and actions.
Every part has the exceptions and how I solved them.

Projection

Create a new object with the selected information from the OData resource. This way you create a project or a view on that data.
This one actually just worked without exceptions

Code

Select the SurveyName and the Reports from a Survey and store them in a View object.

public class View {
    public string NameOfSurvey { get; set; }
    public ICollection< Report> Reports { get; set; }
}
var projection = context.CreateQuery<Survey>("Surveys")
    .Select(s => new View { 
        NameOfSurvey = s.SurveyName, 
        Reports = s.Reports })
    .First();

Data format

The format of the data in the messages can be JSON or ATOM/XML and is configurable. The default is XML.

Exception

When you call the UseJson method without a parameter, you must use the LoadServiceModel property to provide a valid IEdmModel instance.

Solution

Load the metadata and parse it to an IEdmModel. Then set the Format to JSON. This is not needed when the default ATOM/XML is used.

Code

var model = default(Microsoft.Data.Edm.IEdmModel);
var metadataRequest = WebRequest.Create(context.GetMetadataUri());
using (var response = metadataRequest.GetResponse()) {
    using (var stream = response.GetResponseStream()) {
        using (var reader = System.Xml.XmlReader.Create(stream)) {
            model = Microsoft.Data.Edm.Csdl.EdmxReader.Parse(reader);
        }
    }
    response.Close();
}
context.Format.LoadServiceModel = () => model;
context.Format.UseJson();

Exception #2

415 Unsupported Media Type

Solution

The $count will return an integer and only that, not an object. JSON and ATOM/XML cannot handle this. You must read the bare response and parse it to an integer.

Code

// results in HTTP415
var requestNumberOfCompletedRecordsForFlightSurvey = new DataServiceRequest<int>(
    new Uri(context.BaseUri, "Surveys('1')/DatabaseReports/$count"));

// bare WebRequest will succeed
var requestNumberOfCompletedRecordsForFlightSurvey =
    new Uri(context.BaseUri, "Surveys('1')/DatabaseReports/$count?$filter=Status eq 'Complete'");
var request = WebRequest.Create(requestNumberOfCompletedRecordsForFlightSurvey);
using(var response = request.GetResponse()) {
    using(var stream = response.GetResponseStream()) {
        using(var reader = new System.IO.StreamReader(stream)) {
            var content = reader.ReadToEnd();
            int count = 0;
            if (int.TryParse(content, out count)) Console.WriteLine("Response '{0}'", count);
            else Console.WriteLine("Response not a number ({0})", content);
        }
    }
    response.Close();
}

Batch

Combine multiple requests as one message to the OData service. The result is again one message, containing all the results.

Nice bonus is that the context will “cache” expanded properties. The cache fills in the gaps for requests that did not expand.
In the code you’ll see that the second request expands on Reports and that only the first result misses the information. The third request expands on the DatabaseReports which were missing before. The last request gets all properties set without expanding.

Exception

415 Unsupported Media Type (again)

Solution

The exception is part of the complete result. Check for the result.StatusCode while iterating the batch.

Code

var requestFlightSurveyOperation = new DataServiceRequest<Survey>(
    new Uri(context.BaseUri, "FlightSurvey"));
var requestFlightSurvey = new DataServiceRequest<Survey>(
    new Uri(context.BaseUri, "Surveys?$filter=SurveyName eq 'Flight'&$expand=Reports"));
var requestFlightSurveyById = new DataServiceRequest<Survey>(
    new Uri(context.BaseUri, "Surveys('1')?$expand=DatabaseReports"));
var requestFlightSurveyOperationParameter = new DataServiceRequest<Survey>(
    new Uri(context.BaseUri, "GetSurvey?surveyName='Flight'" ));
// one request message for all requests with one response message containing all results
var batch = context.ExecuteBatch(requestFlightSurveyOperation, requestFlightSurvey, requestFlightSurveyById, requestFlightSurveyOperationParameter);
foreach (QueryOperationResponse result in batch) {
    if (result.StatusCode > 299 || result.StatusCode < 200) {
        Console.WriteLine( "An error occurred.");
        Console.WriteLine(result.Error.Message);
    } else if (result.Query.ElementType == typeof( Survey)) {
        foreach (Survey s in result) {
            Console.WriteLine("Found survey {0} with {1} reports and {2} database reports",
                s.SurveyName,
                s.Reports == null ? "null" : s.Reports.Count.ToString(),
                s.DatabaseReports == null ? "null" : s.DatabaseReports.Count.ToString());
        }
    }
}

Action

An action can mutate the data. This involves implementing some interfaces:

  • IServiceProvider, on the DataService to provide the IDataServiceActionProvider implementation
  • IDataServiceActionProvider implementation to be returned in the GetService of the DataService
  • IDataServiceInvokable implementation for the action
  • IUpdatable, on the DataProvider, just clear the default implementation of SaveChanges
  • IDataServiceUpdateProvider2, on the DataProvider, ScheduleInvokable should invoke the invokable

After implementing all this you can check it with the $select=DataProvider.* request
http://MACHINENAME/Reporting.Service/SurveyServiceOData.svc/Surveys?$select=SurveyProvider.*

Exception

‘The service operation ‘GetLastReport’ returned by the provider is not read-only. Please make sure that all the service operations are set to read-only.’

Solution

Make sure to call getLastReportAction.SetReadOnly();

Exception #2

‘The service action ‘GetLastReport’ has the binding parameter of type ‘Reporting.Entities.Survey’, but there is no visible resource set for that type.

Solution

Use the types from the IDataServiceMetadataProvider. Don’t try to create them yourself.

Code

var metadataprovider = context.GetService(typeof(IDataServiceMetadataProvider)) as IDataServiceMetadataProvider;
var surveyType = metadataprovider.Types.SingleOrDefault(s => s.Name == "Survey");

Exception #3

When ‘returnType’ is an entity type or an entity collection type, ‘resultSetPathExpression’ and ‘resultSet’ cannot be both null and the resource type of the result set must be assignable from ‘returnType’.

Solution

Use the right return resourceSet when creating the ServiceAction.

Exception #4

404 not found

Solution

Set breakpoint in every method of IDataServiceActionProvider and debug to the cause. Mine was the lookup statement in the TryResolveServiceAction.

Exception #5

405 Method Not Allowed

Solution

Do a POST not a GET

Exception #6

501 Not Implemented The data source must implement IUpdatable, IDataServiceUpdateProvider or IDataServiceUpdateProvider2 to support updates.

Solution

Implement the required interfaces on the DataProvider. (Data.SurveyProvider)

Exception #7

204 No content

Solution

The parameter can be a Query in stead of a single object. Use something like the First() statement to get the object.

Code

public class SurveyServiceOData : DataService<Data.SurveyProvider>, IServiceProvider {
    public static void InitializeService(DataServiceConfiguration config) {
        // other config left out for clarity
        config.SetServiceActionAccessRule("*", ServiceActionRights.Invoke);
    }
    // Other operations here ...

    public object GetService(Type serviceType) {
        if (serviceType == typeof(IDataServiceActionProvider)) {
            return new SurveyServiceActionProvider();
        } else {
            return null;
        }
    }
}
public class SurveyServiceActionProvider : IDataServiceActionProvider
{
    public bool AdvertiseServiceAction(DataServiceOperationContext operationContext, ServiceAction serviceAction, object resourceInstance, bool resourceInstanceInFeed, ref Microsoft.Data.OData.ODataAction actionToSerialize) {
        // everything is available
        return true;
    }
    public IDataServiceInvokable CreateInvokable(DataServiceOperationContext operationContext, ServiceAction serviceAction, object[] parameterTokens) {
        if (serviceAction.Name == "GetLastReport") return new GetLastReportActionInvokable(parameterTokens);
        throw new NotImplementedException();
    }
    public IEnumerable<ServiceAction> GetServiceActions(DataServiceOperationContext operationContext) {
        return GetActions(operationContext);
    }
    public IEnumerable<ServiceAction> GetServiceActionsByBindingParameterType(DataServiceOperationContext operationContext, ResourceType bindingParameterType) {
        var actions = GetActions(operationContext);
        return actions.Where(x => x.BindingParameter != null)
                      .Where(x => x.BindingParameter.ParameterType == bindingParameterType);
    }
    public bool TryResolveServiceAction(DataServiceOperationContext operationContext, string serviceActionName, out ServiceAction serviceAction) {
        var actions = GetActions(operationContext);
        serviceAction = actions.FirstOrDefault(x => x.Name.Equals(serviceActionName));
        return serviceAction != null;
    }
    private List<ServiceAction> GetActions(DataServiceOperationContext context) {
        var result = new List<ServiceAction>();
        // types from metadata
        var metadataprovider = context.GetService(typeof(IDataServiceMetadataProvider)) as IDataServiceMetadataProvider;
        var surveyType = metadataprovider.Types.SingleOrDefault(s => s.Name == "Survey");
        var reportType = metadataprovider.Types.SingleOrDefault(s => s.Name == "Report");
        var reportResourceSet = new ResourceSet("Reports", reportType);
        // service action
        var getLastReportAction = new ServiceAction(
            "GetLastReport",    // name of the action
            reportType,         // return type
            reportResourceSet,  // resource set of the return type
            OperationParameterBindingKind.Always,   // binding
            new[] {             // parameters, with first parameter the binding parameter
                new ServiceActionParameter("survey", surveyType)
            }
        );
        getLastReportAction.SetReadOnly();
        result.Add(getLastReportAction);
        return result;
    }
}
public class GetLastReportActionInvokable : IDataServiceInvokable {
    object[] _parameterTokens;
    Entities.Report result;
    public GetLastReportActionInvokable(object[] parameterTokens) {
        _parameterTokens = parameterTokens;
    }
    public object GetResult() {
        return result;
    }
    public void Invoke() {
        try {
            var parameter1 = _parameterTokens[0];
            var survey = parameter1 as Entities.Survey;
            if (parameter1 is EnumerableQuery) {
                survey = (parameter1 as IEnumerable<Entities.Survey>).First();
            }
            if (survey != null && survey.Reports != null) {
                result = survey.Reports.OrderByDescending(r => r.Created).FirstOrDefault();
            }
        } catch {
            throw new DataServiceException(500, "Exception executing action GetLastReport");
        }
    }
}
public class SurveyProvider : IUpdatable, IDataServiceUpdateProvider2 {
    // IQueryable properties here ...

    // other IUpdatable left at "throw new NotImplementedException()"
    // this makes the provider readonly as changes are not persisted
    public void SaveChanges() { }

    // other IDataServiceUpdateProvider2 left at 
    //       "throw new NotImplementedException()"
    public void ScheduleInvokable(IDataServiceInvokable invokable) {
        invokable.Invoke();
    }
}

Conclusion

Projections, batching and format are nice features out-of-the-box. Actions take a lot of implementation when you use your own DataProvider, but can be very powerful.
This concludes my exploration of OData. I think of it as a database service based on open standards for compatibility with some limitations and some extra’s. Choose this when you want users to explore your data from any platform, but leave it when you cannot work around the limited query language.

References

Using actions to implement server-side behavior on MSDN
How IDataServiceActionProvider works post on MSDN blog
ActionProvider example on codeplex

About erictummers

Working in a DevOps team is the best thing that happened to me. I like challenges and sharing the solutions with others. On my blog I’ll mostly post about my work, but expect an occasional home project, productivity tip and tooling review.
This entry was posted in Development and tagged , , , . Bookmark the permalink.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.