Try our conversational search powered by Generative AI!

IContentApiModelFilter is always a singleton

Vote:
 

Hello, 

i have a serious issue with dependency injection of ContentApiModelFilters. It seems that it's always using a singleton no matter what lifecycle you use.
We have many filters and all behave as if ServiceInstance.Singleton was used. For example:

[ServiceConfiguration(typeof(IContentApiModelFilter), Lifecycle = ServiceInstanceScope.Scoped)]
public class LinkBlockContentApiModelFilter : ContentApiModelFilter<ContentApiModel>
{
	#region fields

	private readonly IUrlResolver _urlResolver;
	private readonly IContentLoader _contentLoader;
	private readonly ContentResolver _contentResolver; // this is not episerver's
	// and other fields

	#endregion

	#region constructor

	public LinkBlockContentApiModelFilter(IUrlResolver urlResolver, IContentLoader contentLoader, ContentResolver contentResolver, ILogger<LinkBlockContentApiModelFilter> logger)
	{
		_urlResolver = urlResolver;
		_contentLoader = contentLoader;
		_contentResolver = contentResolver;
                // and other fields
	}

	#endregion
	
	#region Filter()

	public override void Filter(ContentApiModel contentApiModel, ConverterContext converterContext)
	{
		// some code using the fields
    }
}
               

So the issue is that we always use the same instance of the fields used via constructor injetion. The ContentResolver for example is responsible to resolve content for a given url(using IUrlResolver.Route). But since we cache that value we always get the same content once it was resolved, on any page. 

Why is it so, are the filters used in a singleton context by episerver?

It also does not make a difference if i register it by myself:

services.AddScoped<IContentApiModelFilter, BreadcrumbContentApiModelFilter>();
services.AddScoped<IContentApiModelFilter, LinkBlockContentApiModelFilter>();
// and so on

Is the only solution to use a workaround like not using constructor injection at all but always use the ServiceProvider?

ContentResolver contentResolver = ServiceLocator.Current.GetInstance<ContentResolver>();
// now work with this which should be a scoped instance

Edit: Unfortunately it is not a solution to just implement IContentApiModelFilter instead of inheriting from ContentApiModelFilter<ContentApiModel> as Eric Herlitz has suggested. Even if you register it manually in Startup.cs via AddScoped you will have a singleton in the filter. The reason i could find out via reflection of the epi source code. ContentConvertingService is a singleton itself:

[ServiceConfiguration(Lifecycle = ServiceInstanceScope.Singleton)]
public class ContentConvertingService
{
    // ...

    public ContentConvertingService(
      //....
      IEnumerable<IContentApiModelFilter> contentApiModelFilters)
    {
      // ...
      this._contentApiModelFilters = contentApiModelFilters;
    }

    public virtual ContentApiModel ConvertToContentApiModel(IContent content, ConverterContext converterContext)
    {
      List<IContentFilter> source = content != null ? this.GetContentFilters(content.GetType()) : throw new ArgumentNullException(nameof (content));
      if (source.Any<IContentFilter>())
      {
        content = ContentConvertingService.TryGetWriteableClone(content);
        source.ForEach((Action<IContentFilter>) (c => c.Filter(content, converterContext)));
        if (content is IReadOnly readOnly)
          readOnly.MakeReadOnly();
      }
      ContentApiModel contentApiModel = this._contentConverterResolver.Resolve(content).Convert(content, converterContext);
      this.GetContentModelFilters(contentApiModel).ForEach(f => f.Filter(contentApiModel, converterContext));
      return contentApiModel;
    }
}

You either have to avoid a singleton in ContentConvertingService or find a way to resolve the filters without using constructor injection to fix this bug. 

#317097
Edited, Feb 13, 2024 15:59
Vote:
 

ContentApiModelFilter is an internal class, while you can use it you shouldn't. Instead rely on IContentApiModelFilter.

I've posted an example implementation here https://world.optimizely.com/forum/developer-forum/Developer-to-developer/Thread-Container/2024/2/what-is-the-best-way-to-add-new-properties-to-the-response-of-the-content-delivery-api/

This can be registered into DI with any lifetime manager that may suit your needs

eg services.AddSingleton<IContentApiModelFilter, PageBaseFilter>();

#317100
Feb 13, 2024 20:46
Tim Schmelter - Feb 14, 2024 8:30
Thank s for the answer. It's not an internal class, but yes, it's in an internal namespace. However, unfortunately it does not work to implement IContentApiModelFilter and register it in Startup.cs via .AddScoped
It is still a singleton so the constructor is called only once.
Edit: I see why it is so, ConvertToContentApiModel uses the filters and is in a singleton itself and the filters are constructor injected:
[ServiceConfiguration(Lifecycle = ServiceInstanceScope.Singleton)]
public class ContentConvertingService

So we have to live with the workaround using ServiceLocator until this bug is fixed.
* You are NOT allowed to include any hyperlinks in the post because your account hasn't associated to your company. User profile should be updated.