1

Sorry I know there is a similar question that was asked here Wrapping a Web API response in JSON but still having it work with IQueryable and oData but I'd like to ask if it's possible to do the similar without having it to rewrap the result?

I have a big number of existing ASP.NET Core (3.1) WebAPI endpoints that are returning a response type which, beside the data I'd like to apply the OData filters to, always includes additional base properties for errors and a completion indicator that are all serialized to json.

I'd like to have those WebAPI endpoints become OData as well, or in case no filter is appended as part of the querystring just return the result as it was/is...i.e. if ?$filter=value eq 'Y' is appended to an existing WebAPI endpoint URL it should apply that OData filter just to the data contained within the json response (thus not having to change the OData filter to contain the adjusted path for data or if say $top=N is applied it would return the top N items in the data array), so for a regular response like this: {"complete":true,"data":[{"id":1,"value":"X"},{"id":2,"value":"Y"},{"id":3,"value":"Z"}],"errors":[]} it would return just the middle element in the data array but keep the surrounding json properties like so: {"complete":true,"data":[{"id":2,"value":"Y"}],"errors":[]} (this is returning a DataResponse< IdValue > in a WebAPI controller for which I've found a partial solution described bellow, but unfortunately since DataResponse is generic I'd have to add all of these NewtonSoft JsonConverter to the startup which seems like a bad solution to me so I'd like the solution to work for any passed in type beside simplistic IdValue and preferably even for OData endpoints i.e. same endpoints but with $top/$skip/$filter passed in the end of querystring, for any class which is serializable to JSON of course...).

Trying to do that what I've managed so far was to change that generic DataResponse type in a way that it also implements IQueryable interface and had its implementation Expression, ElementType and Provider be returned as those of the contained data property which is now of type IEnumerable like so:

public class DataResponse<T> : BaseResponse, IQueryable<T>
   {
       // BaseResponse holds the errors and complete indicator properties and the DataReponse has the data property bellow like so:
       public IEnumerable<T> Data { get; set; }

       // I just implemented tehe IQueryable interface like so:
       public Expression Expression
       {
           get
           {
               return Data.AsQueryable().Expression;
           }
       }

       public Type ElementType
       {
           get
           {
               return Data.AsQueryable().ElementType;
           }
       }

       public IQueryProvider Provider
       {
           get
           {
               return Data.AsQueryable().Provider;
           }
       }

       public IEnumerator<T> GetEnumerator()
       {
           Items = (IEnumerable<T>)this.Provider.Execute(this.Expression);
           return this.GetEnumerator();
       }

       IEnumerator IEnumerable.GetEnumerator()
       {
           if (typeof(Expression) == typeof(System.Linq.EnumerableQuery))
           {
               // Checking the expression since in case of no OData querystring just this would fail
               return ((IEnumerable)this.Provider.Execute(this.Expression)).GetEnumerator();
           }
           else
           {
               return Data.GetEnumerator();
           }
       }
   }
}

alongside a NewtonSoft JsonConverter (which was a must for using OData on Endpoints with WebAPI controller writen using ASP.NET Core 3.1 as per numerous articles I've read):

public class DataResponseJsonConverter<T> : JsonConverter<DataResponse<T>> {

 public override void WriteJson(JsonWriter writer, DataResponse<T> value, JsonSerializer serializer) {
   JToken t; t = JToken.FromObject(new {
       Data = value.Data, 
       Complete = value.Complete 
   }); }
   JObject o = (JObject)t;
   o.WriteTo(writer); 
 } 

 public override DataResponse<T> ReadJson(JsonReader reader, Type objectType, DataResponse<T> existingValue, bool hasExistingValue, JsonSerializer serializer) { 
   throw new NotImplementedException(); 
 } 
}

which was added to the Configure method of the Startup.cs class like so (whole file omitted for brevity):

services.AddControllers().AddNewtonsoftJson(options => {
   options.SerializerSettings.Converters.Add(new DataResponseJsonConverter<IdValue>()); 
});

but the code above only works for the IdValue I added in the NetwonsoftJson options and not all and every class my controllers could be returning (I was hoping I could add a generic DataResponseJsonConverter< object >() and I've even added the JsonConverter attribute to the DataResponse class without the inner generic type class but both of those fail, although it uses that converter I wrote above but without a concrete class being added into options for each dataresponse class I'm guessing it fails because it can't figure out how to serialize any of those other DataResponse< not_specified_classes_in_startup > return types).

So although managing to apply the OData filters successfully now with the attempts described the code above still just returns the Data portion of the response instead of the whole wrapped DataResponse including the errors (which I can't set as they aren't settable in the BaseResponse) and even that completion indicator with passing in an OData endpoint, and me trying to change the IEnumerable.GetEnumerator() to new up and/or rewrap the response in that single place there just causes a stack overflow error (since I suppose OData serialized the response using the GetEnumerator call and that would just keep calling itself if I were to return the recreate and return an enumerator of the same typed object there...maybe, I'm thinking ).

Is there a way to do this in ideally just this one place thus having it work with just decorating the controller method with the [EnableQuery] and nothing else in the controller?

Can it be done that way instead of having to rewrite each and every controller and apply the ODataQueryOptions there (which I'm not even sure can be done easily in my case given the errors are filled elsewhere and completion indicator part of the response even has no public setter) and/or do I have to do something even more complex (I think I saw there's maybe somthing like a QueryTranslator also but not sure if I can use that) and how please? I'm just getting into the IQueryable so if this is a simple thing and I missed something I apologize. TIA

GI1
  • 116
  • 8
  • Just to add that the code above adds Complete to the response when the OData Endpoint is called without any OData filter options...when any OData $top/$skip/$filter value is passed in the querystring it will still return just the Items and no Complete value in the response...undoubtedly because it's not able to use the JsonConverter I added in the newtonsoft's startup.cs options which OData doesn't seem to be using, best I can figure the Converter should be for a QueryExpression or something, but I'm not sure so if someone could give more insight I'd be grateful, thank you in advance for that! – GI1 Dec 14 '20 at 00:22

0 Answers0