4

I'm currently executing Web API with oData filter requests as follows:

public IQueryable<OrganizationViewModel> Get(ODataQueryOptions<Organization> oDataQuery)
{
    var query = new FindOrganizationsQuery(oDataQuery);
    var result =_findOrganizationsQueryHandler.Execute(query);
    return result.Organizations.Select(o => new OrganizationViewModel { Id = o.PublicId, Name = o.Name });
}

The handler looks like:

public FindOrganizationsQueryResult Execute(FindOrganizationsQuery request)
{
    var organizations = request.ODataQuery.ApplyTo(_mgpQueryContext.Organizations).Cast<Organization>();            
    return new FindOrganizationsQueryResult(organizations);
}

And the query class looks like:

public class FindOrganizationsQuery
{
    public FindOrganizationsQuery(ODataQueryOptions<Organization> oDataQuery)
    {
        ODataQuery = oDataQuery;
    }
    public ODataQueryOptions<Organization> ODataQuery { get; set; }
}

So if I pass an oData filter with the request, it is handled nicely and this all works fine.

But now, instead of passing the type ODataQueryOptions to the Get operation, I would like to have the FindOrganizationsQuery class, like:

public IQueryable<OrganizationViewModel> FindOrganizations(FindOrganizationsQuery query)
{
    // query is null
}

However, the query parameters is always null. How can I pass the oData filter if the ODataQueryOptions parameters is in another class?

abatishchev
  • 98,240
  • 88
  • 296
  • 433
L-Four
  • 13,345
  • 9
  • 65
  • 109
  • What is `FindOrganizationsQuery` and why do you need it? Why the inner `ODataQueryOptions` whilst `FindOrganizations` returns an `IQueryable` – Aron Aug 15 '13 at 10:08
  • I use CQS, so I have queries and commands in my application layer that needs to be executed from within the web api, which is in my service layer. I return a view model instead of the actual organization using projection because I want to decide what properties to expose (for example, I don't expose navigation properties). But this all works. – L-Four Aug 15 '13 at 10:13
  • Okay...but why should downstream specifying queries on `Organization` and receive an `OrganizationViewModel`? You would still be exposing the navigation properties like this... – Aron Aug 15 '13 at 10:17
  • Not at all. The view model is not the same as the actual data entities. – L-Four Aug 15 '13 at 10:40
  • I guess the queries will only work, if the used property names are equal in both Organization and OrganizationViewModel? – niklr Oct 29 '14 at 11:23
  • The querying is done on the actual data entity (ODataQueryOptions), the result is a custom view model mapped from that data entity (request.ODataQuery.ApplyTo(_mgpQueryContext.Patients).Cast();). – L-Four Oct 29 '14 at 15:51

1 Answers1

0

You can write a custom parameter binding attribute for FindOrganizationsQuery the same way we do for ODataQueryOptions and then attribute your FindOrganizationsQuery with that attribute.

Some sample code below,

public class CustomQueryBindingAttribute : ParameterBindingAttribute
{
    public override HttpParameterBinding GetBinding(HttpParameterDescriptor parameter)
    {
        return new CustomQueryBinding(parameter);
    }

    internal class CustomQueryBinding : HttpParameterBinding
    {
        public CustomQueryBinding(HttpParameterDescriptor parameter)
            : base(parameter)
        {
        }

    internal class CustomQueryBinding : HttpParameterBinding
    {
        public CustomQueryBinding(HttpParameterDescriptor parameter)
            : base(parameter)
        {
        }

        public override Task ExecuteBindingAsync(ModelMetadataProvider metadataProvider,
            HttpActionContext actionContext, CancellationToken cancellationToken)
        {
            IEdmModel model = actionContext.Request.GetEdmModel() ?? actionContext.ActionDescriptor.GetEdmModel(typeof(Organization));
            ODataQueryContext queryContext = new ODataQueryContext(model, typeof(Organization));

            object customQuery = CreateCustomQuery(queryContext, actionContext.Request);
            SetValue(actionContext, customQuery);

            return Task.FromResult(0);
        }

        private object CreateCustomQuery(ODataQueryContext queryContext, HttpRequestMessage request)
        {
            Type parameterType = Descriptor.ParameterType;
            // Assuming all custom queries have this public property.
            Type oDataQueryOptionsOfTType = parameterType.GetProperty("ODataQuery").PropertyType;

            object odataQueryOptions = Activator.CreateInstance(oDataQueryOptionsOfTType, queryContext, request);
            return Activator.CreateInstance(parameterType, odataQueryOptions);
        }
    }
}

and the extension method I copied from web API source code as it is not public.

public static class HttpActionDescriptorExtensions
{
    internal const string EdmModelKey = "MS_EdmModel";

    internal static IEdmModel GetEdmModel(this HttpActionDescriptor actionDescriptor, Type entityClrType)
    {
        // save the EdmModel to the action descriptor
        return actionDescriptor.Properties.GetOrAdd(EdmModelKey + entityClrType.FullName, _ =>
        {
            ODataConventionModelBuilder builder = new ODataConventionModelBuilder(actionDescriptor.Configuration, isQueryCompositionMode: true);
            EntityTypeConfiguration entityTypeConfiguration = builder.AddEntity(entityClrType);
            builder.AddEntitySet(entityClrType.Name, entityTypeConfiguration);
            IEdmModel edmModel = builder.GetEdmModel();
            return edmModel;
        }) as IEdmModel;
    }
}

I have the complete sample here.

RaghuRam Nadiminti
  • 6,718
  • 1
  • 33
  • 30
  • Thank you. FindOrganizationsQuery is used in the CustomQueryBinding, so how can this be made generic so that it works with any type of class, not only Organization/FindOrganizationsQuery ? – L-Four Aug 17 '13 at 09:12
  • The parameter type is available in the binding. You need to figure out the type of `T` of the ODataQueryOptions that you expect in the ctor for the custom query. I have updated the sample code assuming that all your custom queries have a public property with name ODataQuery. Also, you need to place this attribute on all your custom queries. You could also place this attribute on the base class as well and it gets inherited. – RaghuRam Nadiminti Aug 19 '13 at 18:06