Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

(Binary) serialization of Taxonomies not supported #192

Closed
xantari opened this issue Feb 26, 2020 · 14 comments
Closed

(Binary) serialization of Taxonomies not supported #192

xantari opened this issue Feb 26, 2020 · 14 comments
Assignees
Milestone

Comments

@xantari
Copy link

xantari commented Feb 26, 2020

Brief bug description

I am attempting to cache the objects using a variant of your Webhooks version of the CachingDeliveryClient that I have modified to store into a distributed cache. But it seems the objects are not marked as serializable :(

I get this error when attempting to store my object into our distributed cache:

System.Runtime.Serialization.SerializationException: Type 'Kentico.Kontent.Delivery.DeliveryTaxonomyResponse' in Assembly 'Kentico.Kontent.Delivery, Version=12.3.0.0, Culture=neutral, PublicKeyToken=null' is not marked as serializable.
   at System.Runtime.Serialization.FormatterServices.InternalGetSerializableMembers(Type type)
   at System.Runtime.Serialization.FormatterServices.<>c.<GetSerializableMembers>b__5_0(MemberHolder mh)
   at System.Collections.Concurrent.ConcurrentDictionary`2.GetOrAdd(TKey key, Func`2 valueFactory)
   at System.Runtime.Serialization.FormatterServices.GetSerializableMembers(Type type, StreamingContext context)
   at System.Runtime.Serialization.Formatters.Binary.WriteObjectInfo.InitMemberInfo()
   at System.Runtime.Serialization.Formatters.Binary.WriteObjectInfo.InitSerialize(Object obj, ISurrogateSelector surrogateSelector, StreamingContext context, SerObjectInfoInit serObjectInfoInit, IFormatterConverter converter, ObjectWriter objectWriter, SerializationBinder binder)
   at System.Runtime.Serialization.Formatters.Binary.ObjectWriter.Serialize(Object graph, BinaryFormatterWriter serWriter, Boolean fCheck)
   at System.Runtime.Serialization.Formatters.Binary.BinaryFormatter.Serialize(Stream serializationStream, Object graph, Boolean check)
   at System.Runtime.Serialization.Formatters.Binary.BinaryFormatter.Serialize(Stream serializationStream, Object graph)
   at ARRT.Kentico.Common.Extensions.Serialization.ToByteArray(Object obj) in C:\TFS\ARRT.All.Projects\ARRT.Kentico.Common\Extensions\SerilizationExtensions.cs:line 20
   at ARRT.Kentico.Common.Caching.Webhooks.DistributedCacheManager.GetOrAddAsync[T](String key, Func`1 valueFactory, Func`2 dependenciesFactory, Func`2 shouldCache) in C:\TFS\ARRT.All.Projects\ARRT.Kentico.Common\Caching\Webhooks\DistributedCacheManager.cs:line 95
   at ARRT.Kentico.Common.Caching.Webhooks.DistributedCachingDeliveryClient.GetTaxonomyAsync(String codename) in C:\TFS\ARRT.All.Projects\ARRT.Kentico.Common\Caching\Webhooks\DistributedCachingDeliveryClient.cs:line 321
   at ARRT.Web.Helpers.Extensions.DeliveryClientExtensions.GetSitemapTaxonomy(IDeliveryClient deliveryClient) in C:\TFS\ARRT.All.Projects\ARRT.Web\Helpers\Extensions\DeliveryClientExtensions.cs:line 18
   at ARRT.Web.HostingServices.SitemapCacheHostedService.StartAsync(CancellationToken cancellationToken) in C:\TFS\ARRT.All.Projects\ARRT.Web\HostingServices\SitemapCacheHostedService.cs:line 36
   at Microsoft.Extensions.Hosting.Internal.Host.StartAsync(CancellationToken cancellationToken)
   at Microsoft.Extensions.Hosting.HostingAbstractionsHostExtensions.RunAsync(IHost host, CancellationToken token)
   at Microsoft.Extensions.Hosting.HostingAbstractionsHostExtensions.RunAsync(IHost host, CancellationToken token)
   at Microsoft.Extensions.Hosting.HostingAbstractionsHostExtensions.Run(IHost host)
   at ARRT.Web.Program.Main(String[] args) in C:\TFS\ARRT.All.Projects\ARRT.Web\Program.cs:line 27

Repro steps

Try serialize a DeliveryTaxonomyResponse to a distributed cache.

We use this code to take care of the serialization/deserialization:

public static byte[] ToByteArray(this object obj)
        {
            if (obj == null)
            {
                return null;
            }
            BinaryFormatter binaryFormatter = new BinaryFormatter();
            using (MemoryStream memoryStream = new MemoryStream())
            {
                binaryFormatter.Serialize(memoryStream, obj);
                return memoryStream.ToArray();
            }
        }
        public static T FromByteArray<T>(this byte[] byteArray) where T : class
        {
            if (byteArray == null)
            {
                return default(T);
            }
            BinaryFormatter binaryFormatter = new BinaryFormatter();
            using (MemoryStream memoryStream = new MemoryStream(byteArray))
            {
                return binaryFormatter.Deserialize(memoryStream) as T;
            }
        }

Expected behavior

All Kentico Kontent models should be serializable.

Test environment

  • Platform/OS: .NET Core 3.1.2, Kentico Kontent 12.3.0 librarys
@xantari
Copy link
Author

xantari commented Feb 26, 2020

Tried to work around this issue using BSON data format but no go on that front as well.

Attempted new serialization/deserialization routines:

 public static byte[] ToByteArray(this object obj)
        {
            if (obj == null)
            {
                return null;
            }
            //BinaryFormatter binaryFormatter = new BinaryFormatter();
            //using (MemoryStream memoryStream = new MemoryStream())
            //{
            //    binaryFormatter.Serialize(memoryStream, obj);
            //    return memoryStream.ToArray();
            //}
            using (MemoryStream ms = new MemoryStream())
            {
                using (BsonDataWriter writer = new BsonDataWriter(ms))
                {
                    JsonSerializer serializer = new JsonSerializer();
                    serializer.Serialize(writer, obj);
                }
                return ms.ToArray();
            }
        }
        public static T FromByteArray<T>(this byte[] byteArray) where T : class
        {
            //if (byteArray == null)
            //{
            //    return default(T);
            //}
            //BinaryFormatter binaryFormatter = new BinaryFormatter();
            //using (MemoryStream memoryStream = new MemoryStream(byteArray))
            //{
            //    return binaryFormatter.Deserialize(memoryStream) as T;
            //}
            using (MemoryStream ms = new MemoryStream(byteArray))
            {
                using (BsonDataReader reader = new BsonDataReader(ms))
                {
                    JsonSerializer serializer = new JsonSerializer();

                    return serializer.Deserialize<T>(reader);
                }
            }
        }

New Error:

Newtonsoft.Json.JsonSerializationException: Unable to find a constructor to use for type Kentico.Kontent.Delivery.DeliveryTaxonomyResponse. A class should either have a default constructor, one constructor with arguments or a constructor marked with the JsonConstructor attribute. Path 'Taxonomy'.
   at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.CreateNewObject(JsonReader reader, JsonObjectContract objectContract, JsonProperty containerMember, JsonProperty containerProperty, String id, Boolean& createdFromNonDefaultCreator)
   at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.CreateObject(JsonReader reader, Type objectType, JsonContract contract, JsonProperty member, JsonContainerContract containerContract, JsonProperty containerMember, Object existingValue)
   at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.CreateValueInternal(JsonReader reader, Type objectType, JsonContract contract, JsonProperty member, JsonContainerContract containerContract, JsonProperty containerMember, Object existingValue)
   at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.Deserialize(JsonReader reader, Type objectType, Boolean checkAdditionalContent)
   at Newtonsoft.Json.JsonSerializer.DeserializeInternal(JsonReader reader, Type objectType)
   at Newtonsoft.Json.JsonSerializer.Deserialize(JsonReader reader, Type objectType)
   at Newtonsoft.Json.JsonSerializer.Deserialize[T](JsonReader reader)
   at ARRT.Kentico.Common.Extensions.Serialization.FromByteArray[T](Byte[] byteArray) in C:\TFS\ARRT.All.Projects\ARRT.Kentico.Common\Extensions\SerilizationExtensions.cs:line 52
   at ARRT.Kentico.Common.Caching.Webhooks.DistributedCachingDeliveryClient.GetTaxonomyAsync(String codename) in C:\TFS\ARRT.All.Projects\ARRT.Kentico.Common\Caching\Webhooks\DistributedCachingDeliveryClient.cs:line 321
   at ARRT.Web.Helpers.Extensions.DeliveryClientExtensions.GetSitemapTaxonomy(IDeliveryClient deliveryClient) in C:\TFS\ARRT.All.Projects\ARRT.Web\Helpers\Extensions\DeliveryClientExtensions.cs:line 18
   at ARRT.Web.HostingServices.SitemapCacheHostedService.StartAsync(CancellationToken cancellationToken) in C:\TFS\ARRT.All.Projects\ARRT.Web\HostingServices\SitemapCacheHostedService.cs:line 36
   at Microsoft.Extensions.Hosting.Internal.Host.StartAsync(CancellationToken cancellationToken)
   at Microsoft.Extensions.Hosting.HostingAbstractionsHostExtensions.RunAsync(IHost host, CancellationToken token)
   at Microsoft.Extensions.Hosting.HostingAbstractionsHostExtensions.RunAsync(IHost host, CancellationToken token)
   at Microsoft.Extensions.Hosting.HostingAbstractionsHostExtensions.Run(IHost host)
   at ARRT.Web.Program.Main(String[] args) in C:\TFS\ARRT.All.Projects\ARRT.Web\Program.cs:line 27

@petrsvihlik
Copy link
Contributor

hi @xantari. I'm looking into it right now.

@xantari
Copy link
Author

xantari commented Feb 27, 2020

Thanks so much! Let me know if you want any more of our code.

@petrsvihlik petrsvihlik changed the title Distributed Caching not possible with Kentico Kontent? Binary serialization not supported Feb 28, 2020
@petrsvihlik
Copy link
Contributor

Hi @xantari ,
the code pointed out above is indeed not supported at the moment.

  1. The first example would require models such as TaxonomyGroup to be marked with [Serializable] + potentially the model's properties & fields to be marked with some other attributes determining what should be serialized & what shouldn't.

  2. The second example fails because the models don't have public constructors that could be used by the (binary) JSON serializers. Especially during deserialization.

This effectively prevents serialization and deserialization of Taxonomies.

Let me please further understand your scenario so that we can come up with a good fix:

  1. Do you require binary serialization or would e.g. JSON serialization do? Even MS docs recommend avoiding binary serialization if possible
  2. Do you need to serialize the whole DeliveryTaxonomyResponse or would it be enough to serialize just the TaxonomyGroup? In general, I would avoid serializing any *Response object as they contain metadata such as request URLs, raw content, etc. that are not relevant to the object's data itself.

@petrsvihlik petrsvihlik changed the title Binary serialization not supported (Binary) serialization of Taxonomies not supported Feb 28, 2020
@petrsvihlik petrsvihlik added this to the vNext milestone Feb 28, 2020
@xantari
Copy link
Author

xantari commented Feb 28, 2020

IDistributedCache interface which is what nearly all of the distributed cache providers all have Set() and SetAsync() methods that require raw binary data:

https://docs.microsoft.com/en-us/dotnet/api/microsoft.extensions.caching.distributed.idistributedcache?view=dotnet-plat-ext-3.1

  1. Do you require binary serialization or would e.g. JSON serialization do? Even MS docs recommend avoiding binary serialization if possible

IDistributedCache requires binary data (byte[] as shown in Set methods in link above).

  1. Do you need to serialize the whole DeliveryTaxonomyResponse or would it be enough to serialize just the TaxonomyGroup? In general, I would avoid serializing any *Response object as they contain metadata such as request URLs, raw content, etc. that are not relevant to the object's data itself.

I just took your current CacheDeliveryClient from here:

https://github.com/Kentico/kontent-boilerplate-net/blob/master/src/content/Kentico.Kontent.Boilerplate/Caching/Webhooks/CachingDeliveryClient.cs

Then I changed the CacheManager to use the DistributedCache. Which means doing binary serialization of all the objects there.

The CachingDeliveryClient.cs at the above URL is serializing the entire DeliveryTaxonomyResponse.

@xantari
Copy link
Author

xantari commented Feb 28, 2020

Whats interesting is I just noticed the extension methods here: https://docs.microsoft.com/en-us/dotnet/api/microsoft.extensions.caching.distributed.idistributedcache?view=dotnet-plat-ext-3.1

Which show a bunch of strings.

So perhaps I can try serializing all of that data as regular JSON strings instead. Will try that and update this issue request when I finish.

@petrsvihlik
Copy link
Contributor

So perhaps I can try serializing all of that data as regular JSON strings instead. Will try that and update this issue request when I finish.

Yup, that'd be great. There still might be a problem deserializing the taxonomies but a potential fix might be simpler than in the case of binary serialization.

Thanks for the effort!

@xantari
Copy link
Author

xantari commented Feb 28, 2020

Couldn't get it working with String's either.

Get this error when trying to de-serialize DeliveryTaxonomyResponse.

Newtonsoft.Json.JsonSerializationException: Unable to find a constructor to use for type Kentico.Kontent.Delivery.DeliveryTaxonomyResponse. A class should either have a default constructor, one constructor with arguments or a constructor marked with the JsonConstructor attribute. Path 'Taxonomy', line 1, position 12.
   at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.CreateNewObject(JsonReader reader, JsonObjectContract objectContract, JsonProperty containerMember, JsonProperty containerProperty, String id, Boolean& createdFromNonDefaultCreator)
   at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.CreateObject(JsonReader reader, Type objectType, JsonContract contract, JsonProperty member, JsonContainerContract containerContract, JsonProperty containerMember, Object existingValue)
   at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.CreateValueInternal(JsonReader reader, Type objectType, JsonContract contract, JsonProperty member, JsonContainerContract containerContract, JsonProperty containerMember, Object existingValue)
   at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.Deserialize(JsonReader reader, Type objectType, Boolean checkAdditionalContent)
   at Newtonsoft.Json.JsonSerializer.DeserializeInternal(JsonReader reader, Type objectType)
   at Newtonsoft.Json.JsonSerializer.Deserialize(JsonReader reader, Type objectType)
   at Newtonsoft.Json.JsonConvert.DeserializeObject(String value, Type type, JsonSerializerSettings settings)
   at Newtonsoft.Json.JsonConvert.DeserializeObject[T](String value, JsonSerializerSettings settings)
   at Newtonsoft.Json.JsonConvert.DeserializeObject[T](String value)
   at ARRT.Kentico.Common.Caching.Webhooks.DistributedCachingDeliveryClient.GetTaxonomyAsync(String codename) in C:\TFS\ARRT.All.Projects\ARRT.Kentico.Common\Caching\Webhooks\DistributedCachingDeliveryClient.cs:line 352
   at ARRT.Web.Helpers.Extensions.DeliveryClientExtensions.GetSitemapTaxonomy(IDeliveryClient deliveryClient) in C:\TFS\ARRT.All.Projects\ARRT.Web\Helpers\Extensions\DeliveryClientExtensions.cs:line 18
   at ARRT.Web.HostingServices.SitemapCacheHostedService.StartAsync(CancellationToken cancellationToken) in C:\TFS\ARRT.All.Projects\ARRT.Web\HostingServices\SitemapCacheHostedService.cs:line 36
   at Microsoft.Extensions.Hosting.Internal.Host.StartAsync(CancellationToken cancellationToken)
   at Microsoft.Extensions.Hosting.HostingAbstractionsHostExtensions.RunAsync(IHost host, CancellationToken token)
   at Microsoft.Extensions.Hosting.HostingAbstractionsHostExtensions.RunAsync(IHost host, CancellationToken token)
   at Microsoft.Extensions.Hosting.HostingAbstractionsHostExtensions.Run(IHost host)
   at ARRT.Web.Program.Main(String[] args) in C:\TFS\ARRT.All.Projects\ARRT.Web\Program.cs:line 27

It is erroring on this line:

return JsonConvert.DeserializeObject(Encoding.UTF8.GetString(data));

        /// <summary>
        /// Returns a taxonomy group.
        /// </summary>
        /// <param name="codename">The codename of a taxonomy group.</param>
        /// <returns>The taxonomy group with the specified codename.</returns>
        public async Task<DeliveryTaxonomyResponse> GetTaxonomyAsync(string codename)
        {
            var data = (await CacheManager.GetOrAddAsync(
                CacheHelper.GetTaxonomyKey(codename),
                () => DeliveryClient.GetTaxonomyAsync(codename),
                CacheHelper.GetTaxonomyDependencies,
                response => response != null));

            return JsonConvert.DeserializeObject<DeliveryTaxonomyResponse>(Encoding.UTF8.GetString(data));
        }

Attached is my full DistributedCachingDeliveryClient.cs.

DistributedCachingDeliveryClient Code.zip

@petrsvihlik
Copy link
Contributor

Yes, I suspected that would happen. As I said earlier:

The second example fails because the models don't have public constructors that could be used by the (binary) JSON serializers. Especially during deserialization.
This effectively prevents serialization and deserialization of Taxonomies.

We know for sure that we'll need to fix the taxonomies. But I'm more curious whether the other objects/methods work fine - like GetItemsAsync<T>.
Could you please verify that your implementation works fine for content items? If so, we'll know that if we replicate the pattern to taxonomies, it'll resolve your issue.

If possible, I'd like to avoid using the [Serializable] attributes in the SDK and rather rely on JSON/BSON serialization.

Thank you!

@xantari
Copy link
Author

xantari commented Mar 2, 2020

Couldn't get GetItemsAsync to work either.

Tried this:

var result = await deliveryClient.GetItemAsync<LandingPage>("home", new DepthParameter(100));

Then got the following error:

[11:03:39 FTL] Application start-up failed
Newtonsoft.Json.JsonSerializationException: Unable to find a constructor to use for type Kentico.Kontent.Delivery.DeliveryItemResponse`1[ARRT.Web.Models.LandingPage]. A class should either have a default constructor, one constructor with arguments or a constructor marked with the JsonConstructor attribute. Path 'Item', line 1, position 8.
   at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.CreateNewObject(JsonReader reader, JsonObjectContract objectContract, JsonProperty containerMember, JsonProperty containerProperty, String id, Boolean& createdFromNonDefaultCreator)
   at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.CreateObject(JsonReader reader, Type objectType, JsonContract contract, JsonProperty member, JsonContainerContract containerContract, JsonProperty containerMember, Object existingValue)
   at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.CreateValueInternal(JsonReader reader, Type objectType, JsonContract contract, JsonProperty member, JsonContainerContract containerContract, JsonProperty containerMember, Object existingValue)
   at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.Deserialize(JsonReader reader, Type objectType, Boolean checkAdditionalContent)
   at Newtonsoft.Json.JsonSerializer.DeserializeInternal(JsonReader reader, Type objectType)
   at Newtonsoft.Json.JsonSerializer.Deserialize(JsonReader reader, Type objectType)
   at Newtonsoft.Json.JsonConvert.DeserializeObject(String value, Type type, JsonSerializerSettings settings)
   at Newtonsoft.Json.JsonConvert.DeserializeObject[T](String value, JsonSerializerSettings settings)
   at Newtonsoft.Json.JsonConvert.DeserializeObject[T](String value)
   at ARRT.Kentico.Common.Caching.Webhooks.DistributedCachingDeliveryClient.GetItemAsync[T](String codename, IEnumerable`1 parameters) in C:\TFS\ARRT.All.Projects\ARRT.Kentico.Common\Caching\Webhooks\DistributedCachingDeliveryClient.cs:line 112
   at ARRT.Kentico.Common.Caching.Webhooks.DistributedCachingDeliveryClient.GetItemAsync[T](String codename, IQueryParameter[] parameters) in C:\TFS\ARRT.All.Projects\ARRT.Kentico.Common\Caching\Webhooks\DistributedCachingDeliveryClient.cs:line 75
   at ARRT.Web.HostingServices.SitemapCacheHostedService.StartAsync(CancellationToken cancellationToken) in C:\TFS\ARRT.All.Projects\ARRT.Web\HostingServices\SitemapCacheHostedService.cs:line 38
   at Microsoft.Extensions.Hosting.Internal.Host.StartAsync(CancellationToken cancellationToken)
   at Microsoft.Extensions.Hosting.HostingAbstractionsHostExtensions.RunAsync(IHost host, CancellationToken token)
   at Microsoft.Extensions.Hosting.HostingAbstractionsHostExtensions.RunAsync(IHost host, CancellationToken token)
   at Microsoft.Extensions.Hosting.HostingAbstractionsHostExtensions.Run(IHost host)
   at ARRT.Web.Program.Main(String[] args) in C:\TFS\ARRT.All.Projects\ARRT.Web\Program.cs:line 27

It seems the generated models are not compatible with serialization either.

This was the command we used to generate our models:

dotnet tool restore
dotnet tool run KontentModelGenerator -p "406ac8c6-58e8-00b3-e3c1-0c312965deb2" -o "..\Models\ContentTypes" -n "ARRT.Web.Models" -g -s -f "Generated" -b "ContentTypeBase"

If fails when it tries to deserialize the JSON data that is stored in the distributed cache:

return JsonConvert.DeserializeObject<DeliveryItemResponse<T>>(Encoding.UTF8.GetString(data));

Attached is what was in the JSON data that got returned from the distributed cache.

Example Distributed Cache JSON data.zip

@xantari
Copy link
Author

xantari commented Mar 2, 2020

I've been trying to figure out a way around this issue and started down the path of making the delivery client use it's own HttpClient that pulls it's json data from the distributed cache.

My issue is that I don't fully understand how the cache eviction works with the current sample code and those SemaphoreSlims to know how to correlate it to some JSON data that is in the distributed cache based off of the unique URL that is used as the Cache Key in the distributed cache.

I was inspecting the data within the CacheManager and it appears there is an ApiUrl property in the response. This would correlate exactly to what the cache key is that is being used in the HttpRequestCachingHandler shown below.

So perhaps this might be a way to solve this problem, but I don't fully understand all the internals here to complete it.

    public class HttpRequestCachingHandler : DelegatingHandler
    {
        private readonly ICacheManager _cacheManager;

        public HttpRequestCachingHandler(ICacheManager cacheManager)
        {
            _cacheManager = cacheManager;
        }

        protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
        {
            string jsonData;
            HttpResponseMessage response;

            //Try get the JSON data from our distributed cache
            if (_cacheManager.TryGet<string>(request.RequestUri.ToString(), out jsonData))
            {
                response = new HttpResponseMessage() { StatusCode = HttpStatusCode.OK };
                response.Content = new StringContent(jsonData, System.Text.Encoding.UTF8, "application/json");
                return response;
            }
            else //Not in cache, lets fetch it from Kentico.
            {
                response = await base.SendAsync(request, cancellationToken);
		
		//TODO: Put the JSON data into the cache here using request.RequestUri.ToString() as the cache key
            }

            return response;
        }
    }

The KenticoHttpClient:

    public class KenticoHttpClient
    {
        public KenticoHttpClient(HttpClient client)
        {
            Client = client;
        }

        public HttpClient Client { get; }
    }

The startup code:

            services.AddTransient<HttpRequestCachingHandler>();
            services.AddHttpClient("kenticoclient")
            .AddHttpMessageHandler((s) => s.GetService<HttpRequestCachingHandler>())
            .AddTypedClient<KenticoHttpClient>();

            services.AddSingleton<IContentLinkUrlResolver, CustomContentLinkUrlResolver>();
            services.AddSingleton<ITypeProvider, CustomTypeProvider>();

            services.AddWebhookInvalidatedCachingClient(builder =>
            {
                var client = DeliveryClientBuilder
                        .WithOptions(_ => deliveryOptions)
                        .WithTypeProvider(builder.GetService<ITypeProvider>())
                        .WithContentLinkUrlResolver(builder.GetService<IContentLinkUrlResolver>())
                        .WithHttpClient(builder.GetRequiredService<KenticoHttpClient>().Client)
                        .Build();
                return client;
            }, (options) =>
            {
                options.StaleContentTimeout = TimeSpan.FromSeconds(2);
                options.DefaultTimeout = TimeSpan.FromHours(24);
            });

@petrsvihlik
Copy link
Contributor

petrsvihlik commented Mar 3, 2020

@xantari thank you very much for all the information. Implementing IDistributedCache is much more complex than IMemoryCache (mostly due to the serialization).

We now need to decide what level of support we want to incorporate in the SDK. Questions we are asking ourselves are:

  • Do we want to cache HTTP responses (JSONs)?
  • Do we want to cache strongly-typed models?
  • Or do we want to cache whole Delivery*Response objects?

Caching HTTP responses
As you know, one of the things we're planning is better support for HttpClientFactory and, transitively, retry policies using Polly:

Since Polly also supports caching, it could be a way to go for caching HTTP responses:

Disadvantages:

  • Eviction of cache entries is not supported yet (planned for Polly v8 though)
  • You could probably build only a very flat cache with no sophisticated cache dependencies, etc. (but it might potentially be suitable for your scenario...?)

This can already be tested if you use the Generated clients approach of registering the HttpClientFactory:

services.AddHttpClient("FactoryClient", c => { /* Do whatever else you wish here... */ })
                    .AddHttpMessageHandler<LoggingHandler>()
                    .AddTypedClient(c => DeliveryClientBuilder.WithOptions(...).WithHttpClient(c).Build())
                    .AddPolicyHandlerFromRegistry(/* see https://nodogmablog.bryanhogan.net/2018/11/caching-in-polly-6-and-the-httpclientfactory/ */);

CachingInPolly6.zip

Caching strongly-typed models & Delivery*Response objects
Would require major refactorings:

  • Create public constructors (for DeliveryItemResponse<T>, ApiResponse, etc.)
  • Make the models (such as DeliveryItemResponse<T>) free of IModelProvider (move the logic outside the models)
  • add setters
  • reconsider the dynamic properties such as dynamic LinkedItems

So the question now is: Do we want to support object-level caching as for IMemoryCache or are we ok to support caching of JSON HTTP responses for IDistributedCache?

If you have any input on this @xantari please let us know or we can schedule a call if you'd be up for it.

@xantari
Copy link
Author

xantari commented Mar 3, 2020

Our main goal is as follows:

  1. Cache all data from Kentico (including assets, which we are now doing).
  2. Evict cache entries that change in Kentico Kontent by using Webhooks.

The caching of HTTP JSON data using Polly would work fine as long as there was a way to remove the entire cache when a change occurred so that it could start building up the cache again. However you need to know all the cache keys Webserver Node 1, Webserver Node 2, and Webserver Node 3 put into the cache for instance. (example of a 3 node web server).

So when the webhook came in, how would you know how to clear the cache? Even keeping track of keys on each node is hard, because Node 1 might only have 15 of the keys, Node 2 might have 30 of the keys, and Node 3 has 12 of the cache keys.

If there is a way to simply clear the entire distributed cache then we could perhaps easily solve this with Polly and when an incoming webhook came in, we just clear the entire cache so that all nodes start to re-fetch content again.

That is why I was trying to get it to work with what you already have working with IMemoryCache. Because it has all the dependency stuff figured out already and seems to figure out the depedency keys to evict. It would have provided the best solution as It would have just been a matter of swapping out IMemoryCache for IDistributedCache if it weren't for the serialization issue.

@petrsvihlik
Copy link
Contributor

thanks for the video call, the motivation is now clear and let's treat this as a more generic issue of supporting distributed caching in #196

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

3 participants