Search code examples
c#inheritancewcf-data-servicesodata

Inheritance is not supported, even with a custom OData Service Provider?


EDIT: I went ahead and created a little project to demonstrate this issue (called RestfulTimesTest), available on SkyDrive.

I built a custom OData Service Provider to provide queries and updates into a custom model, based on Alex James' excellent blog post on Creating a Data Service Provider.

Consider the following 3 CLR classes: ResidentialCustomer, Customer, and User. ResidentialCustomer extends Customer, Customer has a list of Users, and User has a reference back to Customer.

The issue I'm having is that the metadata can include ResidentialCustomer or the association between Customer and User, but not both. If I include both, I get the following error when trying to display or access the metadata via the DataService:

{System.NullReferenceException: Object reference not set to an instance of an object.
at System.Data.Services.Providers.DataServiceProviderWrapper.GetResourceAssociationSet(ResourceSetWrapper resourceSet, ResourceType resourceType, ResourceProperty resourceProperty)
at System.Data.Services.Serializers.MetadataSerializer.MetadataManager.GetAndValidateResourceAssociationSet(ResourceSetWrapper resourceSet, ResourceType resourceType, ResourceProperty navigationProperty)
at System.Data.Services.Serializers.MetadataSerializer.MetadataManager.PopulateAssociationsForSetAndType(ResourceSetWrapper resourceSet, ResourceType resourceType)
at System.Data.Services.Serializers.MetadataSerializer.MetadataManager.PopulateAssociationsForSet(ResourceSetWrapper resourceSet)
at System.Data.Services.Serializers.MetadataSerializer.MetadataManager..ctor(DataServiceProviderWrapper provider, IDataService service)
at System.Data.Services.Serializers.MetadataSerializer.GenerateMetadata(MetadataEdmSchemaVersion metadataEdmSchemaVersion, IDataService service)
at System.Data.Services.Providers.DataServiceProviderWrapper.WriteMetadataDocument(MetadataSerializer serializer, XmlWriter writer, IDataService service)
at System.Data.Services.Serializers.MetadataSerializer.WriteRequest(IDataService service)
at System.Data.Services.ResponseBodyWriter.Write(Stream stream)}

The issue is happening when the GetResourceAssociationSet method in the class that implements IDataServiceMetadataProvider (see more details below) is called with a ResourceSet of a different type (ResidentialCustomer) than the passed in ResourceType (Customer):

public ResourceAssociationSet GetResourceAssociationSet(ResourceSet resourceSet, ResourceType resourceType, ResourceProperty resourceProperty) 
{ 
    return resourceProperty.CustomState as ResourceAssociationSet; 
} 

This causes a failure in the .net library class method ResourceAssociationSet.GetResourceAssociationSetEnd, where it fails to find a ResourceAssociationSetEnd, subsequently causing the null reference exception.

The class that implements IServiceProvider (see more details below) sets up the metadata. It sets up the association between Customer and User as follows:

        ResourceAssociationSet customerUserListSet = new ResourceAssociationSet(
            "CustomerUserList",
            new ResourceAssociationSetEnd(
                customerSet,
                customer,
                customerUserList
            ),
            new ResourceAssociationSetEnd(
                userSet,
                user,
                userCustomer
            )
        );
        customerUserList.CustomState = customerUserListSet;
        userCustomer.CustomState = customerUserListSet;
        metadata.AddAssociationSet(customerUserListSet);

A ResidentialCustomer should have access to its list of Users just like a Customer. An object that inherits another should be able to use the base associations. I do not believe the solution is to add another association between ResidentialCustomer and User, and trying to do so leads to property conflicts or undefined properties. What piece am I missing for setting up associations between objects that are inherited by other objects?

Additional Details: The associated classes for the custom provider are as follows:

An interface for DataContext classes such as:

public interface IODataContext
{
    IQueryable GetQueryable(ResourceSet set); 
    object CreateResource(ResourceType resourceType);
    void AddResource(ResourceType resourceType, object resource);
    void DeleteResource(object resource);
    void SaveChanges();
}

A class to implement IDataServiceMetadataProvider such as:

public class ODataServiceMetadataProvider : IDataServiceMetadataProvider
{
    private Dictionary<string, ResourceType> resourceTypes = new Dictionary<string, ResourceType>();
    private Dictionary<string, ResourceSet> resourceSets = new Dictionary<string, ResourceSet>();
    private List<ResourceAssociationSet> _associationSets = new List<ResourceAssociationSet>(); 

    public string ContainerName
    {
        get { return "MyDataContext"; }
    }

     public string ContainerNamespace
     {
         get { return "MyNamespace"; }
     }

    public IEnumerable<ResourceSet> ResourceSets
    {
         get { return this.resourceSets.Values; }
    }

    public IEnumerable<ServiceOperation> ServiceOperations
    {
        get { yield break; }
    }

    public IEnumerable<ResourceType> Types
    {
        get { return this.resourceTypes.Values; }
    }

    public bool TryResolveResourceSet(string name, out ResourceSet resourceSet)
    {
        return resourceSets.TryGetValue(name, out resourceSet);
    }

    public bool TryResolveResourceType(string name, out ResourceType resourceType)
    {
        return resourceTypes.TryGetValue(name, out resourceType);
    }

    public bool TryResolveServiceOperation(string name, out ServiceOperation serviceOperation)
    {
        serviceOperation = null;
        return false;
    }

    public void AddResourceType(ResourceType type)
    {
        type.SetReadOnly();
        resourceTypes.Add(type.FullName, type);
    }

    public void AddResourceSet(ResourceSet set)
    {
        set.SetReadOnly();
        resourceSets.Add(set.Name, set);
    }

    public bool HasDerivedTypes(ResourceType resourceType)
    {
        if (resourceType.InstanceType == typeof(ResidentialCustomer))
        {
            return true;
        }
        return false;
    }

    public IEnumerable<ResourceType> GetDerivedTypes(ResourceType resourceType)
    {
        List<ResourceType> derivedResourceTypes = new List<ResourceType>();
        if (resourceType.InstanceType == typeof(ResidentialCustomer))
        {
            foreach (ResourceType resource in Types)
            {
                if (resource.InstanceType == typeof(Customer))
                {
                    derivedResourceTypes.Add(resource);
                }
            }
        }
        return derivedResourceTypes;
    }

    public void AddAssociationSet(ResourceAssociationSet associationSet) 
    {
        _associationSets.Add(associationSet); 
    }

    public ResourceAssociationSet GetResourceAssociationSet(ResourceSet resourceSet, ResourceType resourceType, ResourceProperty resourceProperty)
    {
        return resourceProperty.CustomState as ResourceAssociationSet;
    }

    public ODataServiceMetadataProvider() { }
}

A class to implement IDataServiceQueryProvider such as:

public class ODataServiceQueryProvider<T> : IDataServiceQueryProvider where T : IODataContext
{
    T _currentDataSource;
    IDataServiceMetadataProvider _metadata;

    public object CurrentDataSource
    {
        get
        {
            return _currentDataSource;
        }
        set
        {
            _currentDataSource = (T)value;
        }
    }

    public bool IsNullPropagationRequired
    {
        get { return true; }
    }

    public object GetOpenPropertyValue(object target, string propertyName)
    {
        throw new NotImplementedException();
    }

    public IEnumerable<KeyValuePair<string, object>> GetOpenPropertyValues(object target)
    {
        throw new NotImplementedException();
    }

    public object GetPropertyValue(object target, ResourceProperty resourceProperty)
    {
        throw new NotImplementedException();
    }

    public IQueryable GetQueryRootForResourceSet(ResourceSet resourceSet)
    {
        return _currentDataSource.GetQueryable(resourceSet);
    }

    public ResourceType GetResourceType(object target)
    {
        Type type = target.GetType();
        return _metadata.Types.Single(t => t.InstanceType == type);
    }

    public object InvokeServiceOperation(ServiceOperation serviceOperation, object[] parameters)
    {
        throw new NotImplementedException();
    }

    public ODataServiceQueryProvider(IDataServiceMetadataProvider metadata)
    {
        _metadata = metadata;
    }
}

A class to implement IDataServiceUpdateProvider such as:

public class ODataServiceUpdateProvider<T> : IDataServiceUpdateProvider where T : IODataContext
{
    private IDataServiceMetadataProvider _metadata;
    private ODataServiceQueryProvider<T> _query;
    private List<Action> _actions;

    public T GetContext()
    {
        return ((T)_query.CurrentDataSource);
    }

    public void SetConcurrencyValues(object resourceCookie, bool? checkForEquality, IEnumerable<KeyValuePair<string, object>> concurrencyValues)
    {
        throw new NotImplementedException();
    }

    public void SetReference(object targetResource, string propertyName, object propertyValue)
    {
        _actions.Add(() => ReallySetReference(targetResource, propertyName, propertyValue));
    }

    public void ReallySetReference(object targetResource, string propertyName, object propertyValue)
    {
        targetResource.SetPropertyValue(propertyName, propertyValue);
    }

    public void AddReferenceToCollection(object targetResource, string propertyName, object resourceToBeAdded)
    {
        _actions.Add(() => ReallyAddReferenceToCollection(targetResource, propertyName, resourceToBeAdded));
    }

    public void ReallyAddReferenceToCollection(object targetResource, string propertyName, object resourceToBeAdded)
    {
        var collection = targetResource.GetPropertyValue(propertyName);
        if (collection is IList)
        {
            (collection as IList).Add(resourceToBeAdded);
        }
    }

    public void RemoveReferenceFromCollection(object targetResource, string propertyName, object resourceToBeRemoved)
    {
        _actions.Add(() => ReallyRemoveReferenceFromCollection(targetResource, propertyName, resourceToBeRemoved));
    }

    public void ReallyRemoveReferenceFromCollection(object targetResource, string propertyName, object resourceToBeRemoved)
    {
        var collection = targetResource.GetPropertyValue(propertyName);
        if (collection is IList)
        {
            (collection as IList).Remove(resourceToBeRemoved);
        }
    }

    public void ClearChanges()
    {
        _actions.Clear();
    }

    public void SaveChanges()
    {
        foreach (var a in _actions)
            a();
        GetContext().SaveChanges();
    }

    public object CreateResource(string containerName, string fullTypeName)
    {
        ResourceType type = null;
        if (_metadata.TryResolveResourceType(fullTypeName, out type))
        {
            var context = GetContext();
            var resource = context.CreateResource(type);
            _actions.Add(() => context.AddResource(type, resource));
            return resource;
        }
        throw new Exception(string.Format("Type {0} not found", fullTypeName));
    }

    public void DeleteResource(object targetResource)
    {
        _actions.Add(() => GetContext().DeleteResource(targetResource));
    }

    public object GetResource(IQueryable query, string fullTypeName)
    {
        var enumerator = query.GetEnumerator();
        if (!enumerator.MoveNext())
            throw new Exception("Resource not found");
        var resource = enumerator.Current;
        if (enumerator.MoveNext())
            throw new Exception("Resource not uniquely identified");

        if (fullTypeName != null)
        {
            ResourceType type = null;
            if (!_metadata.TryResolveResourceType(fullTypeName, out type))
                throw new Exception("ResourceType not found");
            if (!type.InstanceType.IsAssignableFrom(resource.GetType()))
                throw new Exception("Unexpected resource type");
        }
        return resource;
   }

    public object ResetResource(object resource)
    {
        _actions.Add(() => ReallyResetResource(resource));
        return resource;
    }

    public void ReallyResetResource(object resource)
    {
        var clrType = resource.GetType();
        ResourceType resourceType = _metadata.Types.Single(t => t.InstanceType == clrType);
        var resetTemplate = GetContext().CreateResource(resourceType);

        foreach (var prop in resourceType.Properties
                 .Where(p => (p.Kind & ResourcePropertyKind.Key) != ResourcePropertyKind.Key))
        {
            var clrProp = clrType.GetProperties().Single(p => p.Name == prop.Name);
            var defaultPropValue = clrProp.GetGetMethod().Invoke(resetTemplate, new object[] { });
            clrProp.GetSetMethod().Invoke(resource, new object[] { defaultPropValue });
        }
    }

    public object ResolveResource(object resource)
    {
        return resource;
    }

    public object GetValue(object targetResource, string propertyName)
    {
        var value = targetResource.GetType().GetProperties().Single(p => p.Name == propertyName).GetGetMethod().Invoke(targetResource, new object[] { });
        return value;
    }

    public void SetValue(object targetResource, string propertyName, object propertyValue)
    {
        targetResource.GetType().GetProperties().Single(p => p.Name == propertyName).GetSetMethod().Invoke(targetResource, new[] { propertyValue });
     }

     public ODataServiceUpdateProvider(IDataServiceMetadataProvider metadata, ODataServiceQueryProvider<T> query)
     {
         _metadata = metadata;
         _query = query;
         _actions = new List<Action>();
    }
}

A class to implement IServiceProvider such as:

public class ODataService<T> : DataService<T>, IServiceProvider where T : IODataContext
{
    private ODataServiceMetadataProvider _metadata;
    private ODataServiceQueryProvider<T> _query;
    private ODataServiceUpdateProvider<T> _updater;

    public object GetService(Type serviceType)
    {
        if (serviceType == typeof(IDataServiceMetadataProvider))
        {
            return _metadata;
        }
        else if (serviceType == typeof(IDataServiceQueryProvider))
        {
            return _query;
        }
        else if (serviceType == typeof(IDataServiceUpdateProvider))
        {
            return _updater;
        }
        else
        {
            return null;
        }
    }

    public ODataServiceMetadataProvider GetMetadataProvider(Type dataSourceType)
    {
        ODataServiceMetadataProvider metadata = new ODataServiceMetadataProvider();
        ResourceType customer = new ResourceType(
            typeof(Customer),
            ResourceTypeKind.EntityType,
            null,
            "MyNamespace",
            "Customer",
            false
        );
        ResourceProperty customerCustomerID = new ResourceProperty(
            "CustomerID",
            ResourcePropertyKind.Key |
            ResourcePropertyKind.Primitive,
            ResourceType.GetPrimitiveResourceType(typeof(Guid))
        );
        customer.AddProperty(customerCustomerID);
        ResourceProperty customerCustomerName = new ResourceProperty(
            "CustomerName",
            ResourcePropertyKind.Primitive,
            ResourceType.GetPrimitiveResourceType(typeof(string))
        );
        customer.AddProperty(customerCustomerName);
        ResourceType residentialCustomer = new ResourceType(
            typeof(ResidentialCustomer),
            ResourceTypeKind.EntityType,
            customer,
            "MyNamespace",
            "ResidentialCustomer",
            false
        );
        ResourceType user = new ResourceType(
            typeof(User),
            ResourceTypeKind.EntityType,
            null,
            "MyNamespace",
            "User",
            false
        );
        ResourceProperty userUserID = new ResourceProperty(
            "UserID",
            ResourcePropertyKind.Key |
            ResourcePropertyKind.Primitive,
            ResourceType.GetPrimitiveResourceType(typeof(Guid))
        );
        user.AddProperty(userUserID);
        ResourceProperty userCustomerID = new ResourceProperty(
            "CustomerID",
            ResourcePropertyKind.Primitive,
            ResourceType.GetPrimitiveResourceType(typeof(Guid))
        );
        user.AddProperty(userCustomerID);
        ResourceProperty userEmailAddress = new ResourceProperty(
            "EmailAddress",
            ResourcePropertyKind.Primitive,
            ResourceType.GetPrimitiveResourceType(typeof(string))
        );
        user.AddProperty(userEmailAddress);

        var customerSet = new ResourceSet("Customers", customer);
        var residentialCustomerSet = new ResourceSet("ResidentialCustomers", residentialCustomer);
        var userSet = new ResourceSet("Users", user);

        var userCustomer = new ResourceProperty(
            "Customer",
            ResourcePropertyKind.ResourceReference,
            customer
        );
        user.AddProperty(userCustomer);

        var customerUserList = new ResourceProperty(
            "UserList",
            ResourcePropertyKind.ResourceSetReference,
            user
        );
        customer.AddProperty(customerUserList);

        metadata.AddResourceType(customer);
        metadata.AddResourceSet(customerSet);
        metadata.AddResourceType(residentialCustomer);
        metadata.AddResourceSet(residentialCustomerSet);
        metadata.AddResourceType(user);
        metadata.AddResourceSet(userSet);

        ResourceAssociationSet customerUserListSet = new ResourceAssociationSet(
            "CustomerUserList",
            new ResourceAssociationSetEnd(
                customerSet,
                customer,
                customerUserList
            ),
            new ResourceAssociationSetEnd(
                userSet,
                user,
                userCustomer
            )
        );
        customerUserList.CustomState = customerUserListSet;
        userCustomer.CustomState = customerUserListSet;
        metadata.AddAssociationSet(customerUserListSet);

        return metadata;
    }

    public ODataServiceQueryProvider<T> GetQueryProvider(ODataServiceMetadataProvider metadata)
    {
        return new ODataServiceQueryProvider<T>(metadata);
    }

    public ODataServiceUpdateProvider<T> GetUpdateProvider(ODataServiceMetadataProvider metadata, ODataServiceQueryProvider<T> query)
    {
        return new ODataServiceUpdateProvider<T>(metadata, query);
    }

    public ODataService()
    {
        _metadata = GetMetadataProvider(typeof(T));
        _query = GetQueryProvider(_metadata);
        _updater = GetUpdateProvider(_metadata, _query);
    }
}

The DataContext class holds the CLR collections and wires up the service operations such as:

public partial class MyDataContext: IODataContext
{
    private List<Customer> _customers = null;
    public List<Customer> Customers
    {
        get
        {
            if (_customers == null)
            {
                _customers = DataManager.GetCustomers);
            }
            return _customers;
        }
    }

    private List<ResidentialCustomer> _residentialCustomers = null;
    public List<ResidentialCustomer> ResidentialCustomers
    {
        get
        {
            if (_residentialCustomers == null)
            {
                _residentialCustomers = DataManager.GetResidentialCustomers();
            }
            return _residentialCustomers;
        }
    }

    private List<User> _users = null;
    public List<User> Users
    {
        get
        {
            if (_users == null)
            {
                _users = DataManager.GetUsers();
            }
            return _users;
        }
    }

    public IQueryable GetQueryable(ResourceSet set)
    {
        if (set.Name == "Customers") return Customers.AsQueryable();
        if (set.Name == "ResidentialCustomers") return ResidentialCustomers.AsQueryable();
        if (set.Name == "Users") return Users.AsQueryable();
        throw new NotSupportedException(string.Format("{0} not found", set.Name));
    }

    public object CreateResource(ResourceType resourceType)
    {
        if (resourceType.InstanceType == typeof(Customer))
        {
            return new Customer();
        }
        if (resourceType.InstanceType == typeof(ResidentialCustomer))
        {
            return new ResidentialCustomer();
        }
        if (resourceType.InstanceType == typeof(User))
        {
            return new User();
        }
        throw new NotSupportedException(string.Format("{0} not found for creating.", resourceType.FullName));
    }

    public void AddResource(ResourceType resourceType, object resource)
    {
        if (resourceType.InstanceType == typeof(Customer))
        {
            Customer i = resource as Customer;
            if (i != null)
            {
                Customers.Add(i);
                return;
            }
        }
        if (resourceType.InstanceType == typeof(ResidentialCustomer))
        {
            ResidentialCustomeri = resource as ResidentialCustomer;
            if (i != null)
            {
                ResidentialCustomers.Add(i);
                return;
            }
        }
        if (resourceType.InstanceType == typeof(User))
        {
            Useri = resource as User;
            if (i != null)
            {
                Users.Add(i);
                return;
            }
        }
        throw new NotSupportedException(string.Format("{0} not found for adding.", resourceType.FullName));
    }

    public void DeleteResource(object resource)
    {
        if (resource.GetType() == typeof(Customer))
        {
            Customers.Remove(resource as Customer);
            return;
        }
        if (resource.GetType() == typeof(ResidentialCustomer))
        {
            ResidentialCustomers.Remove(resource as ResidentialCustomer);
            return;
        }
        if (resource.GetType() == typeof(User))
        {
            Users.Remove(resource as User);
            return;
        }
        throw new NotSupportedException(string.Format("{0} not found for deletion.", resource.GetType().FullName));
    }

    public void SaveChanges()
    {
        foreach (var item in Customers.Where(i => i.IsModified == true))
            item.Save();
        foreach (var item in ResidentialCustomers.Where(i => i.IsModified == true))
            item.Save();
        foreach (var item in Users.Where(i => i.IsModified == true))
            item.Save();
    }
}

The data service uses the custom data service class and the data context, such as:

public class MyDataService : ODataService<MyDataContext>
{
    public static void InitializeService(DataServiceConfiguration config)
    {
        config.SetEntitySetAccessRule("Customers", EntitySetRights.All);
        config.SetEntitySetAccessRule("ResidentialCustomers", EntitySetRights.All);
        config.SetEntitySetAccessRule("Users", EntitySetRights.All);
        config.DataServiceBehavior.MaxProtocolVersion = DataServiceProtocolVersion.V2;
        config.DataServiceBehavior.AcceptProjectionRequests = true; 
    }
}

Solution

  • Upon further testing, it seems like there are currently two severe constraints on the metadata in order to expose it with a custom (or any?) OData Service Provider:

    1) A ResourceType cannot extend another ResourceType (the BaseType for a ResourceType must be null). To some extent, this constraint can be alleviated by replicating all properties of the base type into the extended type in the metadata.

    2) A ResourceType or ResourceSet cannot be involved in an association, if its InstanceType extends another InstanceType referred to in the metadata, regardless of how the ResourceTypes for each instance type are set up. In other words in the example, if ResidentialCustomer extends Customer in the CLR model, I can't use a ResourceType or ResourceSet with an InstanceType of ResidentialCustomer in any association. I don't know of any workaround to this, other than not providing these necessary associations via the OData Service Provider.

    If I violate any of the above 2 constraints, I get a null reference exception in the .net library class method ResourceAssociationSet.GetResourceAssociationSetEnd().

    Are these constraints correct, or are there any examples to workaround these constraints or properly set up the metadata for these scenarios?