How to modify the response of the content delivery api by adding other content's models?

Vote:
 

Hello,

I need to modify the content api model if a page contains a spefic "Job filter"-block. In that case all jobs on the site must be added to the model under the meta data of that block.

So my idea was to create a custom ContentApiModelFilter<ContentApiModel> and check if the current content is of type JobFilterBlock. If so, i add as a new property "Jobs" the content api models of all jobs. But the problem:

  • If i use ContentConvertingService this is a circular dependency because  ContentConvertingService uses(calls) the filters at the end. So i would create an infinite loop.

What is the corect way? Following would run(Lazy<ContentConvertingService> to avoid already an exception in dependency injection), but indeed crashes because it is an infinite loop:

[ServiceConfiguration(typeof(IContentApiModelFilter), Lifecycle = ServiceInstanceScope.Singleton)]
public class JobFilterContentApiModelFilter : ContentApiModelFilter<ContentApiModel>
{
	#region constants

	private const string _PROPERTYNAME_JOBS = "Jobs";

	#endregion

	#region fields

	private readonly IContentLoader _contentLoader;
	private readonly Lazy<ContentConvertingService> _lazyContentConvertingService;
	private readonly IJobPageFinder _jobPageFinder;

	#endregion

	#region constructor

	/// <summary>
	/// Creates a new object of type <see cref="JobFilterContentApiModelFilter"/>.
	/// Is called automatically by the system.
	/// </summary>
	/// <param name="contentLoader">The <see cref="IContentLoader"/> instance to load content information</param>
	/// <param name="jobPageFinder">The <see cref="IJobPageFinder"/> instance to resolve all jobs of the site's job-portal</param>
	/// <param name="services">Service to get an instance of <see cref="Lazy&lt;ContentConvertingService&gt;"/> to resolve all jobs of the site's job-portal</param>
	public JobFilterContentApiModelFilter(
		IContentLoader contentLoader, 
		IJobPageFinder jobPageFinder,
		IServiceProvider services)
	{
		_contentLoader = contentLoader;
		_lazyContentConvertingService = new Lazy<ContentConvertingService>(services.GetRequiredService<ContentConvertingService>);
		_jobPageFinder = jobPageFinder;
	}

	#endregion

	#region Filter-method

	/// <summary>
	/// The main filter method. In this method, we check if the content is a <see cref="JobFilterBlock"/>.
	/// In that case all existing job pages are added to the contentApiModel.
	/// The filtering is done on client-side.
	/// </summary>
	/// <param name="contentApiModel">
	/// The current <see cref="ContentApiModel"/> we'll be adding the job-pages to
	/// </param>
	/// <param name="converterContext">The converter context with information about the current content</param>
	public override void Filter(ContentApiModel contentApiModel, ConverterContext converterContext)
	{
		if(!_contentLoader.TryGet(converterContext.ContentReference, out IContent? content) 
		   || content is not JobFilterBlock jobFilter)
		{
			return;
		}

		
		// this is a JobFilterBlock, add all job-pages models to the contentApiModel
		List<object> allJobPageModels = GetAllJobPageModels(converterContext);

		if (!allJobPageModels.Any())
		{
			return;
		}

		contentApiModel.Properties[_PROPERTYNAME_JOBS] = allJobPageModels;
	}

	#endregion


	#region private methods

	/// <param name="converterContext"></param>
	/// <returns>A List containing all job-pages' api-models</returns>
	private List<object> GetAllJobPageModels(ConverterContext converterContext)
	{
		IList<JobPage> allJobs = _jobPageFinder.GetAllJobPages();
		List<object> jobModels = new(allJobs.Count);
		jobModels.AddRange(allJobs.Select(GetJobApiModel));
		return jobModels;

		object GetJobApiModel(JobPage job)
		{
			return _lazyContentConvertingService.Value.Convert(job, converterContext);
		}
	}

	#endregion
}
#311881
Edited, Nov 02, 2023 17:34
Vote:
 

Your class look a bit suspicious to me.

JobFilterContentApiModelFilter should implement IContentApiModelFilter and be injected, eg

// NO ATTRIBUTES HERE
public class JobFilterContentApiModelFilter : IContentApiModelFilter
{
	private readonly IContentLoader _contentLoader;
	private readonly ContentConvertingService _contentConvertingService; // or possibly IContentConverterResolver?
	private readonly IJobPageFinder _jobPageFinder;
	private readonly IServiceProvider _services;

	public JobFilterContentApiModelFilter(
		IContentLoader contentLoader, 
		IJobPageFinder jobPageFinder,
		ContentConvertingService contentConvertingService
		IServiceProvider services)
	{
		_contentLoader = contentLoader;
		_contentConvertingService = contentConvertingService;
		_jobPageFinder = jobPageFinder;
		_services = services;
	}
}

Inject in startup

services.AddSingleton<IContentApiModelFilter, JobFilterContentApiModelFilter>();

And work your way from there.

#311924
Edited, Nov 03, 2023 8:36
Tim Schmelter - Nov 03, 2023 9:11
Thanks, i will try it. But will it really solve the issue that the filter uses ContentConvertingService.Convert which uses the filter(incl. JobFilterContentApiModelFilter) that uses ContentConvertingService.Convert which uses the filter that uses the ContentConvertingService.... ? So an infinite loop.

The question is: I want the api model of the JobPage, not from the current JobFilterBlock, why is it an infinite loop at all? Why is onverterContext.ContentReference again JobFilterBlock even if i pass the JobPage to ContentConvertingService.Convert which does not contain a JobFilterBlock?

Edit: Why you removed the attributes? I was working without, otherwise i need to register this somewhere else and i don't know where and how.
By the way: ContentConvertingService does not implement any interface

Edit2: Found a solution and added as answer :)
Vote:
 

I found the answer myself, thanks @Eric for your assistance. The root issue is that there is a circular dependency between ContentApiModelFilter and ContentConvertingService. The latter is using the former to filter the contentApiModel at the end. But my implementation needs the ContentConvertingService to get the models of each JobPage.

  • So first ContentConvertingService must be lazy to avoid a dependency injection issue which does not allow circular dependencies. 
  • The next challenge(my question here) was that there was an infinite loop since my filter calls the converting-service that uses the filter that uses the converting-service ...
    • The solution is to create a new ConverterContext, one for each JobPage. Look at GetAllJobPageModels:
[ServiceConfiguration(typeof(IContentApiModelFilter), Lifecycle = ServiceInstanceScope.Singleton)]
public class JobFilterContentApiModelFilter : IContentApiModelFilter
{
	#region constants

	private const string _PROPERTYNAME_JOBS = "Jobs";
	private const string _EXPAND_ALL_PROPERTIES = "*";

	#endregion

	#region fields

	private readonly IContentLoader _contentLoader;
	private readonly Lazy<ContentConvertingService> _lazyContentConvertingService;
	private readonly IJobPageFinder _jobPageFinder;

	#endregion

	#region constructor

	/// <summary>
	/// Creates a new object of type <see cref="JobFilterContentApiModelFilter"/>.
	/// Is called automatically by the system.
	/// </summary>
	/// <param name="contentLoader">The <see cref="IContentLoader"/> instance to load content information</param>
	/// <param name="jobPageFinder">The <see cref="IJobPageFinder"/> instance to resolve all jobs of the site's job-portal</param>
	/// <param name="services">Service to get an instance of <see cref="Lazy&lt;ContentConvertingService&gt;"/> to resolve all jobs of the site's job-portal</param>
	public JobFilterContentApiModelFilter(
		IContentLoader contentLoader,
		IJobPageFinder jobPageFinder,
		IServiceProvider services)
	{
		_contentLoader = contentLoader;
		_lazyContentConvertingService =
			new Lazy<ContentConvertingService>(services.GetRequiredService<ContentConvertingService>);
		_jobPageFinder = jobPageFinder;
	}

	#endregion

	#region properties

	Type IContentApiModelFilter.HandledContentApiModel => typeof(ContentApiModel);

	#endregion

	#region Filter-method

	/// <summary>
	/// The main filter method. In this method, we check if the content is a <see cref="JobFilterBlock"/>.
	/// In that case all existing job pages are added to the contentApiModel.
	/// The filtering is done on client-side.
	/// </summary>
	/// <param name="contentApiModel">
	/// The current <see cref="ContentApiModel"/> we'll be adding the job-pages to
	/// </param>
	/// <param name="converterContext">The converter context with information about the current content</param>
	public void Filter(ContentApiModel contentApiModel, ConverterContext converterContext)
	{
		if (!_contentLoader.TryGet(converterContext.ContentReference, out IContent? content)
			|| content is not JobFilterBlock jobFilter)
		{
			return;
		}


		// this is a JobFilterBlock, add all job-pages models to the contentApiModel
		List<object> allJobPageModels = GetAllJobPageModels(converterContext);

		if (!allJobPageModels.Any())
		{
			return;
		}

		contentApiModel.Properties[_PROPERTYNAME_JOBS] = allJobPageModels;
	}

	#endregion


	#region private methods

	/// <param name="converterContext"></param>
	/// <returns>A List containing all job-pages' api-models</returns>
	private List<object> GetAllJobPageModels(ConverterContext converterContext)
	{
		IList<JobPage> allJobs = _jobPageFinder.GetAllJobPages();
		List<object> jobModels = new(allJobs.Count);
		jobModels.AddRange(allJobs.Select(GetJobApiModel));
		return jobModels;

		object GetJobApiModel(JobPage job)
		{
			return _lazyContentConvertingService.Value.Convert(job, GetJobContext(job));
		}

		ConverterContext GetJobContext(JobPage job)
		{
			return new ConverterContext(
				job.ContentLink,
				converterContext.Language,
				converterContext.Options,
				converterContext.ContextMode,
				null,
				_EXPAND_ALL_PROPERTIES,
				converterContext.ExcludePersonalizedContent
			);
		}
	}

	#endregion
}
#311929
Edited, Nov 03, 2023 11:12
Eric Herlitz - Nov 03, 2023 11:38
Awesome!
This topic was created over six months ago and has been resolved. If you have a similar question, please create a new topic and refer to this one.
* 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.