contentful.net
contentful.net copied to clipboard
Issue with deserialization of references
I have encountered what I assume is de-serialization issue, or, at least, unexpected behavior, which I'll try to convey in a simplified manner:
In the CMS: DocType1 { "referenceToDocType3": <referenceDocType3>, "referenceToDocType2": <referenceDocType2>, }
DocType2 { "anotherReferenceToDocType3": <referenceDocType3> }
In the backend:
class DocType1
{
public object ReferenceToDocType2 {get;set;}
}
class DocType2
{
public object AnotherReferenceToDocType3 {get;set;}
}
class DocType3
{
...
}
In these conditions, when I retrieve DocType1 allowing for resolving dependencies, AnotherReferenceToDocType3 is consistently null, unless I change DocType1 to include a ReferenceToDocType3 field (even if I don't use it on that level).
Hi @pmpontes I believe you've run into what I discuss in this blog post: https://robertlinde.se/posts/why-is-my-item-null-contentful-net/
Let me know if it helps you out.
Hi @Roblinde Thanks for the reply. Indeed the issue seems to be what is described in the linked blog post. I looked around for a bit, guess I didn't use the right keywords. So, what I reported was indeed already known and probably not an actual issue. However, enabling that option appears to prevent the use of IContentTypeResolver. In a different field, I have a list of references which are de-serialized to concrete sub-classes via an IContentTypeResolver, which fails when the ResolveEntriesSelectively is enabled because it seems to ignore the IContentTypeResolver and instead attempt to de-serialize them to the base (abstract) class. Any way I can leverage both?
Great! I was hoping it'd be easy to fix.
It shouldn't affect the IContentTypeResolver
, but perhaps there's some kind of edge case here. Could you describe the issue in a little more detail and I'll see if I can understand what's going on.
Again, simplifying a lot:
In the CMS:
ContentBlocksPage
{
"contentBlocks": [
{
sys: { contentTypeId: "textContentBlock" }
...
}
]
}
In the backend:
class ContentBlocksPage {
public List<BaseContentBlock> ContentBlocks { get; set; }
...
}
public abstract class BaseContentBlock {}
public class TextContentBlock : BaseContentBlock
{ ... }
public class EntityResolver : IContentTypeResolver
{
public Type Resolve(string contentTypeId)
{
return new Dictionary<string, Type>()
{
{ "textContentBlock", typeof(TextContentBlock) }
}.TryGetValue(contentTypeId, out var type) ? type : null;
}
}
When I try to get a ContentBlocksPage, I get an exception, essentially saying it can't de-serialize ContentBlocks
to BaseContentBlock
:
Newtonsoft.Json.JsonSerializationException: 'Could not create an instance of type BaseContentBlock. Type is an interface or abstract class and cannot be instantiated.
If BaseContentBlock
isn't abstract, it simply instantiates the base class object, so in either case, it seems to not be using the IContentTypeResolver
when ResolveEntriesSelectively=true
.
Not sure if this is clear enough.
Hmm, this certainly looks like it should work fine. If you debug and inspect just before the call to GetEntries
is the resolver set correctly on the client object?
Yes, I double checked just in case that the custom resolver is set on the contentful client. The only change I made was changing ResolveEntriesSelectively
to true
.
I am using the latest version of the nuget (6.0.7).
Ok, just a theory here. If you add a property public SystemProperties Sys { get; set; }
to the BaseContentBlock
?
Actually, my BaseContentBlock
already extends a BaseEntity
class which does have public SystemProperties Sys { get; set; }
, I guess I oversimplified.
If you set a breakpoint inside the EntityResolver , is it ever hit?
I'm trying to reproduce it, but it seems to be working when I try it... trying to figure out what could be the cause.
It is indeed being hit.
Having run a few different tests myself, I'm suspecting it might have to do with depth (?).
The error I am getting is not when getting the ContentBlocksPage
s directly, I'm trying to get it through an entity called Website
(again, simplified):
class Website {
public ContentBlocksPage HomePage { get;set }
...
}
If I get the ContentBlocksPage
directly, it seems to de-serialize the content blocks correctly.
Ah, then that would be a possible explanation. It could be that you're hitting an unresolved entry. If you try to increase the Include
parameter, does it deserialise correctly?
The include parameter is already set to 10, which I believe is the limit, though at any rate, this query should have only 3 levels (Website > ContentBlocksPage > ContentBlock). And I guess that wouldn't explain why the de-serialization works when ResolveEntriesSelectively
is set to false.
Hmm, the only thing I can think of is if the entity exists in a different part of the json structure and the $type attribute gets set there instead of where it actually is supposed to be set. Are some of the entries in your List<BaseContentBlock> also used elsewhere in the structure?
Hm, a couple of the blocks, possibly (in other ContentBlocksPage
s), but not the very first one, which is where the exception is happening (Path 'items[0].homePage.en.contentBlocks.en[0].sys'.
). I've actually tried to debug the ContentfulClient
locally but I guess it takes a bit more intimate understanding of the JSON structure and the overall GetEntries
function.
I notice that the path includes the locale in this case. Are you using locale=*
to fetch content for all locales at the same time? I might have overlooked that scenario when I look at the code.
Yes, we're using the locale=*
option.
Ok, I'll look into if there's something I've overlooked in that scenario. I assume all your models have their properties wrapped in Dictionary<string, T>
then?
If you were to switch to a specific locale (with appropriate models) are you still able to reproduce the problem?
Indeed, my Website class actually looks like (I oversimplified too much again):
class Website {
public Dictionary<string, ContentBlocksPage> HomePage { get;set }
...
}
If I specify a single locale, and change the Dictionary<string, T>
to T
, the de-serialization of the content blocks seems to work (I get an exception when actually de-serializing the content block fields only, because it's still expecting them to be localized)
Great, seems like we've found a bug. I'll look into it and get back to you.
@pmpontes I have now managed to reproduce the problem and create a unit test covering it. I will release a fix shortly.
@pmpontes I have just released version 6.0.8 which I hope will address this. Let me know if it works for you.
@Roblinde I've upgraded to the latest version, happy to report it works as expected now. Thanks so much for the assistance.
I seem to have run into another possible issue related with the ResolveEntriesSelectively option, this time related with Assets. The scenario:
class Website {
Dictionary<string, ShopSettings> ShopSettings { get;set }
...
}
class ShopSettings {
Dictionary<string, List<OverviewPage>> Pages { get;set }
...
}
class OverviewPage {
Dictionary<string, Employee> ContactPerson { get;set }
...
}
class Employee {
Dictionary<string, Contentful.Core.Models.Asset> Photo { get;set }
...
}
Contentful.Core.Models.Asset{
File File { get; set; }
Dictionary<string, File> FilesLocalized { get; set; }
}
When the option ResolveEntriesSelectively=true
, Photo
doesn't seem to be properly de-serialized, both File and FilesLocalized are left as null, so this might be related?
It's de-serialized properly when ResolveEntriesSelectively=false
.
@pmpontes I'll have a look. Almost certainly related.
@pmpontes having a hard time reproducing this behaviour. If there's anything else you can think of that could give me some more information it would be highly appreciated.
@pmpontes not sure if you still have this issue, but I've just released version 6.0.16 that fixes one more relatively uncommon scenario with deserialization. Perhaps this solves your issue as well?