locked
WCF + EF: Customizing metadata per-request. RRS feed

  • Question

  • I'm working on a project using WCF data services with the Entity Framework provider, and for this project the service MUST implement column level security for a multi-user system - that is, on a per-request basis, I need to be able to either allow/deny queries based on the user's permissions. Basically, what I REALLY want is column-level EntitySet access rights, in addition to the existing table-level rights system. These rights would simply be verified for each request prior to forwarding the parsed request to the provider. If the client referenced a field he or she did not have access to for read, it would return an error. If the client asked for a resource without explicitly specifying a $select option, it would simply not include the excluded fields in the result (or maybe it would be more correct to just require a $select with only ok fields in this case?). 

    Since neither WCF Data Services nor Entity Framework have native support for this functionality, I have attempted to create a workaround using reflection. Basically, I create an IDataServiceMetadataProvider implementation that wraps the internal built-in provider for metadata generation (in this case ObjectContextServiceProvider) and filters the metadata by excluding certain properties. 

            void ProcessingPipeline_ProcessingRequest(object sender, DataServiceProcessingPipelineEventArgs e)
            {
                var dataServiceTType = this.GetType().BaseType;
                // get the existing provider wrapper - we need the existing services in order to wrap them
                var existingProviderField = dataServiceTType.GetField("provider", BindingFlags.Instance | BindingFlags.NonPublic);
                var existingProvider = existingProviderField.GetValue(this);
                var providerFieldType = existingProvider.GetType();
                var existingMetadataProvider = providerFieldType.GetField("metadataProvider", BindingFlags.Instance | BindingFlags.NonPublic)
                    .GetValue(existingProvider);
                var existingCacheItem = providerFieldType.GetField("metadata", BindingFlags.Instance | BindingFlags.NonPublic)
                    .GetValue(existingProvider);
                var existingQueryProvider = providerFieldType.GetField("queryProvider", BindingFlags.Instance | BindingFlags.NonPublic)
                    .GetValue(existingProvider);
                var existingDataService = providerFieldType.GetField("dataService", BindingFlags.Instance | BindingFlags.NonPublic)
                    .GetValue(existingProvider);
    
                // this metadata provider wraps the built-in EF provider for metadata calls
                // and filters metadata on a per-request basis
                var wrappedMetadataProvider = new CustomMetadataProviderInstance(existingMetadataProvider as IDataServiceMetadataProvider);
    
                // create a new MetaDataCacheItem. Otherwise it will use the one it already generated
                var configuration = dataServiceTType.GetMethod("CreateConfiguration", BindingFlags.Static | BindingFlags.NonPublic)
                    .Invoke(this, new object[] { this.GetType(), existingMetadataProvider });
                var cacheItemConstructor = dataServiceTType.Assembly
                    .GetType("System.Data.Services.Caching.DataServiceCacheItem")
                    .GetConstructors(BindingFlags.Instance | BindingFlags.NonPublic)[0];
                var newCacheItem = cacheItemConstructor.Invoke(
                    new object[] { 
                        existingCacheItem.GetType()
                        .GetProperty("Configuration", BindingFlags.Instance | BindingFlags.NonPublic)
                        .GetValue(existingCacheItem) 
                    });
    
                // create a new provider with the wrapped metadata provider
                var providerConstructor = providerFieldType.GetConstructors(BindingFlags.NonPublic | BindingFlags.Instance)[0];
                var newProvider = providerConstructor.Invoke(
                    new object[] { newCacheItem, wrappedMetadataProvider, existingQueryProvider, existingDataService });
    
                // I'm guessing this is important
                providerFieldType.GetMethod("PopulateMetadataCacheItemForBuiltInProvider", BindingFlags.Instance | BindingFlags.NonPublic)
                    .Invoke(newProvider, null);
    
                // set our provider wrapper to the new one we just created.
                existingProviderField.SetValue(this, newProvider);
            }

    The workaround actually works ok for the built-in linq to objects provider, but the problem with the entity framework provider seems to be that, even if internally the provider is the ObjectContextServiceProvider, if I change the metadata provider to my custom wrapper using reflection, it will still generate query expressions meant for Linq-to-Objects - not for entity framework. This causes errors such as this one (which happens when I try to do a $select):

    Unable to cast the type 'System.String' to type 'System.Object'. LINQ to Entities only supports casting EDM primitive or enumeration types.

    This appears to be the same kind of errors one gets when you attempt to create a custom provider that wraps Entity Framework - basically WCF is generating Linq-to-Object expressions (because I assume it's checking the type of the metadata provider and only generating EF Expressions when the metadata provider is of type ObjectContextServiceProvider?

    Is there any possible way to get this workaround working for the entity framework provider? I need to get WCF to continue building queries for entity framework, and not linq-to-objects, and yet still customize the metadata on a per-request basis.

    Saturday, November 24, 2012 5:22 AM

Answers

  • Hi,

    There's no simple way to do this. In fact even the code you already have (using private reflection) is not going to be supported in any way (and might break with new versions of the runtime assemblies).

    The general problem of wrapping Reflection (or Custom) provider around EF one is well known. There's no way currently to tell WCF DS that the provider you have is actually EF even though it doesn't look like one. What you can do though is "fixup" the expression tree for the query so that it works with EF. Basically you intercept the calls to get the IQueryable (which you very likely already do) and return your own IQueryable implementation which is a wrapper around the EF one. In that wrapper just before execution you walk the expression tree and fix it up.

    The particular problem you're hitting is because of different behavior between LINQ to Objects and LINQ to EF. L2O requires a specific cast expression to System.Object is an expression of non-object type (string) is assigned to a property of object type. L2E on the other hand doesn't like that and fails (like you've seen). In this case the fix is pretty simple, just remove the explicit cast to System.Object from the expression tree. I remember that there are other small differences though - especially around $select and $expand.

    Thanks,


    Vitek Karas [MSFT]

    • Marked as answer by JeroMiya Monday, December 3, 2012 3:40 PM
    Wednesday, November 28, 2012 7:46 AM
    Moderator

All replies

  • Hi JeroMiya,

    Welcome to the MSDN forum.

    I am trying to involve another expert in your thread. Please wait for the response. Sorry for any inconvenience.

    Have a nice day.


    Alexander Sun [MSFT]
    MSDN Community Support | Feedback to us

    Wednesday, November 28, 2012 1:14 AM
  • Hi,

    There's no simple way to do this. In fact even the code you already have (using private reflection) is not going to be supported in any way (and might break with new versions of the runtime assemblies).

    The general problem of wrapping Reflection (or Custom) provider around EF one is well known. There's no way currently to tell WCF DS that the provider you have is actually EF even though it doesn't look like one. What you can do though is "fixup" the expression tree for the query so that it works with EF. Basically you intercept the calls to get the IQueryable (which you very likely already do) and return your own IQueryable implementation which is a wrapper around the EF one. In that wrapper just before execution you walk the expression tree and fix it up.

    The particular problem you're hitting is because of different behavior between LINQ to Objects and LINQ to EF. L2O requires a specific cast expression to System.Object is an expression of non-object type (string) is assigned to a property of object type. L2E on the other hand doesn't like that and fails (like you've seen). In this case the fix is pretty simple, just remove the explicit cast to System.Object from the expression tree. I remember that there are other small differences though - especially around $select and $expand.

    Thanks,


    Vitek Karas [MSFT]

    • Marked as answer by JeroMiya Monday, December 3, 2012 3:40 PM
    Wednesday, November 28, 2012 7:46 AM
    Moderator