aliostad / CacheCow

An implementation of HTTP Caching in .NET Core and 4.5.2+ for both the client and the server
MIT License
847 stars 172 forks source link

Newtonsoft.Json.JsonSerializationException: Self referencing loop detected for property #223

Closed jagbarcelo closed 5 years ago

jagbarcelo commented 5 years ago

I have a WebApi project where CacheCow.Server is used for caching EntityFramework responses (usually Json objects taken from a database). Due to the parent-child relationships and circular referencing loops of System.Data.Entity.DynamicProxies, we get this exception for some of our published API methods:

Excepción producida: 'Newtonsoft.Json.JsonSerializationException' en mscorlib.dll Error de conexión en WebApiSiteAssembly.Attributes.HandleApplicationExceptionsAttribute.OnException: Newtonsoft.Json.JsonSerializationException: Self referencing loop detected for property 'Group' with type 'System.Data.Entity.DynamicProxies.Group_58148824D915E126EAB1B78FC3443C90AE50FB12F4254FBF524A3C7D29E323D2'. Path '[5].ProjectSummaries[0]'. en Newtonsoft.Json.Serialization.JsonSerializerInternalWriter.CheckForCircularReference(JsonWriter writer, Object value, JsonProperty property, JsonContract contract, JsonContainerContract containerContract, JsonProperty containerProperty) en Newtonsoft.Json.Serialization.JsonSerializerInternalWriter.CalculatePropertyValues(JsonWriter writer, Object value, JsonContainerContract contract, JsonProperty member, JsonProperty property, JsonContract& memberContract, Object& memberValue) en Newtonsoft.Json.Serialization.JsonSerializerInternalWriter.SerializeObject(JsonWriter writer, Object value, JsonObjectContract contract, JsonProperty member, JsonContainerContract collectionContract, JsonProperty containerProperty) en Newtonsoft.Json.Serialization.JsonSerializerInternalWriter.SerializeValue(JsonWriter writer, Object value, JsonContract valueContract, JsonProperty member, JsonContainerContract containerContract, JsonProperty containerProperty) en Newtonsoft.Json.Serialization.JsonSerializerInternalWriter.SerializeList(JsonWriter writer, IEnumerable values, JsonArrayContract contract, JsonProperty member, JsonContainerContract collectionContract, JsonProperty containerProperty) en Newtonsoft.Json.Serialization.JsonSerializerInternalWriter.SerializeValue(JsonWriter writer, Object value, JsonContract valueContract, JsonProperty member, JsonContainerContract containerContract, JsonProperty containerProperty) en Newtonsoft.Json.Serialization.JsonSerializerInternalWriter.SerializeObject(JsonWriter writer, Object value, JsonObjectContract contract, JsonProperty member, JsonContainerContract collectionContract, JsonProperty containerProperty) en Newtonsoft.Json.Serialization.JsonSerializerInternalWriter.SerializeValue(JsonWriter writer, Object value, JsonContract valueContract, JsonProperty member, JsonContainerContract containerContract, JsonProperty containerProperty) en Newtonsoft.Json.Serialization.JsonSerializerInternalWriter.SerializeList(JsonWriter writer, IEnumerable values, JsonArrayContract contract, JsonProperty member, JsonContainerContract collectionContract, JsonProperty containerProperty) en Newtonsoft.Json.Serialization.JsonSerializerInternalWriter.SerializeValue(JsonWriter writer, Object value, JsonContract valueContract, JsonProperty member, JsonContainerContract containerContract, JsonProperty containerProperty) en Newtonsoft.Json.Serialization.JsonSerializerInternalWriter.Serialize(JsonWriter jsonWriter, Object value, Type objectType) en Newtonsoft.Json.JsonSerializer.SerializeInternal(JsonWriter jsonWriter, Object value, Type objectType) en Newtonsoft.Json.JsonConvert.SerializeObjectInternal(Object value, Type type, JsonSerializer jsonSerializer) en CacheCow.Server.JsonSerialiser.Serialise(Object o) en CacheCow.Server.DefaultTimedETagExtractor.Extract(Object viewModel) en CacheCow.Server.DefaultCacheDirectiveProvider.Extract(Object viewModel) en CacheCow.Server.WebApi.HttpCacheAttribute.<OnActionExecutedAsync>d__5.MoveNext()

The fix is quite straightforward, in CacheCow.Server.JsonSerialiser, just change the original return Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(o)) with this:

      JsonSerializerSettings settings = new JsonSerializerSettings()
      {
          ReferenceLoopHandling = Newtonsoft.Json.ReferenceLoopHandling.Ignore,
          PreserveReferencesHandling = Newtonsoft.Json.PreserveReferencesHandling.Objects
      };

      return Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(o, settings));
aliostad commented 5 years ago

HI,

I had a look. Doing this is very specific to your use-case and I am sure some other users could have different use cases.

As a library, CacheCow should allow you to modify its behaviour to suit your need. As you can see, I register JsonSerialiser as the default but you can replace that type with yours including those aspects - just implement a simple interface.

If you need help on that please let me know.

jagbarcelo commented 5 years ago

I see your point. I even tried to do what you suggest before giving up and trying to fix it myself in your own code. I understand that i have to create a new class MyJsonSerialiser that implements ISerialiser, but beyond that, I cannot find a way to instruct CacheCow into using my custom class. In previous versions we needed to explicitly declare a cachingHandler: var cachingHandler = new CacheCow.Server.CachingHandler(GlobalConfiguration.Configuration, eTagStore); … but now, with just the decoration of my controller's actions with [HttpCache] attribute (I'm using ASP.NET Web API) I simply cannot find an entry point to do this customisation. Can you give me a hint about how to achieve this? Thanks a lot.

jagbarcelo commented 5 years ago

Thanks for reopening this issue. ;) It seems I need to use Dependency Injection along with Castle Windsor to achieve this, but yet I haven't managed to make it work. Here's what I have done:

protected void Application_Start()
{
  AreaRegistration.RegisterAllAreas();
  GlobalConfiguration.Configure(WebApiConfig.Register);
  FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
  RouteConfig.RegisterRoutes(RouteTable.Routes);
  BundleConfig.RegisterBundles(BundleTable.Bundles);

  var container = new Castle.Windsor.WindsorContainer();
  ConfigDependencyInjection(container);
  GlobalConfiguration.Configuration.DependencyResolver = new WindsorDependencyResolver(container);
}

static void ConfigDependencyInjection(Castle.Windsor.IWindsorContainer container)
{
  CacheCow.Server.WebApi.CachingRuntime.RegisterDefaultTypes((
    (t1, t2, isTransient) =>
    {
      if (isTransient)
        container.Register(Component.For(t1).ImplementedBy(t2).LifestyleTransient());
      else
        container.Register(Component.For(t1).ImplementedBy(t2).LifestyleSingleton());
    }));

  container.Register(
    Component.For<CacheCow.Server.ISerialiser>().ImplementedBy<MyJsonSerialiser>().LifestyleTransient()
  );
}

The implementation of WindsorDependencyResolver is a copy of the one in your sample file \samples\CacheCow.Samples.WebApi.WithQueryAndIoc\Program.cs

Still, it does not make the trick and the class being called is your original JsonSerialiser instead of my custom MyJsonSerialiser. Any suggestions? This is my first approach to Castle Windsor (which I found very promising), but I am certainly doing something wrong.

Thanks again for your help and such a good library.

aliostad commented 5 years ago

Hi @jagbarcelo

I was typing while you sent the second one. I was saying that you had better look at the samples, especially CacheCow.Samples.WebApi.WithQueryAndIoc... which seems you already are.

So let me update the samples with your serializer and hopefully this can demonstrate the purpose. It should not be long... lemme do it now

aliostad commented 5 years ago

Hi @jagbarcelo

I just updated the samples project to replace JsonSerialiser with your serialiser:

Component.For<ISerialiser>().ImplementedBy<IgnoreLoopJsonSerialiser>().IsDefault()

Not the IsDefault() while registering which replaces previous registrations. When you run the sample, you will see that the new serialiser is being used.

image

jagbarcelo commented 5 years ago

Yes! IsDefault() did the trick. Thanks!