Blog posts by Minesh Shah2024-03-06T20:29:56.0000000Z/blogs/Minesh-Shah/Optimizely WorldExploring SaaS CMS: API Clients and Content Management APIs/blogs/Minesh-Shah/Dates/2024/3/exploring-saas-cms-api-clients-and-content-management-apis/2024-03-06T20:29:56.0000000Z<h2>Introduction</h2>
<p>In continuation of my <a href="/link/807ad30b59c140a6baec9c7633a5a256.aspx">previous post</a> on leveraging the Content Management API and OpenID Connect Authentication on the PaaS-based Optimizely CMS, I delve into the delivery mechanisms within the SaaS-based CMS Platform. Surprisingly, the majority of functionalities are readily available and seamlessly integrated into the system. In this article, I provide a quick preview of the available features and guide on configuring the exposed API for definition and content management.</p>
<p><strong>Note:</strong> It's essential to bear in mind that while I explore these features, the SaaS platform is still in its BETA phase, and APIs are currently at version 0.5. Changes might occur as the SaaS CMS transitions into general availability.</p>
<h2>Configuring the API Client (Equivalent to OpenID Connect Package)</h2>
<p>The API client functionality comes pre-installed in the SaaS CMS, and the setup is near identical to what we found on the PaaS platform with the Open ID Connect package. The tool can be found within Settings and the Access Rights section as highligted in the image below. </p>
<p>Once on the API Client interface we can create a Client ID, the Client Secret is automatically generated please store this safely as there is no way of retrieving once you leave the page. The option to "Allow the client to impersonate users" is self-explanatory; enabling this allows the client to function as another user within the CMS.</p>
<p><img src="/link/2a7d3af9b80749d189b9c3ddc6fa2287.aspx" /></p>
<h3>Using the credentials</h3>
<p>I'll demonstrate how to utilize these credentials using Postman to retrieve a JWT token for subsequent API calls. To obtain a Bearer Auth Token, a GET call needs to be made to the designated URL:</p>
<pre class="language-markup"><code>https://app-xxxprod.cms.optimizely.com/_cms/v0.5/oauth/token</code></pre>
<p><span>Sending the required parameters along with the request, including </span><code>grant_type</code><span>, </span><code>client_id</code><span>, </span><code>client_secret</code><span>, and optionally </span><code>act_as</code><span>, will result in the generation of a token for future requests. Notably, this token expires automatically after 300 seconds.</span></p>
<p><span>Example: </span></p>
<p><span><img src="/link/afad4afd59dd4d66b7dc4837d4a088e6.aspx" /></span></p>
<h2>Authorisation to API using the Bearer Token</h2>
<p>With the Bearer Token generated, subsequent API requests can now be authenticated by passing this token as the "Authorization" Header parameter.</p>
<p>Example:</p>
<p><img src="/link/73a267ca07894182883fda04000e5583.aspx" /></p>
<h2>Content Definitions API</h2>
<p><span>Now armed with the bearer token, we can interact with the Content and Definitions API. The first API we explore is the Content Definitions API. Detailed API reference can be found </span><a href="https://docs.developers.optimizely.com/content-management-system/v1.0.0-SaaS-Core/reference/contenttypes_list">here</a><span>.</span></p>
<h3>Get Content Types</h3>
<p><span>A simple GET request to the following URL provides a list of all content types within the CMS:</span></p>
<pre class="language-markup"><code>https://app-xxxprod.cms.optimizely.com/_cms/v0.5/contenttypes</code></pre>
<p>Example: </p>
<p><img src="/link/c9015e217b714b16ba0c466475e46fa2.aspx" /></p>
<p>To only get the details of a certain known type we pass in the definition name (key) to the URL:</p>
<pre class="language-markup"><code>https://app- xxxprod.cms.optimizely.com/_cms/v0.5/contenttypes/articlepage</code></pre>
<h3>Create Content Type</h3>
<p>To create a content type, a POST request is made to the same URL, passing in the necessary parameters.</p>
<p>Example: </p>
<p><img src="/link/593612ffa8464867aa73b1f11a9f202f.aspx" /></p>
<p><img src="/link/1c718e89bd7144c696bb0078b05f4c24.aspx" /></p>
<h2>Content {Delivery} API</h2>
<p>The API reference to the Content API can be found here : <a href="https://docs.developers.optimizely.com/content-management-system/v1.0.0-SaaS-Core/reference/content_create">Create content (optimizely.com)</a></p>
<h3>Get Content</h3>
<p>To retrieve a content item, a GET request is made to the designated URL, using the Guid of the page as the key. The key is in a UUID format so should not include any dashes e.g. 115988243510434482925671c3ee601a</p>
<pre class="language-markup"><code>https://app- xxxprod.cms.optimizely.com/_cms/v0.5/content/{key}</code></pre>
<p>Example: </p>
<p><img src="/link/418614fbd0f245c4b586f1b1ca1ea884.aspx" /></p>
<h2>Conclusion</h2>
<p>As you can see its very easy to interact with the API’s and retrieve the relevant information you may need, as well as programmatically being able to create Content Models and Instances of these models. Its great to see this has all been included from the get go and provides a lot of scope to decide on how we manage the content definition creation process.</p>A Quick Guide to Using the Content Management API and OpenID Connect Authentication/blogs/Minesh-Shah/Dates/2023/11/a-quick-guide-to-using-the-content-management-api-and-openid-connect-authentication/2023-11-01T22:44:53.0000000Z<h2>Introduction</h2>
<p>Content management in Optimizely CMS becomes more efficient and streamlined with the power of the <strong>Content Management API</strong>.</p>
<p>The<span> </span><strong>Optimizely Content Management API<span> </span></strong>adds REST endpoints for basic content management operations such as:</p>
<ul>
<li>Create content objects</li>
<li>Modify existing content objects</li>
<li>Delete content objects</li>
<li>Get draft content objects</li>
<li>Move content objects</li>
</ul>
<p>The REST API is useful for pushing external content to Optimizely without having to deploy custom integration code to the Optimizely Content Management System (CMS).</p>
<p>In this quick guide, I'll walk through the steps to set up a solution for content creation using the API.</p>
<h2>Prerequisites</h2>
<p>Before diving into the process, ensure you have the following:</p>
<ul>
<li>An active Optimizely CMS instance (For Alloy site setup, refer to my guide <a href="/link/9eaccb8259bf485296a54407dc071768.aspx">here</a>).</li>
<li>Familiarity with an API request tool like Postman.</li>
<li>A good understanding of your CMS's content structure and types.</li>
</ul>
<h2>Step 1: Install and Configure the Content Management API</h2>
<p><span>First, let's set up the Content Management API. If you're using Visual Studio, you can install it via the Package Manager or via the CLI using this command:</span></p>
<pre class="language-csharp"><code>dotnet add package EPiServer.ContentManagementApi</code></pre>
<p><span>Once installed, modify the Startup Class to include the basic configuration. In the </span><code>ConfigureServices</code><span> method, add the following line:</span></p>
<pre class="language-csharp"><code>services.AddContentManagementApi(o => o.DisableScopeValidation = true);</code></pre>
<p>Then, build and run your solution with:</p>
<pre class="language-csharp"><code>dotnet run</code></pre>
<h2>Step 2: Test the Endpoint</h2>
<p><span>After installing the Content Management API, you can test the endpoint using Postman. Send a simple POST request to </span><code>/api/episerver/v3.0/contentmanagement</code><span>. As we haven't added any parameters, expect a 400 Bad Request status code with a specific JSON response.</span></p>
<pre class="language-markup"><code>{
"errors": {
"filename": [
"The filename field is required."
],
"content-Type": [
"The contentType field is required."
]
},
"type": "https://tools.ietf.org/html/rfc7231#section-6.5.1",
"title": "One or more validation errors occurred.",
"status": 400,
"traceId": "00-b2f4877cb70b04fc20ea6fc0dfe1d2fe-d37b0cf2e4dc9458-00",
"code": "InvalidModel"
}
</code></pre>
<p><span>This response tells us the endpoint is operational. Alternatively, it would have resulted in a 404 status code. At this stage, we can try adding content to Optimizely CMS via Postman. Use the provided JSON body, but you'll likely encounter a 401 status due to permissions.</span></p>
<pre class="language-markup"><code>{
"name": "Text Block (Content Management API)",
"language": {
"name": "en"
},
"contentType": [
"Block",
"EditorialBlock"
],
"parentLink": {
"id": "33"
},
"mainBody": {
"value": "<p>Hello World</p>"
},
"status": "Published"
}
</code></pre>
<p>Response Example</p>
<pre class="language-markup"><code>{
"type": "https://tools.ietf.org/html/rfc7235#section-3.1",
"title": "Unauthorized",
"status": 401,
"detail": "Access was denied to content 33. The required access level was \"Create, Publish\".",
"instance": "/api/episerver/v3.0/contentmanagement",
"traceId": "00-c91883ef32f39b8dda0571df62c44b7b-d48fe0471f6254da-00"
}
</code></pre>
<p><span>For testing purposes, you can temporarily grant access rights to "Everyone" for creating and publishing pages. After making this change, re-run the request. You should now receive a 201 (Success) status code with the content successfully created in CMS.</span></p>
<p><span><img src="/link/17191f74030b45d2890b9a429189d49c.aspx" /></span></p>
<p><span><img src="/link/6bec4a590ea3429eab25c352fb109049.aspx" /></span></p>
<h2>Step 3: API Authentication via OpenID Connect</h2>
<p>In the real world, giving "Everyone" access to publish content is not acceptable. To ensure authorized individuals/services create and publish content, we need to authenticate Content Management API requests using OpenID Connect and Bearer Tokens (JWT). Here's how:</p>
<ul>
<li>Install the OpenIDConnect NuGet package with this command:
<pre class="language-csharp"><code>dotnet add package EPiServer.OpenIDConnect.UI</code></pre>
</li>
<li><span><span>Amend the Startup class to configure this package and make Content Management API support it. Update the code as follows:</span></span>
<pre class="language-csharp"><code>services.AddContentManagementApi(OpenIDConnectOptionsDefaults.AuthenticationScheme, options =>
{
options.DisableScopeValidation = true;
});
services.AddOpenIDConnect<ApplicationUser>(
useDevelopmentCertificate: true,
signingCertificate: null,
encryptionCertificate: null,
createSchema: true
);
services.AddOpenIDConnectUI();
</code></pre>
</li>
<li><span>After saving the file, build and run the solution and access Optimizely Admin mode. Look for "OpenID Connect" in Settings, create a new application (set Scopes as "epi_content_management").</span><span><img src="/link/4a8cd7fe899141e49f8d4b6e62c806ec.aspx" /></span></li>
<li><span>Test if it all works by making a POST request in Postman to <code>/api/episerver/connect/token</code> with the following parameters:</span>
<ul>
<li>client_id: api-client</li>
<li>client_secret: SuperSecret</li>
<li>grant_type: client_credentials</li>
</ul>
</li>
<li><span>You'll receive a time-limited access token.</span><span><img src="/link/f2faa2d4556842c28c6ce3ae081acd34.aspx" /></span></li>
</ul>
<p><span>Having got this far and sucessfully being able to generate a bearer token, we have to grant our newly created client permissions to create and publish content, we do this by managing access rights in Optimizely. Remove additional rights given to the "Everyone" group and add them to the <code>api-client</code> with the same rights instead. </span></p>
<p><span><img src="/link/57750409614146909e5d1a7d5fb85875.aspx" /></span></p>
<p>Finally, copy the access token from Postman and make another POST request to <code>/api/episerver/v3.0/contentmanagement</code>, this time also setting the Authorization Token.</p>
<p><img src="/link/f5558b9a21b44378aaf97fa3d41bc492.aspx" /></p>
<p><img src="/link/26638829b9a54d08b457425e8a539d74.aspx" /></p>
<p>That's it! We're now securely creating content via the Content Management API using a JWT Bearer Token.</p>
<p><img src="/link/58c2c1319e4e45d99bfaf0c15bf50040.aspx" /></p>
<p>** For additional Security measures you can add some additional CORS policies to your Optimizely Solution so only certain services can call the Content Management API requests or even lock the API down via IP Restrictions. </p>
<h2>Conclusion</h2>
<p>The Content Management API alongside Content Definitions API are quite powerful tools, they are not difficult to setup and have many use cases for example with the Content Management API we can quite easily utilise for migration processes and import a large amount of content into the CMS in bulk all programmatically. With the recent announcement of the SaaS CMS offering the Content Definitions API is quite useful in creating Content Types and something that can quite easily be source controlled, so definitely something to look out for and try-out. </p>
<h3>References </h3>
<ol>
<li><a href="https://docs.developers.optimizely.com/content-management-system/v1.6.0-content-management-api/docs">Get started with Content Management API (optimizely.com)</a></li>
<li><a href="https://docs.developers.optimizely.com/content-management-system/v1.5.0-content-delivery-api/docs/api-authentication">API authentication (optimizely.com)</a></li>
<li><a href="https://docs.developers.optimizely.com/content-management-system/v1.6.0-content-management-api/docs/api-fundamentals">Examples of Content Management API (optimizely.com)</a></li>
</ol>Insights from Setting up and Getting Started with SaaS Core [Beta]/blogs/Minesh-Shah/Dates/2023/10/insights-from-setting-up-and-getting-started-with-saas-core/2023-10-23T15:36:59.0000000Z<h2>Introduction</h2>
<p>Shortly after Opticon San Diego OMVPs were given a chance to test drive and evaluate the SaaS Core CMS [Beta]. Having seen David Knipe’s demo in one of the breakout sessions I initially felt this should be plain sailing how wrong I was, fully expected considering it is in early Beta.</p>
<p>Below I write about some of the pain points I found which will hopefully help the wider community avoid the issues I faced, and hopefully get to a fully working solution a lot quicker. </p>
<h2>Initial Setup</h2>
<p>So like David I wanted to utilise Remko’s Next.JS Presentation Layer and CMS Content Types, this is based on Mosey Bank Demo Templates. The Git Repository for this can be found <a href="https://github.com/episerver/cms-saas-vercel-demo/">here</a>.</p>
<p>Firstly, I started by Importing the Content Definitions and Content Instances into Optimizely, the import file is called InitialData.episerverdata. </p>
<h2>Optimizely Graph Synch Issues</h2>
<p>The content imported just fine although indexing this to Optimizely Graph was not working, later it was found that one of the properties utilised “<strong>LinkItem</strong>” property was not being serialized properly which was causing the whole indexing Job to fail. This is a known Bug and Optimizely are currently working on rectifying this.</p>
<pre class="language-markup"><code>There's an issue during contents are being indexed: An error while sync content. Status: 0, message: Error when building index operation for contentIds: 27_30., An error while sync content. Status: 0, message: Error when building index operation for contentIds: 84_89.. (see log for more information)</code></pre>
<p>I felt the best way to resolve the issue would be to delete the properties from the Content Types: </p>
<ul>
<li>Button</li>
<li>Hero Banner</li>
<li>Menu Item</li>
<li>Website Footer</li>
</ul>
<p>Upon doing so my Content Graph Indexing Job ran thru fine. <em>(This would cause knock on affects with my presentation layer as the property is expected to be present in templates and graph more on this later).</em></p>
<p><em><img src="/link/f151a4e58dc6423891f62759db6b46b3.aspx" /></em></p>
<h2>Vercel Deployment</h2>
<p>Ok so now we have Content in CMS and the Graph Indexing Job looks to be working it was time to move onto deploying the presentation layer. For this we need to have a Git and Vercel account (Hobby account as a bare minimum).</p>
<p>Navigating to Remok’s Git repo you will see the following Deploy button, simply click this to start the process.</p>
<p><img src="/link/b4a92534275b41d99744e14d035bb189.aspx" /></p>
<p>When configuring the project you will need to populate some of the Optimizely Graph Security Tokens and DXP URL, these can be found in the Dashboard tab within Optimizely and the DXP URL will be the domain address you see in CMS e.g. <a href="https://app-ocxcxxxx122w0uprod.cms.optimizely.com/">https://app-ocxcxxxx122w0uprod.cms.optimizely.com/</a></p>
<p><img src="/link/d59206291aec4c2f841e904618e33371.aspx" /></p>
<p>So, my first run of this did not work the build process failed due to missing properties of course I should have known this having deleted the LinkItem properties earlier.</p>
<p>Rather than amend the frontend templates I added the LinkItem properties back into the Content Types ensuring they were not populated, this meant the serialization process for this property would not break.</p>
<p>On my next run the build process did get further although I was getting errors on all of my landing pages which stated the following:</p>
<pre class="language-markup"><code>Error occurred prerendering page "/en/services". Read more: https://nextjs.org/docs/messages/prerender-error
Error: Expected to load exactly one content item, received 0 from Optimizely Graph.
at cms_content_CmsContent (/vercel/path0/apps/frontend/.next/server/chunks/998.js:197:2804)
at process.processTicksAndRejections (node:internal/process/task_queues:95:5)
at async /vercel/path0/apps/frontend/.next/server/chunks/998.js:197:4372
at async Promise.all (index 0)
at async CmsContentArea (/vercel/path0/apps/frontend/.next/server/chunks/998.js:197:3977)</code></pre>
<p>Having tried to resolve this issue via Content update on the specific Page Instances, I gave up as nothing I was changing in the Content was making an affect with the Build Process. Not getting far I thought it would be best to clear out all the content from Optimizely and start again with just a single Homepage and no block instances.</p>
<p>e.g. </p>
<p><img src="/link/1311d3d7d2a242c6b179c7bc7193d12a.aspx" /></p>
<p>I then deleted my Graph Index and re-ran the Indexing Job. After a successful Index I re-ran the deployment via Vercel and this time it was sucessful and a skeleton homepage was presented</p>
<p><img src="/link/db01853f1eab4ada9439e1c1e060c3df.aspx" /></p>
<h2>On Page Editing</h2>
<p>With my homepage loading I decided to add some content to the content area like a simple Rich Text Block into the Main Content Area, initially I created this as an <strong>Inline Block</strong> which did not work, the template or graph currently cant handle this and it is something Optimizely are working on rectifying. Having changed the Inline Block to a Shared Block the content was rendering just fine on my Homepage.</p>
<p>My next steps were to try and get the On Page Editing aspect of the CMS working for this I had to set the Hostnames up correctly within Manage Websites, my Edit Host is the SaaS CMS and Primary Host is the Vercel Front end i.e.</p>
<p><img src="/link/5b75d4f6f3c9476c89c561826db86185.aspx" /></p>
<p>Navigating to the CMS “On Page Edit” mode I assumed it would just work like shown in the Demo, to my dissmay I was presented with a 404 status code. After lots of digging and discussing with Optimizely it turned out they had turned off ContentReference ID in Optimizely Graph, thus the presentation layer could not route to the correct page to display the On Page edit view. Luckily after a day they re-enabled the numerical identifiers I cleared and re-indexed Opti Graph went back to On Page Edit Mode, and everything was now working.</p>
<p><img src="/link/27ff0d99a26c4907bd6c3bf09f18e85e.aspx" /></p>
<h2>Summary</h2>
<p>Despite several challenges, I was happy to get the solution working including the On Page Editing aspect. If trying yourself after importing the Initial Data I would delete all the Content in Page Tree and Assets Folder and start building the content up once you have a successful deployment. <span>Some known bugs, such as Link Item Serialization and issues with Inline Blocks, require further investigation. Overall, I'm optimistic about the potential of the SaaS CMS Core and look forward to its evolution.</span></p>Responsive Image Rendering at the Edge/blogs/Minesh-Shah/Dates/2023/10/responsive-image-rendering-at-the-edge/2023-10-12T19:29:24.0000000Z<h2><span>Dynamic Image Resizing</span></h2>
<p><span>At Netcel, we recognize the significance of delivering high-performance solutions that enhance user experience and contribute to better Google rankings.</span></p>
<p><span>To achieve this, we prioritize serving optimized images tailored to a user's viewport size, commonly referred to as responsive images. Our approach involves advising our clients to upload the highest quality image available, while we take care of dynamically resizing and serving the appropriate version to end users.</span></p>
<p><span>Here's an example of what the image tag would resemble in the HTML source:</span></p>
<p><span><img src="/link/e783f492cf3e4d138cc59ef74b9a8060.aspx" width="561" height="374" /></span></p>
<p><span>In the given example, the image file "about-intro-people-collage-4.jpg" represents the original high-quality image. The srcset attribute complements the sizes attribute by specifying various image options with their corresponding widths. Together, these attributes determine the appropriate sizes of the image for different viewport widths, display pixel density and layout.</span></p>
<p><strong>Seeing this in Action:</strong></p>
<p><strong>Small viewport (320 pixels width)</strong></p>
<p><span><img src="/link/7462a4ead0c84994aaf892603408822f.aspx" /></span></p>
<p><strong>Medium viewport (768 pixels width)</strong></p>
<p><span><img src="/link/f4b559c48b434092909d9bef0ecbb4e5.aspx" /></span></p>
<p><strong>Large viewport (1200 pixels width)</strong></p>
<p><span><img src="/link/a74be68970c2444a881e878a3610c19a.aspx" /></span></p>
<p><span>As you can see with each request at different viewports the images returned were different in both storage size and width this in return meant the time taken to load was also different. </span></p>
<p><span>To achieve dynamic resizing and optimization of images, it is common to employ specialized tools and libraries that can handle the processing on the server side. Some popular examples of such tools are Image Resizer, Image Processor, and Six Labors ImageSharp.</span></p>
<p>Here's how the process typically works:</p>
<ol>
<li><strong>Image Resizing and Optimization:</strong><br />
<p>These tools provide APIs or server-side libraries that can be integrated into your web application. When a request is made to an image, the server-side code utilizes these tools to resize and optimize the image on-the-fly based on the requested dimensions and optimization settings. This ensures that the image is served in the most appropriate format, size, and quality for the specific user's device and viewport.</p>
</li>
<li><strong>Content Delivery Network (CDN) Caching:</strong>
<p>Once an image is processed and served by the server, it is often beneficial to cache the optimized image in a Content Delivery Network (CDN). CDNs are distributed networks of servers located worldwide, designed to deliver content with low latency. By caching the optimized image in the CDN, subsequent requests for the same image can be served directly from the CDN servers, reducing the load on the origin server and improving response times for users across different regions.</p>
</li>
</ol>
<p>The advantage of this approach is that the image processing and optimization tasks are performed dynamically on the server side, enabling flexibility and adaptability to different image requests. Once the image is processed and cached in the CDN, subsequent requests for the same image can be efficiently served directly from the CDN, reducing the need for repeated processing on the server.</p>
<h2>Edge Resizing and .NET 6 Tag Helpers in Action</h2>
<p>With the evolution to CMS12, we encountered challenges with compatibility and licensing agreements for the image optimization tools we previously used, especially in the context of .NET 6+ compatibility. We sought a solution that would alleviate the need for server-side processing and overcome these hurdles. Fortunately, Optimizely with Cloudflare are now enabling, "Image resizing at the edge for CMS and Commerce." (Beta)</p>
<p>This innovative feature from Optimizely allows us to leverage Cloudflare's powerful edge platform to transform images directly at the edge. It eliminates the necessity for additional tooling within our solution, removing the optimization burden from our servers entirely. Now, we can seamlessly resize, adjust quality, and even convert images to next-generation formats on demand.</p>
<p>The HTML tags on the front end remain unchanged. However, instead of specifying the image widths as query string parameters, we construct the image URL in a specific format to leverage Cloudflare's functionality for resizing and serving the appropriate image. For example, we might use a URL structure like "<strong>/cdn-cgi/image/width=80/siteassets/test-folder/demo.jpg</strong>" to indicate that we want Cloudflare to resize and deliver the image with a width of 80 pixels.</p>
<h3>Model Attribution + Tag Helper</h3>
<p>How we manage this all via code and attribute our Image Content References with the Width and Sizes attributes is quite very simple, here is an example of an Image on a Promo Block </p>
<pre class="language-csharp"><code> [Display(
Name = "Image",
Description = "Image",
GroupName = TabsGroups.Content,
Order = 100)]
[CultureSpecific]
[UIHint(UIHint.Image)]
[ImageSize(Width = 210)]
[ImageSize(Width = 280)]
[ImageSize(Width = 335)]
[ImageSize(Width = 420)]
[ImageSize(Width = 452)]
[ImageSize(Width = 526)]
[ImageSize(Width = 550, IsDefault = true)]
[ImageSize(Width = 560)]
[ImageSize(Width = 630)]
[ImageSize(Width = 670)]
[ImageSize(Width = 840)]
[ImageSize(Width = 904)]
[ImageSize(Width = 1005)]
[ImageSize(Width = 1052)]
[ImageSize(Width = 1100)]
[ImageSize(Width = 1356)]
[ImageSize(Width = 1578)]
[ImageSize(Width = 1650)]
[ImageSizes(Sizes = "(min-width: 1280px) 550px, (min-width: 1024px) calc(100vw - 120px) / 2, (min-width: 768px) calc(100vw - 80px) / 2, calc(100vw - 40px)")]
[Required]
public virtual ContentReference PromoImage { get; set; }</code></pre>
<p>We have then created a simple Tag Helper to read these attributes and generate the Image Tag for us, some caveats to remember, resizing at the edge only works when routing via the CDN so not locally, also not when in the Optimizely Edit Interface. We have fallen back to not resizing in these scenarios and can be seen below. </p>
<pre class="language-csharp"><code>[HtmlTargetElement("img", Attributes = "image-for")]
public class EdgeImageTagHelper : TagHelper
{
private readonly IUrlResolver _urlResolver;
private readonly IContentLoaderService _contentLoader;
private readonly IWebHostEnvironment _webHostEnvironment;
public ModelExpression ImageFor { get; set; }
public string CssClass { get; set; }
public string Fit {get; set; }
public EdgeImageTagHelper(IUrlResolver urlResolver, IContentLoaderService contentLoader, IWebHostEnvironment webHostEnvironment)
{
_urlResolver = urlResolver;
_contentLoader = contentLoader;
_webHostEnvironment = webHostEnvironment;
}
public override void Process(TagHelperContext context, TagHelperOutput output)
{
if (ImageFor.Model is not ContentReference edgeImage) return;
if(ContentReference.IsNullOrEmpty(edgeImage)) return;
if(!_contentLoader.TryGet(edgeImage, out ImageFile imageFile)) return;
var actualUrl = _urlResolver.GetUrl(edgeImage);
var property = ImageFor
.Metadata
.ContainerType
.GetProperties()
.FirstOrDefault(x => x.Name == ImageFor.Metadata.PropertyName);
if (property == null)
{
RenderNonResponsiveImage(output, actualUrl);
return;
}
var shouldSkipEdgeResizing = _webHostEnvironment.IsDevelopment() || _contentLoader.IsContentInEditMode;
var imageSize = property.GetCustomAttributes<ImageSizeAttribute>().ToList();
var imageSizes = property.GetCustomAttribute<ImageSizesAttribute>();
var defaultImageSize = imageSize.FirstOrDefault(x => x.IsDefault);
if (defaultImageSize == null)
{
RenderNonResponsiveImage(output, actualUrl);
return;
}
var srcSetUrls = imageSize
.OrderBy(x => x.Width)
.Select(s => GetEdgeUrl(actualUrl, s.Width, shouldSkipEdgeResizing)).ToList();
SetAttribute(output, "src", GetEdgeUrl(actualUrl, defaultImageSize.Width, shouldSkipEdgeResizing, true));
SetAttribute(output, "srcset", string.Join(",", srcSetUrls));
if (imageSizes != null) SetAttribute(output, "sizes", imageSizes.Sizes);
SetAttribute(output, "alt", imageFile.AlternativeText);
SetAttribute(output, "loading", "lazy");
SetAttribute(output, "decoding", "auto");
SetAttribute(output, "fetchpriority", "auto");
SetAttribute(output, "class", CssClass);
if (imageFile is not IHasPixelSize pixelSize) return;
if (pixelSize.Width > 0)
{
SetAttribute(output,"width", pixelSize.Width.ToString());
}
if (pixelSize.Height > 0)
{
SetAttribute(output, "height", pixelSize.Height.ToString());
}
}
private void RenderNonResponsiveImage(TagHelperOutput output, string url)
{
SetAttribute(output, "src", url);
SetAttribute(output, "class", CssClass);
}
private string GetEdgeUrl(string actualUrl, int width, bool isLocal, bool isDefault = false)
{
if (!isLocal)
{
return isDefault
? $"/cdn-cgi/image/{GetEdgeImageOptions(width)}{actualUrl}"
: $"/cdn-cgi/image/{GetEdgeImageOptions(width)}{actualUrl} {width}w";
}
else
{
return isDefault
? $"{actualUrl}?{GetLocalImageOptions(width)}"
: $"{actualUrl}?{GetLocalImageOptions(width)} {width}w";
}
}
private string GetLocalImageOptions(int width)
{
var options = $"width={width}";
if (!string.IsNullOrWhiteSpace(Fit))
{
options += $"&fit={Fit}";
}
return options;
}
private string GetEdgeImageOptions(int width)
{
var options = string.Empty;
if (!string.IsNullOrWhiteSpace(Fit))
{
options = $"fit={Fit},";
}
options += $"width={width}";
return options;
}
private void SetAttribute(TagHelperOutput output, string key, string value)
{
if (!string.IsNullOrWhiteSpace(value))
{
output.Attributes.SetAttribute(key, value);
}
}
}</code></pre>
<p> Then finally to utilise the Tag Helper in the Razor View we simple do the following : </p>
<pre class="language-markup"><code> <div class="image" @Html.EditAttributes(x=>x.PromoImage)>
<img image-for="@Model.PromoImage" css-class="promoBlockImage" fit="cover" FlexibleAttribute="Goes Here"/>
</div></code></pre>
<p>Tag helpers for me are easier to comprehend and manage I also have the flexibility to add any additional attributes to my HTML tag without any additional coding. </p>
<h2>Conclusion</h2>
<p>The benefits of this new approach are numerous. First and foremost, the image transformation takes place at the edge, leveraging Cloudflare's global network of servers. This ensures lightning-fast delivery of optimized images with minimal latency, enhancing overall performance and user experience.</p>
<p>Furthermore, as the solution is provided by Optimizely and integrated into CMS12 and the Optimizely DXP, we can confidently rely on their support and compatibility with the latest versions of .NET. Say goodbye to the worries of compatibility issues and the hassle of ever-changing licensing agreements.</p>
<p>By leveraging "Image resizing at the edge for CMS and Commerce," we have the potential to simplify our image optimization workflow. There's no longer a need for server-side processing or the installation of additional tools. The optimization process seamlessly occurs within the Cloudflare edge platform, making our solution more scalable and efficient.</p>
<p>In conclusion, Optimizely's solution will empower us to overcome the challenges we faced with previous tools and server-side optimization. With image processing now taking place at the edge, we can deliver optimized images effortlessly, enhancing user experience and performance without adding complexity to our server infrastructure.</p>How to Write an xUnit Test to Verify Unique Content Type Guids in Content Management/blogs/Minesh-Shah/Dates/2023/3/how-to-write-an-xunit-test-to-verify-unique-content-type-guids-in-content-management/2023-03-27T12:55:15.0000000Z<p>When developing an Optimizely CMS solution, it is important to ensure that each content type has a unique GUID. If two or more content types share the same GUID, the CMS can have unexpected behavior and issues. In this blog post, we will explore how to write an xUnit test to verify that all Content Type Guids are unique in an Content Management solution.</p>
<h3>The Problem</h3>
<p><span>When developing an Optimizely CMS solution, it is common to define content types using C# classes that implement the </span><code>IContentData</code><span> interface. Each content type is typically decorated with the </span><code>ContentType</code><span> attribute, which defines properties such as the name and GUID of the content type.</span></p>
<p><span>When multiple content types share the same GUID, it can cause unexpected behavior and issues in the CMS. For example, if two content types have the same GUID, it can cause an exception to be thrown when attempting to create a new instance of one of the content types.</span></p>
<h3>The Solution</h3>
<p><span>To verify that all Content Type Guids are unique in an Optimizely Content Management solution, we can write an xUnit test that iterates through all the content types in the solution and checks that each content type has a unique GUID. Here is the code for the test:</span></p>
<pre class="language-csharp"><code>using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using EPiServer.Core;
using EPiServer.DataAbstraction;
using Xunit;
namespace YourProject.Tests
{
public class ContentTypesGuidTest
{
[Fact]
public void VerifyContentTypesGuidsAreUnique()
{
// Get the assembly that contains the Project.Cms.Domain project
var domainAssembly = Assembly.Load("Project.Cms.Domain");
// Get all content types in the solution, this covers Pages, Blocks and Assets
var contentTypes = typeof(PageData).Assembly.GetTypes()
.Where(x => typeof(IContentData).IsAssignableFrom(x) && !x.IsAbstract);
// Create a dictionary to hold the Guids and their counts
var guidDictionary = new Dictionary<Guid, int>();
// Iterate through all the content types
foreach (var contentType in contentTypes)
{
// Get the SiteContentType attribute of the content type
var siteContentTypeAttribute = contentType.GetCustomAttribute<SiteContentTypeAttribute>();
// Get the value of the Guid property in the SiteContentType attribute
var guid = siteContentTypeAttribute.GUID;
// Check if the Guid already exists in the dictionary
if (guidDictionary.ContainsKey(guid))
{
// Increment the count for the Guid if it already exists
guidDictionary[guid]++;
}
else
{
// Add the Guid to the dictionary if it doesn't exist
guidDictionary.Add(guid, 1);
}
}
// Iterate through the dictionary to check if there are any Guids with a count greater than 1
foreach (var guid in guidDictionary)
{
Assert.Equal(1, guid.Value); // Ensure the count for each Guid is equal to 1
}
}
}
}
</code></pre>
<p><span>This test code uses reflection to get all the content types in the solution that implement the </span><code>IContentData</code><span> interface and are not abstract. It then iterates through all the content types and checks that each content type has a unique GUID. The test fails if any content type has a GUID that is not unique.</span></p>
<p><span>By writing an xUnit test to verify that all Content Type Guids are unique in an Optimizely solution, we can ensure that the CMS has expected behavior and avoid potential issues caused by duplicate GUIDs. </span></p>URL Rewrites in CMS12 (.Net 6) /blogs/Minesh-Shah/Dates/2023/2/url-rewrites-in-cms12--net-6-/2023-02-28T23:22:56.0000000Z<p>URL rewriting is a common technique used in web applications to create user-friendly URLs that are easier to understand, provide consistency and have SEO benefits. In the past, URL rewriting was commonly accomplished using IIS URL Rewrite module. However, with the release of .NET Core, the process of rewriting URLs has undergone some changes.</p>
<p>Microsoft has introduced a new URL rewriting middleware called <strong>Microsoft.AspNetCore.Rewrite</strong>. This middleware is part of the ASP.NET Core framework and is designed to rewrite URLs in a much more flexible and efficient way.</p>
<p>One of the key benefits of using the <strong>Microsoft.AspNetCore.Rewrite</strong> middleware is that it allows URL rewriting without requiring IIS or any other web server. This means that we can create Optimizely Solutions that are completely self-contained and can be run on any platform.</p>
<p>Here is an example of how to use the <strong>Microsoft.AspNetCore.Rewrite</strong> middleware in .NET 6 to handle some common conventions like redirecting to https, enforcing lowercase urls and adding trailing slash to the end of all URLs:</p>
<h3>Lower Case URL – Implement IRule Base Rule</h3>
<pre class="language-csharp"><code> public class LowercaseUrlsRule : IRule
{
public int StatusCode { get; } = (int)HttpStatusCode.MovedPermanently;
public void ApplyRule(RewriteContext context)
{
HttpRequest request = context.HttpContext.Request;
PathString path = context.HttpContext.Request.Path;
HostString host = context.HttpContext.Request.Host;
if (path.HasValue && path.Value.Any(char.IsUpper) || host.HasValue && host.Value.Any(char.IsUpper))
{
HttpResponse response = context.HttpContext.Response;
response.StatusCode = StatusCode;
response.Headers[HeaderNames.Location] = (request.Scheme + "://" + host.Value + request.PathBase.Value + request.Path.Value).ToLower() + request.QueryString;
context.Result = RuleResult.EndResponse;
}
else
{
context.Result = RuleResult.ContinueRules;
}
}
}</code></pre>
<h3>Use Rules in Middleware Startup.cs</h3>
<p>In the example below we are using the <strong>RewriteOptions </strong>class to define paths that should be negated, and adding the rules for forcing HTTPS, Lowercase and Trailing Slashes. I have explicitly added below <strong>app.UseStaticFiles()</strong> so the rules do not get added to files like css, javascript or images.</p>
<pre class="language-csharp"><code> app.UseStaticFiles();
var options = new RewriteOptions()
.Add(context =>
{
if (context.HttpContext.Request.Path.StartsWithSegments("/util") ||
context.HttpContext.Request.Path.StartsWithSegments("/episerver") ||
context.HttpContext.Request.Path.StartsWithSegments("/modules"))
{
context.Result = RuleResult.SkipRemainingRules;
}
})
// Redirect to HTTPS
.AddRedirectToHttpsPermanent()
// Enforce lower case.
.Add(new LowercaseUrlsRule())
// Enforce trailing slash.
.AddRedirect("(.*[^/])$", "$1/");
app.UseRewriter(options);</code></pre>
<p>These techniques are not Optimizely specific and can be applied to any .Net Solution, more info on IRule based rewrites can be found <a href="https://learn.microsoft.com/en-us/aspnet/core/fundamentals/url-rewriting?view=aspnetcore-7.0">here</a></p>Video Demonstration, creating a CMS12 Alloy Sample Site /blogs/Minesh-Shah/Dates/2022/11/video-demonstration-creating-a-cms12-alloy-sample-site-/2022-11-28T18:34:33.0000000Z<p>Hey All</p>
<p>Below you will find a quick video demonstration on how to install a local version of Alloy Sample based on CMS12 / .Net 6. As you will see in the video its extremely simple to do and can be achieved in less than 5 minutes. </p>
<p>I find the Alloy sample site extremely useful for demonstration purposes or doing quick proof of concepts</p>
<p>Incase the commands or instructions are difficult to follow the details are outlined here: </p>
<ul>
<li>Install .Net 6 @ <a href="https://dotnet.microsoft.com/en-us/download/dotnet/6.0">Download .NET 6.0 (Linux, macOS, and Windows) (microsoft.com)</a> </li>
<li>Install Optimizely Dotnet Templates -
<pre class="language-csharp"><code>dotnet new install Episerver.Templates</code></pre>
</li>
<li>Install Alloy MVC Templates -
<pre class="language-csharp"><code>dotnet new epi-alloy-mvc</code></pre>
</li>
<li>Run the Solution -
<pre class="language-csharp"><code>dotnet run</code></pre>
</li>
</ul>
<p>Video : <a href="https://youtu.be/NbfJzx1DiAs">https://youtu.be/NbfJzx1DiAs</a> </p>Display Child Pages in Content Delivery API Response/blogs/Minesh-Shah/Dates/2022/10/display-child-pages-in-content-delivery-api-response/2022-10-04T17:23:48.0000000Z<p>The below example will implement an instance of IContentConverterProvider to customise the serialisation of PageData and output child pages in the Content Delivery API response. It is a very simple implementation although can be extended to fully customise the Content Delivery Response.</p>
<p>All code below can be found on the following GIT Repo : <a href="https://github.com/Netcel-Optimizely/Optimizely-ContentDelivery-Examples">https://github.com/Netcel-Optimizely/Optimizely-ContentDelivery-Examples</a></p>
<p>Firstly let’s create an interface which can be implemented for specific conversion scenarios</p>
<pre class="language-csharp"><code> /// <summary>
/// Content api model property convertor
/// </summary>
public interface IContentApiModelConvertor
{
/// <summary>
/// Convert content to api model
/// </summary>
/// <param name="content"></param>
/// <param name="contentApiModel"></param>
/// <param name="converterContext"></param>
void Convert(IContent content, ContentApiModel contentApiModel, ConverterContext converterContext);
}</code></pre>
<p>Reference : <a href="https://github.com/Netcel-Optimizely/Optimizely-ContentDelivery-Examples/blob/main/src/Netcel.ContentDelivery/Interfaces/IContentApiModelConvertor.cs">https://github.com/Netcel-Optimizely/Optimizely-ContentDelivery-Examples/blob/main/src/Netcel.ContentDelivery/Interfaces/IContentApiModelConvertor.cs</a> </p>
<p>To handle the conversion, we must create our own Content Converter for this create a new class called PageContentConvertor which inherits DefaultContentConverter this already implements IContentConverter and deals with the base conversion logic.</p>
<pre class="language-csharp"><code> public class PageContentConvertor : DefaultContentConverter
{
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly IEnumerable<IContentApiModelConvertor> _contentApiModelConvertors;
/// <summary>
/// Initializes a new instance of the <see cref="PageContentConvertor"/> class.
/// </summary>
public PageContentConvertor()
{
}
/// <summary>
/// Initializes a new instance of the <see cref="PageContentConvertor"/> class.
/// Default page convertor
/// </summary>
/// <param name="contentTypeRepository"></param>
/// <param name="reflectionService"></param>
/// <param name="contentModelService"></param>
/// <param name="contentVersionRepository"></param>
/// <param name="contentLoaderService"></param>
/// <param name="urlResolverService"></param>
/// <param name="propertyConverterResolver"></param>
/// <param name="httpContextAccessor"></param>
/// <param name="contentApiModelConvertors"></param>
public PageContentConvertor(
IContentTypeRepository contentTypeRepository,
ReflectionService reflectionService,
IContentModelReferenceConverter contentModelService,
IContentVersionRepository contentVersionRepository,
ContentLoaderService contentLoaderService,
UrlResolverService urlResolverService,
IPropertyConverterResolver propertyConverterResolver,
IHttpContextAccessor httpContextAccessor,
IEnumerable<IContentApiModelConvertor> contentApiModelConvertors)
: base(
contentTypeRepository,
reflectionService,
contentModelService,
contentVersionRepository,
contentLoaderService,
urlResolverService,
propertyConverterResolver)
{
_httpContextAccessor = httpContextAccessor;
_contentApiModelConvertors = contentApiModelConvertors;
}
/// <summary />
/// <param name="content"></param>
/// <param name="converterContext"></param>
/// <returns></returns>
public override ContentApiModel Convert(IContent content, ConverterContext converterContext)
{
if (converterContext.ContextMode.EditOrPreview())
{
_httpContextAccessor.HttpContext.SetupVisitorGroupImpersonation(content, AccessLevel.Read);
}
var model = base.Convert(content, converterContext);
foreach (var convertor in _contentApiModelConvertors)
{
convertor.Convert(content, model, converterContext);
}
if (converterContext.Options.FlattenPropertyModel)
{
FlattenPropertyMap(model);
}
return model;
}
}</code></pre>
<p>Reference : <a href="https://github.com/Netcel-Optimizely/Optimizely-ContentDelivery-Examples/blob/main/src/Netcel.ContentDelivery/ContentConvertor/PageContentConvertor.cs">https://github.com/Netcel-Optimizely/Optimizely-ContentDelivery-Examples/blob/main/src/Netcel.ContentDelivery/ContentConvertor/PageContentConvertor.cs</a></p>
<p>Register this Class at Startup</p>
<pre class="language-csharp"><code>services.AddSingleton<PageContentConvertor>();</code></pre>
<p>Now, let’s create our own PageContentConvertorProvider which implements IContentConverterProvider</p>
<pre class="language-csharp"><code> public class PageContentConvertorProvider : IContentConverterProvider
{
private readonly PageContentConvertor _pageContentConvertor;
/// <summary>
/// Initializes a new instance of the <see cref="PageContentConvertorProvider"/> class.
/// </summary>
/// <param name="pageContentConvertor"></param>
public PageContentConvertorProvider(PageContentConvertor pageContentConvertor)
{
_pageContentConvertor = pageContentConvertor;
}
public int SortOrder => 200;
/// <summary>
/// Resolve custom page convertor
/// </summary>
/// <param name="content"></param>
/// <returns></returns>
public IContentConverter Resolve(IContent content)
{
// further enhance this to allow different supported convertors
return content is PageData ? _pageContentConvertor : null;
}
}</code></pre>
<p>Reference: <a href="https://github.com/Netcel-Optimizely/Optimizely-ContentDelivery-Examples/blob/main/src/Netcel.ContentDelivery/ContentConvertorProviders/PageContentConvertorProvider.cs">https://github.com/Netcel-Optimizely/Optimizely-ContentDelivery-Examples/blob/main/src/Netcel.ContentDelivery/ContentConvertorProviders/PageContentConvertorProvider.cs</a> </p>
<p>We also register at Startup </p>
<pre class="language-csharp"><code> services.TryAddEnumerable(ServiceDescriptor.Scoped<IContentApiModelConvertor, PageDataApiModelConvertor>());</code></pre>
<p>Finally for the actual business logic to perform the conversion we first need a Model for the Navigation Item</p>
<pre class="language-csharp"><code> /// <summary>
/// Navigation Item Dto.
/// </summary>
public class NavigationItem
{
/// <summary>
/// Gets or sets navigation Title.
/// </summary>
public string Title { get; set; }
/// <summary>
/// Gets or sets navigation Url.
/// </summary>
public string Url { get; set; }
/// <summary>
/// Gets or sets a value indicating whether true if it matches the current page.
/// </summary>
public bool IsSelected { get; set; }
/// <summary>
/// Is Visibly in Menu Property Set
/// </summary>
public bool VisibleInMenu { get; set; }
public IEnumerable<NavigationItem> Children { get; set; } = Enumerable.Empty<NavigationItem>();
}</code></pre>
<p>Reference: <a href="https://github.com/Netcel-Optimizely/Optimizely-ContentDelivery-Examples/blob/main/src/Netcel.ContentDelivery/Models/NavigationItem.cs">https://github.com/Netcel-Optimizely/Optimizely-ContentDelivery-Examples/blob/main/src/Netcel.ContentDelivery/Models/NavigationItem.cs</a> </p>
<p>We than create our Page Model Convertor which contains the business logic to output the Child Pages to Content Delivery API</p>
<pre class="language-csharp"><code> /// <summary>
/// Page Data Content Convertor
/// </summary>
public class PageDataApiModelConvertor : IContentApiModelConvertor
{
private readonly IContentLoader _contentLoader;
private readonly IUrlResolver _urlResolver;
/// <summary>
/// Initializes a new instance of the <see cref="PageDataApiModelConvertor"/> class.
/// </summary>
/// <param name="contentLoader"></param>
/// <param name="urlResolver"></param>
public PageDataApiModelConvertor(IContentLoader contentLoader, IUrlResolver urlResolver)
{
_contentLoader = contentLoader;
_urlResolver = urlResolver;
}
public void Convert(IContent content, ContentApiModel contentApiModel, ConverterContext converterContext)
{
if (content is not PageData pageData)
{
return;
}
// add navigation
var navigation = new List<NavigationItem>();
var children = _contentLoader.GetChildren<PageData>(content.ContentLink);
navigation.AddRange(children.Select(x => CreateNavigationStructure(x, pageData)));
contentApiModel.Properties.Add("ChildPages", navigation);
}
private NavigationItem CreateNavigationStructure(PageData page, IContent currentContent)
{
var model = CreateNavigationItem(page, currentContent);
var children = _contentLoader.GetChildren<PageData>(page.ContentLink);
model.Children = children.Select(x => CreateNavigationStructure(x, currentContent));
return model;
}
private NavigationItem CreateNavigationItem(PageData pageContent, IContent currentContent)
{
return new NavigationItem
{
Title = pageContent.Name,
Url = _urlResolver.GetUrl(pageContent.ContentLink),
IsSelected = pageContent.ContentLink == currentContent.ContentLink,
VisibleInMenu = pageContent.VisibleInMenu,
};
}
}</code></pre>
<p>Reference : <a href="https://github.com/Netcel-Optimizely/Optimizely-ContentDelivery-Examples/blob/main/src/Netcel.ContentDelivery/ContentApiModelConvertors/PageDataApiModelConvertor.cs">https://github.com/Netcel-Optimizely/Optimizely-ContentDelivery-Examples/blob/main/src/Netcel.ContentDelivery/ContentApiModelConvertors/PageDataApiModelConvertor.cs</a> </p>
<p>Register the conversion provider at Startup </p>
<pre class="language-csharp"><code> services.TryAddEnumerable(ServiceDescriptor.Singleton<IContentConverterProvider, PageContentConvertorProvider>());
</code></pre>
<p>If all goes to plan when querying any page via the Content Delivery API the payload of the response should include the child pages.</p>
<p>e.g.</p>
<p><img src="/link/a62dee91a9af46f48377d9c853d38121.aspx" width="1048" alt="" height="1089" /></p>Local Multi-Site Setup for CMS12 /blogs/Minesh-Shah/Dates/2022/7/local-multi-site-setup-for-cms12-/2022-07-12T17:30:24.0000000Z<p>Many solutions these days are Multi-Site enabled, with CMS 11 and using IIS it was simple to add multiple bindings to the same solution. With CMS12 when not using IIS it was not clearly documented on how to create multiple host names although it transpires this is very simple to achieve via the Program.cs class. </p>
<p>To do this we can utilse the UseUrls extention on the WebHostBuilder and add the URLS as comma delimated parameters for the web host to listen on i.e. </p>
<pre class="language-csharp"><code>public class Program
{
public static void Main(string[] args) => CreateHostBuilder(args).Build().Run();
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureCmsDefaults()
.ConfigureWebHostDefaults(webBuilder => webBuilder.UseStartup<Startup>()
.UseUrls("https://localhost:5001", "https://localhost:5002"));
}</code></pre>
<p><img src="/link/711104c457b94e3695534e1a469ccf53.aspx" /></p>Property Lists Serialization and Content Delivery API/blogs/Minesh-Shah/Dates/2022/2/property-lists-serialization-and-content-delivery-api/2022-02-15T10:22:10.0000000Z<p>When utilising property lists in CMS v12 we found the content was not automatically being serialised and returned in the JSON Payload of the Content Delivery API v3.0</p>
<p>To overcome this, we implemented the IPropertyConverter interface found in the EPiServer.ContentApi.Core.Serialization namespace and registered using dependency injection.</p>
<p>Please be aware the Interface’s documentation does state it is currently in Preview State and could introduce breaking changes between minor versions.</p>
<p>To demonstrate the serialization of a property list I have used a simple example of a property list containing of a model containing two string properties.</p>
<p>Example Code:</p>
<p>Create a Property Model Inheriting EPiServer.ContentApi.Core.Serialization.Models.PropertyModel</p>
<pre class="language-csharp"><code> public class BenefitItemPropertyModel : PropertyModel<IEnumerable<BenefitItemModel>, PropertyList<BenefitItemModel>>
{
/// <summary>
/// Initializes a new instance of the <see cref="BenefitItemPropertyModel"/> class.
/// </summary>
/// <param name="type"></param>
public BenefitItemPropertyModel(PropertyList<BenefitItemModel> type)
: base(type)
{
Value = GetValues(type.List);
}
private IEnumerable<BenefitItemModel> GetValues(IList<BenefitItemModel> items)
{
return items.Select(x => new BenefitItemModel
{
BenefitText = x.BenefitText,
BenefitIcon = x.BenefitIcon
});
}
}</code></pre>
<p>Implement the IPropertyConverter Interface and the Convert Method</p>
<pre class="language-csharp"><code> public class BenefitItemListPropertyConvertor : IPropertyConverter
{
/// <inheritdoc />
public IPropertyModel Convert(PropertyData propertyData, ConverterContext contentMappingContext)
{
return new BenefitItemPropertyModel((PropertyList<BenefitItemModel>)propertyData);
}
}</code></pre>
<p>Create a Property Conversion for Property Lists </p>
<pre class="language-csharp"><code> public class ListPropertyConvertorProvider : IPropertyConverterProvider
{
private readonly IServiceProvider _serviceProvider;
/// <summary>
/// Initializes a new instance of the <see cref="ListPropertyConvertorProvider"/> class.
/// List property convertor provider
/// </summary>
/// <param name="serviceProvider"></param>
public ListPropertyConvertorProvider(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
}
/// <summary>
/// The provider which has higher order will be called first to see if it handles specified <see cref="PropertyData" /> type
/// </summary>
public int SortOrder => 1000;
/// <summary>
/// Determines if provider supports specified <paramref name="propertyData" /> type and if so returns a matching
/// <see cref="IPropertyConverter" /> instance. If <paramref name="propertyData" /> is not supported return null
/// </summary>
/// <param name="propertyData">instance of <see /> to resolve <see /> for</param>
/// <returns>A matching <see /> or null if <paramref name="propertyData" /> is not supported by the provider</returns>
public IPropertyConverter Resolve(PropertyData propertyData)
{
return propertyData switch
{
PropertyList<BenefitItemModel> => _serviceProvider.GetService<BenefitItemListPropertyConvertor>(),
_ => null,
};
}
}</code></pre>
<p>Register the dependency in the Startup.cs Class</p>
<pre class="language-csharp"><code>services.TryAddEnumerable(ServiceDescriptor.Singleton<IPropertyConverterProvider, ListPropertyConvertorProvider>());
services.TryAddScoped<BenefitItemListPropertyConvertor>();</code></pre>
<p>Having implemented the above you should get the following response from the content delivery API</p>
<pre class="language-markup"><code> "displayOption": "col-6",
"contentLink": {
"id": 146,
"workId": 0,
"guidValue": "83db556b-a029-4108-8b38-0cf4a29c9d9a",
"expanded": {
"contentLink": {
"id": 146,
"workId": 0,
"guidValue": "83db556b-a029-4108-8b38-0cf4a29c9d9a"
},
"name": " Benefits",
"contentType": [
"Block",
"BenefitsBlock"
],
"theme": "green",
"benefits": [
{
"benefitIcon": "icon_pound",
"benefitText": "Lorem ipsum dolor sit amet"
},
{
"benefitIcon": "icon_pound",
"benefitText": "Lorem ipsum dolor sit amet"
}
]
}
}
},
</code></pre>