QueryContainer is not populated from json passed in to WebApi via [FromBody]

We are in the process of upgrading Elasticsearch from v 5.0 to v.6.8. We have a webApi that uses Nest (which we also just upgraded). We have an API that has this:

        [HttpPost]
        [Route("_cluster/{indices}")]
        public async Task<ClusterResponse> NestQueryCluster([FromBody] ClusterInput clientQuery, [FromUri] string indices, [FromUri] string trendingDays = null)
        {

The model for ClusterInput is:

   /// <summary>
    /// Input model for the cluster
    /// </summary>
    public class ClusterInput
    {
        /// <summary>
        /// Gets or sets the query.
        /// </summary>
        public QueryContainer Query { get; set; }

        /// <summary>
        /// Gets or sets the size of the documents to process
        /// </summary>
        public int? MaxDocumentsToSample { get; set; }

        /// <summary>
        /// Gets or sets beginning time
        /// </summary>
        public DateTimeOffset? From { get; set; }

        /// <summary>
        /// Gets or sets the end time
        /// </summary>
        public DateTimeOffset? To { get; set; }

We are sending in payloads like this:

{
    "query": {
        "bool": {
            "must": [
                {
                    "range": {
                        "creationtimestamp": {
                            "gte": "2021-08-08T00:00:00Z",
                            "lte": "2021-09-07T23:59:59Z"
                        }
                    }
                },
                {
                    "bool": {
                        "should": [
                            {
                                "bool": {
                                    "must": [
                                        {
                                            "bool": {
                                                "should": [
                                                    {
                                                        "terms": {
                                                            "myTerm": [
                                                                "foo",
                                                                "bar"
                                                            ]
                                                        }
                                                  etc...
            ]
        }
    },
    "maxDocumentsToSample": 5000,
    "from": "0001-01-01T00:00:00+00:00",
    "to": "2021-09-08T00:00:00+00:00"
}

When it is read, maxDocumentsToSample, from and to all parse to the correct value. "query" is not, however. It is null and we end up with a generic new QueryContainer. In our v5 environment the query parses into a QueryContainer correctly.

I have worked through this, and I think it is related:
Custom Serialization | Elasticsearch.Net and NEST: the .NET clients [7.x] | Elastic

I did do a lot of upgrade work so that our clients read and write data correctly. I am not sure what parsing would be getting used when creating the Nest object from the default FromBody object creation. I am assuming that it is using the incorrect Json library and it does not have the info to create the object from the Json.

I am currently looking into changing my class to create a JOBject instead, and then I will manually convert it using my net client (i.e. RequestResponseSerializer). Is there a better way to do this? Can I override the conversion that is happening when the class is instantiated and have it use Nest.JsonNetSerliazer instead?

Thanks

I am still not sure what is going on, but if anyone else runs into this, this is how I solved it.

I created a custom model binder. This allowed me to get the class, but then create the missing query with custom logic.

This is the model binder (note that it uses System.Web.Http.ModelBinding)

   public class ClusterInputModelBinder : IModelBinder
    {
        public ClusterInputModelBinder()
        {
        }

        /// <summary>
        /// Bind the ClusterInput class from the body of the request
        /// </summary>
        /// <param name="actionContext">The incoming context</param>
        /// <param name="bindingContext">The outgoing context</param>
        /// <returns>bool as to success</returns>
        public bool BindModel(HttpActionContext actionContext, ModelBindingContext bindingContext)
        {
            // The success status
            bool success = true;

            // This is what we will set at the model
            ClusterInput clusterInput;

            // Get the model from the body of the request
            using (StreamReader stream = new StreamReader(actionContext.Request.Content.ReadAsStreamAsync().Result))
            {
                string body = stream.ReadToEnd();
                JObject clusterInputJobject = JObject.Parse(body);

                // Deserialize into the ClusterInput. Note that clusterInput.Query will not have the query that came in with the request.
                clusterInput = JsonConvert.DeserializeObject<ClusterInput>(body);

                // Make sure we created the class
                if(clusterInput == null)
                {
                    success = false;
                }

                // Only parse the query if there is one
                if(success && clusterInputJobject.ContainsKey("query"))
                {
                    // Get the query as a stream
                    byte[] byteArray = Encoding.ASCII.GetBytes(JsonConvert.SerializeObject(clusterInputJobject["query"]));
                    MemoryStream ms = new MemoryStream(byteArray);

                    // Use a fake client to get access to the Deserializer and create the QueryContainer
                    ElasticClient client = getFakeClient();
                    QueryContainer queryContainer = (QueryContainer)client.RequestResponseSerializer.Deserialize(typeof(QueryContainer), ms);

                    // Set it on our return object
                    clusterInput.Query = queryContainer;

                    if(clusterInput?.Query == null)
                    {
                        success = false;
                    }
                }
            }

            if(success)
            {
                bindingContext.Model = clusterInput;
            }            

            return success;
        }

        private ElasticClient getFakeClient()
        {
            // build a uri from the url
            var node = new Uri("http://fake.com");

            // create a pool with the Uri
            var pool = new SingleNodeConnectionPool(node);

            ConnectionSettings settings = new ConnectionSettings(pool, sourceSerializer: JsonNetSerializer.Default);

            return new ElasticClient(settings);
        }
    }
}

I then updated my api to use it as such:

        public async Task<ClusterResponse> NestQueryCluster([ModelBinder(BinderType = typeof(ClusterInputModelBinder))] ClusterInput clientQuery, [FromUri] string indices, [FromUri] string trendingDays = null)
        {

This topic was automatically closed 28 days after the last reply. New replies are no longer allowed.