.NET SDK - DeliveryItemResponse

In building my project I noticed that the constructor for DeliveryItemResponse was made "internal" at some point during development.

I've had to fork the repo to make it public again to enable what I was trying to do.

My use case is that I'm building a site using Lucene.Net to store content items and wanted to be able to re-hydrate a DeliveryClient.ContentItem from a JObject or JToken stored in the search index.

Is there a reason that this was made internal?

Comments

  • petrs@kentico.com[email protected] Eindhoven, NLMember, Administrator, Kentico Staff admin

    Hi Lee,
    I think it was made for the same reason as why the class was marked as sealed - to prevent developers becoming too dependent on the inner implementation (on how the object is being constructed, etc.)
    You made a valid point but I still think it should remain as it is.
    Consider the following scenario:
    We decide to replace the deserialization method (e.g. we find a faster library than Newtonsoft.Json, etc.), then we'll have to change the constructor and your Lucene-related code stops working. Would that be ok?

    Here's what I propose:

    1. Switch your code to use strongly-typed models
    2. Use whatever serialization method to store the models models in the search index (e.g. Newtonsoft.Json) string json = JsonConvert.SerializeObject(product);
    3. Use a complementary method to deserialize it to the previously generated model Product p = JsonConvert.DeserializeObject<Product>(json);

    In this case, the only shared code would be the models which are just POCOs.

    What do you think about this approach?

  • leelee Member ✭✭

    Hi

    That's all good except that it doesn't allow me to loop through all elements on the content item since the strongly typed models don't have an elements array.

    My implementation of Lucene.Net means that I can throw any Content Item of any Content Type at it and it will index that item. Looping through the "elements" during the building of the index allows me to index and tokenize all text and rich_text fields as well as indexing date_time's. I can control exactly how each field gets indexed without needing to know the Content Type.

    The point of this exercise is to be able to provide a common interface for implementing search providers, without the search provider needing to know anything explicit about the Content Types in use on the site... this way I can use the same boilerplate application for any site I wish to create and for any client without significant rework of the underlying architecture.

    Ultimately there needs to be a way to get the RAW response that came down from the API (which ContentItem ultimately uses under the hood) and be able to cache/index/store that response on a per-content-item basis, and a way to use that to re-hydrate a ContentItem object on the other side of a search or cache request.

    If I then want to use ContentItem.CastTo<> to use strongly typed objects in the consuming applications then I can do so.

  • petrs@kentico.com[email protected] Eindhoven, NLMember, Administrator, Kentico Staff admin

    Hi @LeeConlin , thanks for clarification!
    I get it what you're saying. In that case, I suggest we enrich the response objects with raw data (I've already submitted it some time ago here) and expose a string-based constructor.

    Could you please take a look at https://github.com/Kentico/delivery-sdk-net/issues/58 and verify if it makes sense?

    If you want, you can pick up the issue and send us a pull request. You'd be then able to use the standard NuGet instead of the forked repo.

  • leelee Member ✭✭

    Hi @petrsvihlik I actually got around this by creating an extension method on ContentItem that uses a switch on ContentItem.System.Type to return ContentItem.CastTo<> for the strongly typed object and then serialised that into my index.

    A cleaner solution would be for ContentItem to have a method that can automatically determine which strongly typed object to return in a similar way to how JObject has a "ToObject" method.

    Essentially you would have a ContentItem.CastTo(Type type) as well as the method that uses the generic type.

    Then I could do...

    return ContentItem.CastTo(CustomTypeProvider.GetType(ContentItem.System.Type))

    ... instead of needing a switch.

    As a further enhancement, you could have a ContentItem.CastToObject that would use the type provider under the hood and return an object of the correct type.

  • petrs@kentico.com[email protected] Eindhoven, NLMember, Administrator, Kentico Staff admin

    I actually got around this by creating an extension method on ContentItem that uses a switch on ContentItem.System.Type to return ContentItem.CastTo<> for the strongly typed object and then serialised that into my index.

    I'm not sure if I understand why you did that. If you are serializing the strongly-typed object to your index why do you need the non-strongly typed ContentItem in the first place? Can't you just convert everything to strong types? Or do you still need to loop through the elements for some reason?

    A cleaner solution would be for ContentItem to have a method that can automatically determine which strongly typed object to return in a similar way to how JObject has a "ToObject" method.

    That could be done. IMO, it would require only minor refactorings...In fact, the code works like that under the hood already.

    Essentially you would have a ContentItem.CastTo(Type type) as well as the method that uses the generic type.
    ...
    As a further enhancement, you could have a ContentItem.CastToObject

    Yes, it can be done even without specifying the Type type. And it would be cleaner. The ContentItem holds a reference to the DeliveryClient which holds an instance of ICodeFirstTypeProvider.

    Could you please elaborate a bit more on the first point? Or perhaps share the code with me. I'd really love to get enough information so that we could adjust the SDK and cover more scenarios.

    Thanks again!

  • leelee Member ✭✭

    I have an ISearchProvider interface that is implemented by my LuceneSearchProvider (though my idea is to make it possible to easily implement any search system (ElasticSearch, AzureSearch, etc) without needing to refactor the main application.

    public interface ISearchProvider
        {
            Task BuildIndex();
            SearchResults Search(string searchTerm, string sortFieldCodeName = "", bool reverseSort = false, int take = 0, int skip = 0);
            SearchResults SearchByContentType(string contentType, string sortFieldCodeName = "", bool reverseSort = false, int take = 0, int skip = 0);
            object SearchBySlug(string slug);
            object SearchByCodeName(string codeName);
        }
    

    SearchResults is just a wrapper that contains an IEnumerable<object> and some pagination properties.

    I use object because I can compare using myObj is BlogPost and it gives an implementation that is more generic.

    In my LuceneSearchProvider.BuildIndex() method I tuse the ContentItem objects retrieved from DeliveryClient.GetItemsAsync() to loop through each content item and then an inner loop to itterate the Elements array.

    For each element in the array, I use a switch to determine the type of field to use for indexing.

    Text or RichText => TextField("Body")
    Number => DoubleField("Number")
    etc.

    I also explicitly index System.Name, System.Codename, System.Type and a custom "publish_date" field. The Codename and Type fields are stored using the KeywordAnalyzer so that they can be used to retrieve specific items/content-types from the index later.

    Finally, I use my AsStronglyTypedObject() method to get the object which I then JsonSerialize and add to a "store only" field.

    In the other methods that return results, once I have search results from the index I simply retrieve the stored JSON object and deserialize it.

    for example, in my SearchByCodename() method I do this...

    var results = indexSearcher.Search(mainQuery, 1).ScoreDocs;
    if (results.Any())
    {
        var result = results.FirstOrDefault();
        var item = indexSearcher.Doc(result.Doc).Get("content_item");
        var obj = JsonConvert.DeserializeObject<object>(item) as JObject;
        return obj?.ToObject(CustomTypeProvider.GetType(indexSearcher.Doc(result.Doc).Get("Type")));
    }
    else
    {
        return null;
    }
    

    I would much rather use a "built in" method to get the strongly typed object than having to use a switch that needs to be updated whenever I add new content types.

  • petrs@kentico.com[email protected] Eindhoven, NLMember, Administrator, Kentico Staff admin

    Good news then - you can use a built-in method already.

    Just call nonStronglyTypedContentItem.CastTo<object>() and the API will figure it out.

  • leelee Member ✭✭

    :o :s :'(

    I now feel dumb :(

  • petrs@kentico.com[email protected] Eindhoven, NLMember, Administrator, Kentico Staff admin

    No need to...it's preeetty well hidden :)

    So, since this is sorted, is there anything that could be done to improve your experience with the SDK? Any outstanding issues, or things that could be done better?

  • leelee Member ✭✭

    I implemented the CastTo and that works for serialising the data into the search index, but when retrieving the data back out I find that I have to deserialize it as an object, then cast to JObject to return it as the strongly typed version...

        var result = results.FirstOrDefault();
        var item = indexSearcher.Doc(result.Doc).Get("content_item");
        var obj = JsonConvert.DeserializeObject<object>(item) as JObject;
        return obj?.ToObject(CustomTypeProvider.GetType(indexSearcher.Doc(result.Doc).Get("Type")));
    

    Is there a built-in method in the DeliveryClient to take in an object and cast it to the correct strong type?

    Just returning the plain object here is not an option since it prevents me from doing checks like this:

    if(obj is Article){ }
    

    Also, returning the straight object that comes out of the deserializer means that trying to cast it using var castObj = obj as Article just results in null.

  • leelee Member ✭✭

    @petrsvihlik Another issue I've come across is that JSON.NET doesn't like serializing the object that CastTo<object> returns ... it just crashes the app.

  • petrs@kentico.com[email protected] Eindhoven, NLMember, Administrator, Kentico Staff admin

    Is there a built-in method in the DeliveryClient to take in an object and cast it to the correct strong type?

    No, unfortunately there's no such method and never will be since it's not a responsibility of the Delivery SDK to cast anonymous objects. My guess is changing the code to this will help:

    var item = indexSearcher.Doc(result.Doc).Get("content_item");
    var type = CustomTypeProvider.GetType(indexSearcher.Doc(result.Doc).Get("Type"));
    return JsonConvert.DeserializeObject(item, type );
    

    Another issue I've come across is that JSON.NET doesn't like serializing the object that CastTo returns ... it just crashes the app.

    That's strange, I just successfully tested the following code:

    var response = await client.GetItemsAsync(new EqualsFilter("system.type", "article"));
     var itemAsObject = response.Items[0].CastTo<object>();
     string serialized = JsonConvert.SerializeObject(itemAsObject);
     object deserialized = JsonConvert.DeserializeObject(serialized, new CustomTypeProvider().GetType("article"));
    

    Could you perhaps try to catch the exception and share it here?

  • leelee Member ✭✭

    @petrsvihlik It doesn't throw an exception, it just crashes the entire app pool!

    I found out, however, that it was an error on my part. One of my content types didn't have a strongly typed model and this caused the CastTo to fall over.

  • petrs@kentico.com[email protected] Eindhoven, NLMember, Administrator, Kentico Staff admin

    @LeeConlin That's strange. I was unable to reproduce it. If there is a strongly-typed model missing and I wrap the CastTo<object>() call in a try-catch block it always catches the following exception correctly:

    No corresponding CLR type found for the 'xyz' content type. Provide a correct implementation of 'ICodeFirstTypeProvider' to the 'TypeProvider' property.

  • leelee Member ✭✭

    Without the try/catch the execution simply fails on the line with the CastTo call with no error or explanation. Even running in debug mode in Visual Studio it doesn't break on the exception.

    I'm not ruling out an issue in my environment, but that it happens both in VS and on an Azure App Service suggests something else.

  • petrs@kentico.com[email protected] Eindhoven, NLMember, Administrator, Kentico Staff admin

    Could you then please try to wrap it in a try-catch to see if it can be handled? Also, could you check the windows event viewer for any errors? If an application fails without a prior warning there's typically a record in the event log.

  • leelee Member ✭✭

    I think we've cracked it!

    I ran my solution against the demo project ID used in the cloud-example-navigation project.

    The error is in the serialization of the Article content type and JSON.Net gives the following exception:

    Self referencing loop detected with type 'KenticoCloud.Delivery.Custom.Models.ContentTypes.Article'. Path 'RelatedArticles[0].RelatedArticles'.

    The reason it wasn't breaking on the exception is that the serialization was being done in a separate thread by a Quartz.net job.

  • petrs@kentico.com[email protected] Eindhoven, NLMember, Administrator, Kentico Staff admin

    Yeah, that can happen. This is one of the reasons why Kentico Cloud sends modular content in a separate collection. See eg.: "modular_content": {

    The SDK then deserializes the response in a way that it can contain circular references. This is for the convenience of navigating among the items. If you are about to serialize those objects again, you may consider one of the approaches suggested at StackOverflow.

Sign In or Register to comment.