Friday, 10 July 2026

Optimizely DXP: Every Supported Culture, One Searchable Page

Quick one for anyone building multi-language sites on Optimizely DXP. I put together a reference tool listing all 806 supported cultures. More usefully, it shows which ones actually work with Azure Translator's 1-click auto-translate, and which get full NLP treatment (stemming, tokenization, decompounding) from Optimizely Graph.

If you've ever added a new market and then spent twenty minutes digging through docs to figure out whether editors will get auto-translate for that locale, or whether search will actually work well in that language, this is for that exact moment. Search or filter by culture name or code, grab the exact culture code you need for config, and see right away which capabilities you're getting.

806 cultures. 33 with Azure Translator support. Full Graph NLP coverage mapped out. All in one table.

Check it out here: Optimizely DXP – Supported Cultures / Languages

July 10, 2026

Wednesday, 17 September 2025

How to Allow AI Bots in Your robots.txt File (2025 Edition)

The world of AI is moving fast and so are the bots that crawl the web to feed large language models (LLMs), AI search engines, and generative tools. If you want your site’s content to be available for AI models like ChatGPT, Claude, Perplexity, or Gemini, you can explicitly grant permission by updating your robots.txt file.

In this guide, you’ll learn:

  • Why AI bots matter in 2025
  • Which user-agents to include
  • A ready-to-use robots.txt template

Why AI Bots Need robots.txt Rules

Traditionally, robots.txt controlled search engines like Googlebot and Bingbot. Now, AI companies also respect these directives to decide whether your content can be indexed for AI search or used for model training.

  • Search bots (AI answers/assistants): Allowing them means your content may appear in AI-powered search results (e.g., Perplexity, You.com).
  • Training crawlers: Allowing them means your content may be used to train or fine-tune large AI models (e.g., GPTBot, ClaudeBot).

By updating your robots.txt, you can allow, block, or mix your policies across these bots.


Major AI User-Agents in 2025

OpenAI (ChatGPT / SearchGPT)

  • GPTBot => training crawler
  • ChatGPT-User => on-demand browsing in ChatGPT
  • OAI-SearchBot => indexing for SearchGPT

Anthropic (Claude)

  • ClaudeBot => model training
  • Claude-Web / anthropic-ai => crawling/AI access

Perplexity

  • PerplexityBot => AI search engine

Google (Gemini)

  • Google-Extended => controls if Gemini can use your content for AI

Apple

  • Applebot-Extended => controls Apple AI training usage

Amazon

  • Amazonbot => AI + product/search crawling

Common Crawl

  • CCBot => feeds public datasets used in AI training

AI Search Engines

  • YouBot => You.com
  • PhindBot => Phind AI search
  • ExaBot => Exa.ai
  • AndiBot => Andi search
  • FirecrawlAgent => Firecrawl AI

Example: Allowing AI Bots

Here’s a robots.txt example that explicitly allows AI bots alongside traditional crawlers:

# Default: allow all crawlers
User-agent: *
Disallow:

# --- Explicitly ALLOW major AI/LLM bots ---

# OpenAI
User-agent: GPTBot
Allow: /
User-agent: ChatGPT-User
Allow: /
User-agent: OAI-SearchBot
Allow: /

# Anthropic (Claude)
User-agent: ClaudeBot
Allow: /
User-agent: Claude-Web
Allow: /
User-agent: anthropic-ai
Allow: /

# Perplexity
User-agent: PerplexityBot
Allow: /

# Google AI usage token
User-agent: Google-Extended
Allow: /

# Apple AI usage token
User-agent: Applebot-Extended
Allow: /

# Amazon
User-agent: Amazonbot
Allow: /

# Common Crawl
User-agent: CCBot
Allow: /

# AI search engines
User-agent: YouBot
Allow: /
User-agent: PhindBot
Allow: /
User-agent: ExaBot
Allow: /
User-agent: AndiBot
Allow: /
User-agent: FirecrawlAgent
Allow: /

Optional: Balanced Policy (Allow AI Search, Block Training)

If you want to appear in AI search results but not have your content used for model training, use a mixed policy. For example:

# Allow AI search crawlers
User-agent: PerplexityBot
Allow: /
User-agent: YouBot
Allow: /
User-agent: PhindBot
Allow: /
User-agent: ExaBot
Allow: /
User-agent: AndiBot
Allow: /
User-agent: OAI-SearchBot
Allow: /

# Disallow training/aggregator crawlers
User-agent: GPTBot
Disallow: /
User-agent: ClaudeBot
Disallow: /
User-agent: Google-Extended
Disallow: /
User-agent: Applebot-Extended
Disallow: /
User-agent: CCBot
Disallow: /

Pro Tips

  • Keep it updated: New AI bots appear frequently so review your list quarterly.
  • Combine with enforcement: Some bots may ignore rules. If strict compliance matters, pair robots rules with IP/reverse-DNS checks or WAF bot controls.
  • Test quickly: Host your robots.txt at https://<your-domain>/robots.txt and fetch it from a browser or with curl to verify.

Going Further with SEO

If you’re running an Optimizely CMS site and want deeper control over SEO, check out my open-source tool: SEOBOOST for Optimizely CMS.

AI is becoming a parallel discovery channel next to Google. Your robots.txt file is the simplest way to take control of how your content participates.

September 17, 2025

Thursday, 4 September 2025

How to Preview Unpublished Content on the Frontend in Optimizely CMS 12

Introduction

In my previous post, I explained how to customize the ContentArea rendering pipeline in Optimizely CMS 12 so editors can see unpublished block content while previewing pages in the CMS.

That approach works great inside the CMS UI (Edit/Preview mode). But what if you want to enable the same functionality on the front-end site, using a simple query string flag?

That’s where the ?showdrafts=true parameter comes in.

The Problem

By default, Optimizely only shows published content when you browse a site page directly. Even if a page has a saved draft version (with new or updated blocks), you won’t see it on the public site unless the draft is published.

  • The page instance returned by routing is the published version.
  • The ContentAreaRenderer filters unpublished items before rendering.

As a result, any unpublished blocks in a draft page are invisible.

The Approach

We need three things working together:

  1. Detect ?showdrafts=true.
  2. Load the draft page version instead of the published one.
  3. Render all ContentArea items (published + unpublished) so draft blocks are not filtered out.

Step 1: Helper to Fetch Draft Pages


Step 2: Ensure Draft Blocks Render Too


Override ContentAreaRenderer to skip the internal filtering step when showdrafts=true.

Step 3: Controller Example


Final Result

  • Default URL /about-us → shows published page + published blocks.
  • With ?showdrafts=true → loads the draft page (so unpublished block references are included) and renders them.
  • Works consistently both inside CMS preview and on the public site (for editors/admins).

Closing Thoughts

The ?showdrafts=true query parameter is a simple but powerful way to let editors verify draft content directly on the site without switching to CMS preview.

When combined with the approach from my earlier post, you now have full control over showing unpublished content both inside and outside the CMS interface.

Just remember: never expose drafts to anonymous visitors — always enforce role checks.

September 04, 2025

Tuesday, 2 September 2025

How to Display Block Publishing Status in Optimizely CMS 12

Introduction

One of the most common editor complaints in Optimizely CMS is that it’s not obvious when a block inside a ContentArea has unpublished changes. By default, editors can only see those drafts if they preview the block itself (not the page that contains it). This leads to confusion, because the page looks “finished” but actually contains hidden drafts.

In this post, I’ll walk through how we solved this by decorating ContentArea items in the CMS UI so editors can instantly see the status of each block (Draft, Scheduled, Awaiting Approval, etc.) without leaving the page.

Problem

  • Draft blocks inside a ContentArea aren’t visible in page preview until they’re published.
  • Editors have no indication in All Properties view that a block has unpublished changes.
  • Status information (Draft, Awaiting Approval, Rejected, etc.) is stored in the CMS, but not surfaced in the editor UI.

Solution Overview

We extended the ContentArea editor with a custom Dojo widget and a small API controller:

  1. API Controller (/api/blockstatus)

    • Returns the latest VersionStatus for a block.
    • Maps raw enum values into friendly labels (e.g. CheckedOut → “Draft (in progress)”).
    • Also returns last saved date and changed by user for hover tooltips.

  2. Dojo Widget (poc/editors/ContentAreaWithStatus)

    • Overrides the default ContentAreaEditor.
    • Calls the batch API to fetch statuses for all blocks in a ContentArea.
    • Adds a small badge next to each block item with a friendly label.
    • Optionally tints the whole row with a soft background color for visibility.
    • Shows a tooltip on hover: “Draft (in progress) · Last edited by Jane Doe on 1 Sep 2025, 15:22”.

  3. EditorDescriptor

    • Applies globally to all ContentArea properties, no [UIHint] required.
    • Ensures editors always see status indicators wherever a ContentArea is used.

  4. CSS (color variants)

      
    	  /* Base marker when a non-published status is present */
          .poc-unpublished-block {
              position: relative;
          }
    
          /* Badge base */
          .poc-indicator {
              margin-left: 6px;
              font-size: 11px;
              color: #fff;
              padding: 2px 6px;
              border-radius: 3px;
              font-weight: 500;
          }
          
          /* Badge colors */
          .poc-badge--yellow {background: #ffc107; color: #000;}
          /* Drafts */
          .poc-badge--orange {background: #fd7e14;}
          /* Awaiting approval */
          .poc-badge--red {background: #dc3545;} /* Rejected */
          .poc-badge--blue {background: #0d6efd;} /* Scheduled */
          .poc-badge--gray {background: #6c757d;} /* Archived/Not created */
    
          /* Optional row tint (used when TINT_ROW = true) */
          .poc-row--yellow {background: #fff8db;} /* soft yellow */
          .poc-row--orange {background: #fff0e0;} /* soft orange */
          .poc-row--red {background: #ffe6e9;} /* soft red */
          .poc-row--blue {background: #eaf2ff;} /* soft blue */
          .poc-row--gray {background: #f2f3f5;} /* soft gray */
    	

Status Mapping

We mapped Optimizely’s VersionStatus enum into user-friendly labels and colors:

  • Draft (in progress) → Yellow badge
  • Draft (awaiting publish) → Yellow badge
  • Awaiting Approval → Orange badge
  • Rejected → Red badge
  • Scheduled → Blue badge
  • Archived / Not created → Gray badge
  • Published → No badge (treated as “clean”)

This color coding makes it immediately obvious which blocks need attention.

Benefits

  • Editors see at a glance which blocks are drafts, scheduled, awaiting approval, etc.
  • No hidden surprises in Preview — the status is visible directly in All Properties.
  • Better workflow visibility: tooltips show who last edited the block and when.
  • Performance friendly: uses a single batch API call per ContentArea.
  • Global: applies automatically to all ContentAreas.

Conclusion

By extending the ContentArea editor with a small Dojo widget and a lightweight API, we’ve made unpublished block statuses visible, color-coded, and editor-friendly. This simple improvement reduces confusion, saves time, and gives editors more confidence before publishing.

September 02, 2025

Monday, 1 September 2025

How to Show Unpublished Blocks in Optimizely CMS 12 Preview

Introduction

In this post, we’ll look at why Draft Blocks don’t show in Page Preview by default, and I'll show you a clean, drop-in solution to fix that. The result? Editors get a more accurate “what you see is what you’ll publish” experience - no more hidden draft blocks.

Problem: Preview doesn’t show drafts

By default, when you hit Preview in Optimizely CMS 12 (without using Projects), the system behaves like View mode. That means:

  • ContentAreaRenderer only treats Edit mode as “editor state”
  • IContentAreaLoader fetches published versions even in Preview

So draft blocks disappear. That’s confusing to editors.

Note on Projects vs normal Preview

If you’ve ever wondered why drafts sometimes appear in Preview already: Optimizely Projects can preview draft content that’s part of the active project. That’s project-scoped preview. Outside Projects, default Preview behaves more like public view, so unpublished blocks won’t show.

Solution overview

Here’s what we’re going to do:

  1. Make Preview behave like Edit in the ContentAreaRenderer
  2. Replace IContentAreaLoader so it loads the latest version (which may be a draft) in Preview/Edit
  3. Hook them into your DI container cleanly - no internal types, no circular dependencies

1) Preview behaves like Edit in renderer

This means the renderer stops skipping draft blocks in Preview - just like it would in Edit.

2) Loader returns latest in Preview

By providing your own IContentAreaLoader, you can:

  • In Preview/Edit: resolve the latest version (draft if available)
  • In View: revert to published content

That way, Preview reflects the editor’s current work - not stale published content.

3) Clean DI setup

Register the custom renderer and loader after services.AddCms() using a factory method:

This avoids trying to resolve internal types or causing DI loops- everything stays simple and robust.

Outcome

  • Editors see draft blocks in Preview (even outside Projects)
  • Public users still only see published content
  • The experience feels consistent and reliable
September 01, 2025

Friday, 19 January 2024

Improving Alt Text for Images within TinyMCE in Optimizely CMS

Introduction:

Content editors often face a tricky challenge when dealing with images within TinyMCE in Optimizely CMS. In this post, we'll explore the default options, the hurdles, and a straightforward solution to make our content more inclusive for everyone.

The Challenge:

Content editors often find themselves at a crossroads when integrating images into TinyMCE. Default options include the convenient drag-and-drop of media assets or the selection of images through specific TinyMCE controls. However, a notable issue surfaces when Optimizely CMS defaults to placing an empty alt tag or, in the case of previous version, utilizes the image name (derived from the file name at upload time) as the alternative text.

The Markup Dilemma:

The resulting markup, exemplified by

<img src="/globalassets/someimagefile.png" alt="" /> 
or
<img src="/globalassets/someimagefile.png" alt="someimagefile.png" />
., falls short in aiding accessible users. This empty or file name-based alt text does little to convey meaningful information to those relying on accessibility features.

The Solution

At render time, we can intercept XhtmlString and modify what is rendered.

Step 1: Extend ImageFile.cs with a new  property AltText

Step 2: Create a XhtmlString.cshtml Display Template

To take control of the rendering process for XhtmlString properties and ensure you set it up properly in your solution.

XhtmlString.cshtml

@using EPiServer.Core
@model XhtmlString

@Html.Raw(Html.XhtmlString(Model.AdjustAltText()))      

Step 3: Develop the Extension Method - AdjustAltText()

Create an extension method  AdjustAltText() to instruct Optimizely to render the content for modification. Optimizely utilizes its mechanisms to personalize and render blocks within XhtmlString fields. After obtaining the HTML result, apply the necessary modifications using HtmlAgilityPack.

After deploying this change-up, any time an XhtmlString is rendered, we are now intercepting it and adjusting its alternate text accordingly.

Troubleshooting: In the unlikely event of the AdjustAltText() extension method not triggering, add the UIHintAttribute [UIHint("XhtmlString")] to the XhtmlString datatype property.

    [Display(
        GroupName = SystemTabNames.Content,
        Order = 310)]
    [CultureSpecific]
    [UIHint("XhtmlString")]
    public virtual XhtmlString MainBody { get; set; }            


*Drawing Inspiration from Dylan McCurry's Episerver and Alternate Text for Images in the TinyMCE Rich Text Editor

January 19, 2024

Monday, 8 February 2021

Basic Episerver Coding Best Practices

This is my first post on Episerver development best practices. In this one, I'll present the best practices that should be followed while working with Episerver - ASP.Net MVC framework. 
Here are some best practices you should always have in mind:

A. General Coding Conventions

Compare strings with “==” operator 

The standard C# coding practice advises that string comparison should be done with string.Equals() method, because “==” operator compares strings base on reference equality, and that could lead to unexpected behaviours. 
Use string.Equals() method. 

Remove Unused Using Statements

 Remove all unused using statements.

Simplify Names

Names should be simplified (for example change String -> string, Boolean -> bool, remove leading namespace if the namespace is already declared in using statement).
  • The naming convention for methods, variables, and parameters 
Check all class to make sure the naming follows C# convention. Method and class names should follow Pascal Case, variable and parameter names should follow the Camel Case. Consider changing them for better readability and maintainability. 

High Usage of String Concatenation

The high number of string concatenations generally leads to poor memory usage because the string is immutable. Every time a string is modified, a new instance is created in the memory and that will lead to too many call to memory allocation. To optimize memory usage on string operations, make use of StringBuilder class if there are more than a few concatenations needed in each code block. 

Move hardcoded strings to constants

Consider moving hardcoded strings to constant classes or resource files to avoid duplication and centralize their management. Strings to process the program flow should be moved to constant, while strings used to display messages should be moved to resource files to enable localization.

Hardcoded service’s URL and sensitive information 

There might be some places in the code which have hardcoded strings to store sensitive information like username, password, encryption key and the service’s URL. This information should be moved to the application settings file (web.config) instead. 

Move business Logic from Views 

In the principal domain, logic should go into the model or view model and application logic into the controller. Business logic in views is against MVC principal and it has serious performance implications. 

Empty files & classes 

 Empty files & classes (if any) should be removed for better maintainability. 

Unused classes & methods 

Unused classes & methods (if any) should be removed for better maintainability. 

Potential code quality issues 

Special attention needs to be given for code quality issue witch regards to following 
  •  Variables with possible ‘System.NullReferenceException’. 
  •  Uses empty general catch class suppresses any errors 
  •  Replace with 'String.IsNullOrEmpty' 
  •  String formatting method problems 

Unused properties

Remove unused properties form the blockTypes pageTypes e.t.c 

Move static methods to extension classes

Static methods which operate on some type of data and placed across the code files and the methods are not related to the business logic of their classes should be organized as extension methods instead. 

Use View State to store large information

Avoid using view state. Sometimes the website makes use of ASP.NET View State to store a certain amount of information, sometimes include large objects such as data table. Objects stored in View State are rendered to a hidden field in the HTML output of the web page, and this increases the size of the request. In case the View State gets too large, it could be blocked by proxy servers. 

Implement IDisposable

Use where required. 

JavaScript and CSS code are embedded in razor views (.cshtml) 

Some performance gains can be achieved by minifying the embedded JavaScript and CSS inside the razor views. 

B. Episerver Best Practices

Hide information not used frequently by editors 

To create pages of type “500 page”, “400 page” and so on is not the daily work for editors. It should be better to just show them to a smaller specific group that is responsible to set up the site. This can be done by restricting the access to create that page type to a certain group of editors or administrators.  

Obsolete EPiServer Class/Methods

In each major version, old obsolete methods are removed permanently to make sure that the API is kept clean and usable over time, so even if you can postpone fixing warning messages, it is good practice to make sure all warning messages are fixed before upgrading to a major version. 

Memory leak due to constructing new DataFactory class

DataFactory is a singleton class (which mean one instance for entire application), and it should not be constructed. In fact, EPiServer coding practice does not advice developer to use DataFactory directly, and the developer should use an injected instance of IContentRepository instead. 

EPiServer FindPagesWithCriteria problem

Due to its lack of developer usability and relatively slow performance it's a method many developers love to hate. Under the hood, FindPagesWithCriteria builds a bunch of SQL statements that query the database directly. There is No caching when you use this. When you use FindPagesWithCriteria, you need to be careful that you don't overuse it or better avoid using FindPageWithCriteria altogether.  

Enable ContentOutputCache

Caching can dramatically increase the performance of your website. [ContentOutputCache] you can set a much higher duration for your output cache. The cache will be invalidated as soon as an editor change the page and the visitor will see the updated information. ContentOutputCache attribute is commented out in Page controllers.

Use inline Blocks instead of custom properties

Its easier to manage and localize blocks instead of custom properties. Create inline Blocks e.g for CTA any settings AvailableInEditMode = false 

Overuse of ServiceLocator

The most preferable ways to do the dependency injection in Episerver is to use Constructor injection whenever possible
  
public class DummyPageController : PageControllerBase<DummyPage>
{
        private readonly IContentRepository _contentRepository;
        private readonly ContentAssetHelper _contentAssetHelper;
        
        public DummyPageController(
            ContentAssetHelper _contentAssetHelper, 
            IContentRepository contentRepository)
        {
            _urlResolver = _contentAssetHelper;
            _profileService = _contentRepository;
        }
        
        public ActionResult Index(AccountPage currentPage)
        {
             var startPage = _contentRepository.Get<StartPage>(ContentReference.StartPage);
            var assetsFolder = _contentAssetHelper.GetOrCreateAssetFolder(currentPage.ContentLink);
            return View();
        }
}        
  
Moving forward with the issue with ServiceLocator, to create objects with the service locator cost more than a normal creation with the “new” operator. So, performance gains can be archived if the ServiceLocator is just used once for a page view. 
Example
  
public class DummyPageController : PageControllerBase<DummyPage>
{
    public ActionResult Index(DummyPage currentPage)
{ var startPage = ServiceLocator.Current.GetInstance<IContentRepository>().Get<StartPage>(ContentReference.Stat… var dummySites = ServiceLocator.Current.GetInstance<IContentRepository>().GetChildren<ContentPage>(startPage… … foreach (var site in dummySites) { var pages = ServiceLocator.Current.GetInstance<IContentRepository>().GetChildren<SamplePage>(site.C…

The Problems with Service Locator

Using the snippet of code above is all very good but, in a project, you might have multiple files that need to call IContentRepository. Having this code duplicated also means you will need to write the same tedious set-up code in your unit tests everywhere. 

EPiServer also provides an alternative way of injecting dependencies into your code, via property injection and constructor injection. Injected<> uses property injection via the IoC container to give you access to your dependencies.
An example of how to use Injected<> can be seen below:
  
public class DummyExample
{
	internal Injected<IInterface> interface;
	public DummyExample()
	{
		var implmentation = interface.Service;
	}
}
  
Injected is useful when you need to implement from things like ISelectionFactory or an InitializationModule. If you try to use constructor injection in these types of files, MVC will complain about not having a parameterless constructor. 

EPiServer supports constructor injection for controllers. It is possible to inject dependent objects in your EPiServer controllers if you have configured it properly - created MVC dependency resolver. 

Try to minimize the construction of objects when using the ServiceLocator or better use Injected or constructer injection. 

Implement EPiServer Schedule Jobs Stop Signal 

Stop signal in EPiServer Schedule Jobs should be periodically checked (in a loop for example) so that the execution can be terminated a short time after if the stop signal has been received. 
Periodically check the stop signal in the longest-running code block (the loop). Stop the execution if the stop signal has been received.

Overuse of PageTypes

Overuse of PageTypes in the system, resulting in code duplication. The ideas of using block types in EPiServer to reduce code duplication and rely on fewer page types. If multiple pages serve the same layout but different content, there should be one-page type and different blocks should be created to serve different content.
Use abstract class if there is a need for same looking multiple PageTypes.

Unused PageTypes

It is important to keep the CMS clean and it is recommended to clean up the system from the unused page and block types. 

EPiServer Translation 

EPiServer platform provides the flexibility of translating the hard-coded strings to multiple languages with resource XML files. Currently, the content type names, property names, descriptions and label texts on the pages are hardcoded with fixed strings. The values should be taken from the resource XML files instead, as this would better support a global website with multiple languages. 

Overuse of Block Controllers

Simple blocks without processing logic inside their controllers should not have dedicated controllers as this will degrade the performance. 

Avoid dynamic properties


Register UIDescriptor 

where you disable on-page edit and preview views

For settings kind of pages, ensure that it will not have a template


Use Episerver’s Object Cache instead of .NET’s built-in cache


For property (field) names in code, use standard .NET PascalCase. 

Make sure to set a friendly Display Name and Description

Set default values for Content Types’ Properties (if known)


For media properties, use the ContentReference type and the appropriate UIHint


Use the PropertyFor method to render properties (fields) in ContentType views


Use Container Pages for folder nodes, without presentation (if required) 

use IContainerPage

C. EPiServer Best Practices for Cloud Development & Deployment

Replace log4net with EPiServer.Logging 

Episerver DXP redirects all Episerver logs into .NET Diagnostic Trace if the code uses the Episerver logging abstraction EPiServer.Logging.LogManager. As the application is using log4net and deploying to DXP, replace log4net with EPiServer.Logging.

Reference:  


Keep a look at system requirements

Some of the EPiServer Add-ons, features or components are not compatible with Microsoft Azure, and it is recommended to keep a look at the system requirements of EPiServer Azure deployment to select appropriate add-ons, components or to choose a suitable approach of development.

 Take a look at EPiServer Azure (DXP) system requirements, remove incompatible add-ons, features and components. The system requirements are available at this reference link: http://world.episerver.com/digital-experience-cloud-service/requirements/ 

Avoid code that depends on specific on-premise resources 

 Code that was developed to run on a cloud website should not have any specific dependency on the on-premise resources, such as SMTP server, writing to local files and folders. Session state should be avoided, and if it is impossible to avoid the use of session state, Azure Session Affinity or other optimized session state providers for Azure should be considered.

Try to avoid any code that depends on specific on-premise resources, consider alternative options for them in Azure environment (for example, emails can be sent from SendGrid service, any logic which writes to the local files/folders should now use Azure Blob Storage instead) 

Security 

Read the following documentation for the level of security EPiServer provides https://world.episerver.com/documentation/developer-guides/CMS/security/
 
 It is recommended some level of protection should be there in the website against click-jacking. 

 Install and configure the NuGet package called NWebsec (https://www.nuget.org/packages/NWebsec)
February 08, 2021

Wednesday, 23 August 2017

How to Give Editors Access to Categories in Optimizely CMS 11

This approach applies to legacy Optimizely CMS / EPiServer solutions that use the classic admin interface and web.config-based access rules. It does not apply to CMS 12 or CMS 13.

Overview

In legacy EPiServer/Optimizely CMS implementations, editor and administrator features are separated into two interfaces: Edit and Admin.

Edit mode gives you fine-grained control over content permissions, but sometimes teams need to expose a specific admin feature, such as Categories, without granting full access to the entire admin interface.

A common requirement is to let editors manage categories while keeping the rest of the admin area restricted to administrator roles.

Grant Access to Categories Only

In the classic admin UI, access is controlled through web.config. By default, admin pages are usually limited to roles such as WebAdmins and Administrators.

If you want editor roles such as CmsEditors or WebEditors to access only the Categories screen, add the following configuration:

<location path="EPiServer/CMS/admin/Categories.aspx">
  <system.web>
    <authorization>
      <allow roles="CmsEditors, WebEditors, WebAdmins, Administrators" />
      <deny users="*" />
    </authorization>
  </system.web>
</location>

With this in place, users in the WebEditors role can open /EPiServer/CMS/admin/Categories.aspx directly without receiving access to other admin functionality.

Add a Navigation Link

Direct access works, but it is not very convenient for editors. A better experience is to surface Categories in the CMS navigation.

The simplest approach is to add a menu item in web.config:

<episerver.shell>
  <navigation>
    <add menupath="/global/Categories"
         sortindex="1000"
         text="Categories"
         url="/EPiServer/CMS/admin/Categories.aspx" />
  </navigation>
</episerver.shell>

This works, but there is one downside: the menu item is visible to all users, even if they do not have permission to access the Categories page.

Use a Role-Aware MenuProvider

If you want the Categories menu item to appear only for users who are actually allowed to use it, the better approach is to use a custom MenuProvider.

This lets you control both visibility and access in code, which keeps the navigation cleaner and avoids exposing irrelevant menu items to other users.

The following example adds a Categories submenu item under the CMS section with role-aware access control:

If you want Categories to appear as a top-level global item instead, change /global/cms/categories to /global/categories.

Why This Approach Is Useful

  • Editors get access to exactly one admin feature without broader admin permissions.
  • The navigation is more intuitive for content teams.
  • Role-aware menu items reduce confusion and keep the UI cleaner.

Learn More

If you are new to menu providers, the legacy Optimizely CMS 11 documentation is a good place to start: Extend the CMS navigation.

This is a useful pattern for older Optimizely CMS 11 projects where editors need access to selected admin tools without opening up the full admin interface.

August 23, 2017

How to Manually Clear Site and Page Cache in Optimizely CMS

This post focuses on content and object cache invalidation inside Optimizely CMS. It does not cover CDN cache, browser cache, or output cache.

Background

Most of the time, Optimizely handles cache invalidation automatically and very well. In normal publishing workflows, you usually do not need to clear cache manually.

That said, manual cache invalidation can still be useful in a few special cases: diagnostics, custom integrations, load-balanced proof-of-concepts, or controlled troubleshooting where you want to force content or object cache to refresh.

Below I have reordered the examples so the latest Optimizely CMS 12 and CMS 13 approach comes first, followed by older EPiServer examples for legacy projects.

Optimizely CMS 12 and CMS 13

For modern Optimizely CMS solutions, the recommended APIs are:

  • IContentCacheRemover for content cache invalidation
  • ISynchronizedObjectInstanceCache for general object cache invalidation

Clear the full content cache

using EPiServer;

public class CacheService
{
    private readonly IContentCacheRemover _contentCacheRemover;

    public CacheService(IContentCacheRemover contentCacheRemover)
    {
        _contentCacheRemover = contentCacheRemover;
    }

    public void ClearContentCache()
    {
        _contentCacheRemover.Clear();
    }
}

Clear the cache for a specific page or content item

using EPiServer;
using EPiServer.Core;

public class CacheService
{
    private readonly IContentCacheRemover _contentCacheRemover;

    public CacheService(IContentCacheRemover contentCacheRemover)
    {
        _contentCacheRemover = contentCacheRemover;
    }

    public void ClearSpecificContentCache()
    {
        _contentCacheRemover.Remove(ContentReference.StartPage);
    }
}

Clear a specific language branch

_contentCacheRemover.RemoveLanguage(ContentReference.StartPage, "en");

Clear the general object cache

If you previously used CacheManager, the newer approach is to use ISynchronizedObjectInstanceCache.

using EPiServer.Framework.Cache;

public class CacheService
{
    private readonly ISynchronizedObjectInstanceCache _synchronizedCache;

    public CacheService(ISynchronizedObjectInstanceCache synchronizedCache)
    {
        _synchronizedCache = synchronizedCache;
    }

    public void ClearObjectCache()
    {
        _synchronizedCache.Clear();
    }
}

Example endpoint for internal diagnostics

using EPiServer;
using EPiServer.Core;
using EPiServer.Framework.Cache;
using Microsoft.AspNetCore.Mvc;

[Route("util/cache")]
public class CacheController : Controller
{
    private readonly IContentCacheRemover _contentCacheRemover;
    private readonly ISynchronizedObjectInstanceCache _synchronizedCache;

    public CacheController(
        IContentCacheRemover contentCacheRemover,
        ISynchronizedObjectInstanceCache synchronizedCache)
    {
        _contentCacheRemover = contentCacheRemover;
        _synchronizedCache = synchronizedCache;
    }

    [HttpPost("content/clear")]
    public IActionResult ClearContentCache()
    {
        _contentCacheRemover.Clear();
        return Content("Ok, content cache cleared.");
    }

    [HttpPost("content/{id:int}")]
    public IActionResult ClearContent(int id)
    {
        _contentCacheRemover.Remove(new ContentReference(id));
        return Content("Ok, content cache cleared.");
    }

    [HttpPost("object/clear")]
    public IActionResult ClearObjectCache()
    {
        _synchronizedCache.Clear();
        return Content("Ok, object cache cleared.");
    }
}

Legacy EPiServer Examples

The examples below are for older EPiServer projects and are mainly useful when working with legacy CMS 6 to CMS 11 solutions.

Invalidate the cache for a specific EPiServer page

EPiServer 9.9+

var contentCacheRemover = ServiceLocator.Current.GetInstance<EPiServer.IContentCacheRemover>();
contentCacheRemover.Remove(ContentReference.StartPage);

EPiServer 6

DataFactoryCache.RemovePage(ContentReference.StartPage);

Invalidate the cache for an EPiServer site on a server

EPiServer 7

EPiServer.CacheManager.Clear();

EPiServer 6

EPiServer.DataFactoryCache.Clear();

A small web service to invalidate site cache:

public void ProcessRequest(HttpContext context)
{
    EPiServer.CacheManager.Clear();
    context.Response.ContentType = "text/plain";
    context.Response.Write("Ok, site cache cleared.");
}

Final Note

For most projects, it is best to let Optimizely manage cache invalidation automatically. Manual cache clearing should be reserved for troubleshooting, controlled utilities, and special integration scenarios.

Thanks to Wałdis Iljuczonok for previously highlighting the newer public API approach using IContentCacheRemover instead of older cache APIs.

August 23, 2017

Thursday, 13 July 2017

How to Fix SQL72014 and SQL72045 When Importing a BACPAC

Problem

I was importing a BACPAC generated on another server into my local development environment using SQL Server Management Studio and ran into the following errors:

TITLE: Microsoft SQL Server Management Studio
------------------------------

Could not import package.
Warning SQL0: A project which specifies Microsoft Azure SQL Database v12 as the target platform may experience compatibility issues with SQL Server 2014.
Warning SQL72012: The object [databaseXYZ_Data] exists in the target, but it will not be dropped even though you selected the 'Generate drop statements for objects that are in the target database but that are not in the source check box.
Warning SQL72012: The object [databaseXYZ_Log] exists in the target, but it will not be dropped even though you selected the 'Generate drop statements for objects that are in the target database but that are not in the source check box.
Error SQL72014: .Net SqlClient Data Provider: Msg 12824, Level 16, State 1, Line 5 The sp_configure value 'contained database authentication' must be set to 1 in order to alter a contained database. You may need to use RECONFIGURE to set the value_in_use.
Error SQL72045: Script execution error. The executed script:
IF EXISTS (SELECT 1
FROM   [master].[dbo].[sysdatabases]
WHERE  [name] = N'$(DatabaseName)')
BEGIN
ALTER DATABASE [$(DatabaseName)]
SET CONTAINMENT = PARTIAL
WITH ROLLBACK IMMEDIATE;
END

Error SQL72014: .Net SqlClient Data Provider: Msg 5069, Level 16, State 1, Line 5 ALTER DATABASE statement failed.
Error SQL72045: Script execution error. The executed script:
IF EXISTS (SELECT 1
FROM   [master].[dbo].[sysdatabases]
WHERE  [name] = N'$(DatabaseName)')
BEGIN
ALTER DATABASE [$(DatabaseName)]
SET CONTAINMENT = PARTIAL
WITH ROLLBACK IMMEDIATE;
END

(Microsoft.SqlServer.Dac)

Fix

Run the following T-SQL on the target SQL Server instance before importing the BACPAC:

EXEC sp_configure 'contained database authentication', 1;
GO
RECONFIGURE;
GO

Once that setting is enabled, rerun the import.

Why This Happens

The import process is trying to set the target database to partial containment:

ALTER DATABASE [YourDatabaseName]
SET CONTAINMENT = PARTIAL;

If contained database authentication is disabled on the SQL Server instance, that step fails and the import stops with SQL72014 and SQL72045.

This often happens when the BACPAC comes from Azure SQL Database and is being imported into a local SQL Server environment, where contained database authentication may be turned off by default.

Explanation

At first I suspected the issue might be caused by a corrupt BACPAC or Transparent Data Encryption (TDE), but the real cause was much simpler: the import required support for a contained database.

A partially contained database reduces dependencies on the master database and allows authentication and configuration to live more at the database level rather than relying entirely on server-level logins.

Because this has security implications, SQL Server does not always enable it by default on local or on-premises instances.

Notes

  • SQL72014 and SQL72045 are the main errors causing the import failure.
  • The SQL72012 warnings about data and log objects are not the root cause here.
  • For most local development environments, enabling contained database authentication is enough to complete the import successfully.

Further Reading

July 13, 2017

Sunday, 14 February 2016

How to Upload Files and Folders to Amazon S3 from PowerShell

Overview

If you need to upload files or entire folder structures to Amazon S3 from PowerShell, you no longer need to write your own recursive upload function unless you have very specific custom logic.

Today, there are two practical approaches:

  • AWS CLI v2 – the simplest and most flexible option for most developers.
  • AWS Tools for PowerShell – a good choice if you want to stay fully inside PowerShell cmdlets.

For most cases, I recommend AWS CLI v2.

Important Note About “Folders” in S3

Amazon S3 does not store folders the same way a normal file system does. S3 stores objects by key, and folder-like paths are created through key prefixes such as images/logo.png or releases/v1/app.zip.

That means you do not need to create folders manually before uploading files. If you upload an object to a key like photos/abc.png, S3 will display that path as a folder structure in the console.

Option 1. Use AWS CLI v2 (Recommended)

The modern way to upload files and folders to S3 from PowerShell is to use AWS CLI version 2.

This approach is faster, easier to maintain, and much better than building your own recursive upload script.

Install AWS CLI

Install the latest AWS CLI v2 on your Windows machine before continuing.

Configure Credentials

The old approach of hardcoding AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY in scripts is no longer a good default.

Instead, use a named profile:

aws configure --profile my-s3-profile

If your organisation uses AWS IAM Identity Center (SSO), use:

aws configure sso --profile my-sso-profile
aws sso login --profile my-sso-profile

This keeps credentials out of your script and makes your setup easier to reuse.

Upload a Single File

aws s3 cp "C:\Files\logo.png" "s3://my-bucket/assets/logo.png" --profile my-s3-profile

Upload an Entire Folder

For whole directories, sync is usually better than cp --recursive because it only uploads new or changed files.

aws s3 sync "C:\MyDirectory" "s3://my-bucket/releases/v1/" --profile my-s3-profile

Upload Only Certain File Types

aws s3 sync "C:\MyDirectory" "s3://my-bucket/images/" --exclude "*" --include "*.jpg" --include "*.png" --profile my-s3-profile

Preview Before Uploading

If you want to see what will happen before making changes, use --dryrun:

aws s3 sync "C:\MyDirectory" "s3://my-bucket/releases/v1/" --dryrun --profile my-s3-profile

Mirror a Folder Exactly

If you want S3 to match your local folder exactly, add --delete:

aws s3 sync "C:\MyDirectory" "s3://my-bucket/releases/v1/" --delete --profile my-s3-profile

Be careful: --delete removes files in the target that no longer exist in the source.

Use a Profile in PowerShell

If you do not want to pass --profile on every command, set it in your current PowerShell session:

$env:AWS_PROFILE = "my-s3-profile"
aws s3 sync "C:\MyDirectory" "s3://my-bucket/releases/v1/"

Option 2. Use AWS Tools for PowerShell

If you prefer native PowerShell cmdlets instead of the AWS CLI, you can use AWS Tools for PowerShell.

Install the S3 Module

Install-Module AWS.Tools.S3 -Scope CurrentUser

Use an Existing AWS Profile

Set-AWSCredential -ProfileName my-s3-profile

Upload a Single File

Write-S3Object -BucketName "my-bucket" -Key "assets/logo.png" -File "C:\Files\logo.png"

Upload a Folder Recursively

Write-S3Object -BucketName "my-bucket" -Folder "C:\MyDirectory" -KeyPrefix "releases/v1/" -Recurse

Which Option Should You Use?

  • Use AWS CLI v2 if you want the most common and portable approach.
  • Use AWS Tools for PowerShell if you prefer PowerShell cmdlets and are already working in PowerShell-heavy automation.

Final Thoughts

If your goal is simply to upload or sync files and folders to S3, the built-in AWS tools are much better than writing your own recursive function. They are faster, safer, easier to read, and easier to maintain.

My recommendation today would be simple: use aws s3 sync for folders, use aws s3 cp for single files, and avoid storing access keys directly inside scripts unless you have a very specific reason to do so.

February 14, 2016

Wednesday, 30 December 2015

How to Recursively List All Files and Subfolders Using PowerShell

Here is a simple recursive function to display all file-names with full path in folders and sub-folders. You can only run this command in Windows PowerShell available since the release of Windows 7.
$folder = "c:\\MyDirectory\\"

 Function Upload($item) {
    foreach ($i in Get-ChildItem $item)
    {
        Try
        {
            if((Get-Item $i.FullName) -is [System.IO.DirectoryInfo]){
                  Write-Output $i.FullName
                  Upload($i.FullName)

            }else{
              Write-Output $i.FullName
            }
        }catch{
            Write-Output $i.FullName
        }
    }   
}
 
Upload($folder)

Copy the above code and save in .ps1 file.

December 30, 2015

How to Batch Rename All Files in a Folder Using PowerShell

If you are not interested in external programs to renames all files in a folder to lowercase, there is a simple command for you.

- Open Command Prompt (cmd.exe) in Windows
- Go to the directory and run the following command

for /f "Tokens=*" %f in ('dir /l/b/a-d') do (rename "%f" "%f")

Note: This is not a recursive function, it will rename files to lowercase only on the directory where you will run the command.

December 30, 2015

Sunday, 8 November 2015

How to Create a Yozio SubLink Using C#

If you need to read more about Yozio Sublink API, here is the documentation http://docs.yozio.com/articles/sublink-apis
Below is a code to create Yozio Sublink.
using System;
using System.Net;
using Newtonsoft.Json;

namespace Yozio
{
    public class YozioResult
    {
        public string status { get; set; }
        public Body body { get; set; }
    }
 
    public class Body
    {
        public string sub_link { get; set; }
        public object link_alias { get; set; }
        public MetaData meta_data { get; set; }
        public long timestamp { get; set; }
    }
 
    public class MetaData
    {
        public string utm_source { get; set; }
        public string utm_medium { get; set; }
        public string utm_campaign { get; set; }
    }
 
    public static class YozioApi
    {
        public static YozioResult SubLink
        {
            get
            {
                const string apiKey = "YOUR-YOZIO-API-KEY";
                const string yozioSuperLink = "YOZIO-SUPER-LINK"; //e.gk7.k.cf

                const string urlString = "http://api.yozio.com/v2.0/?app_key={0}&amp;" +
                                         "method=sub.link.create&amp;" +
                                         "short_url={1}&amp;" +
                                         "reassign_old_alias_to_this_link=true&amp;" +
                                         "meta_data[utm_source]=web&amp;" +
                                         "meta_data[utm_medium]=link&amp;" +
                                         "meta_data[utm_campaign]=blog";

                var url = string.Format(urlString, apiKey, yozioSuperLink);

                var client = new WebClient();
                client.Headers.Add("user-agent", "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.2; .NET CLR 1.0.3705;)");
                var json = client.DownloadString(new Uri(url));
                var result = JsonConvert.DeserializeObject<yozioresult>(json);
                return result;
            }
        }
    }
}
Usage: Getting Sublink
Yozio.YozioApi.SubLink.body.sub_link;

/Adnan
November 08, 2015

Wednesday, 7 October 2015

Wednesday, 9 September 2015

How to Stop Visual Studio 2015 Creating Backup Folders During Project Migration

I was in the process of migrating over to Visual Studio 2015 from Visual Studio 2013. When I executed a local command line build, I received the following error.
Microsoft Visual Studio 2015 Version 14.0.23107.0.
Copyright (C) Microsoft Corp. All rights reserved.
Solution file 'xyz.sln' is from a previous version of this application and must 
be migrated in order to build in this version of the application. 
To migrate the solution, open the solution in this version of the application.
Migration completed successfully, but some warnings were detected during migration.
For more information, see the migration report:  UpgradeLog06.htm
I opened the solution using Visual Studio 2015, and got the migration report. I performed the solution clean followed by solution rebuild and everything works fine. On closing the solution, and performing a local command line build again I got the same error message.
Every time I open the solution using VS 2015 a new "Backup" folder is created with iteration and a new migration report is displayed.
The migration report shows 8 projects that have the following 5 warnings.

Visual Studio needs to make non-functional changes to this project in 
order to enable the project to open in Visual Studio 2015, Visual Studio 2013, 
Visual Studio 2012, and Visual Studio 2010 SP1 without impacting project behavior
The solution I was opening in Visual studio 2015 has around 35 projects and every-time a backup folder is created, it takes space in disk and the make changes in solution file. It was quite a hassle to check-in solution file to source control with backup folder information. So I started digging around how to stop this backup folder nonsense, and after some soul searching I finally fixed it.

Here is the solution:
- Firstly, open your project file (.csproj) in text editor.
- Find the following two lines as shown in the image and delete them.

- Then save the solution file.
Now, open your file in visual studio 2015 again. No more new backup folders and migration reports. It is safe to delete the old backup folders(s) and associated HTML file(s).

/Adnan
September 09, 2015

Saturday, 16 May 2015

How to Get Random Items from an Array or List in C#

The simple way to get random item from an Array is to use the return value from random.next(0, array.length) as index to get value from the array.

var randomIndex = random.Next(0, Array.Length);
Console.Write(Array[randomIndex]);
The downside of the above code is it might return you item multiple times (repetition) as we don't keep track of items that we are getting from the source Array.

The easy approach is to consider Array as a deck of cards. We want the items to be 'shuffled' similar to a deck of cards, meaning avoiding any repetition. So we will use  a List<> for the source items, grab them at random and push them to a Stack<> to create the deck of items.

You can create a Stack from anything that is IEnumerable
var stack = new Stack(myList);
See MSDN: http://msdn.microsoft.com/en-us/library/76atxd68.aspx

However, the stack constructor will be using a loop internally, you just don't see it. So for understanding the purpose I will use an example to create Stack with loop.

public static Stack CreateShuffledDeck(IEnumerable values) {

var random = new Random();  var list = new List(values); var stack = new Stack();  while (list.Count > 0) {  // Get the next item at random. var randomIndex = random .Next(0, list.Count); var randomItem = list[randomIndex];  // Remove the item from the list and push it to the top of the deck. list.RemoveAt(randomIndex); stack.Push(randomItem ); }  return stack; } 
Now we have a solution to create a Shuffled Deck. We can now get random items out using Stack.Pop method . Popping something from the stack means "taking the top 'thing'" off the stack.

public static string[] RandomArrayEntries(string[] arrayItems, int count) {
var listToReturn = new List();

if (arrayItems.Length != count) {
var deck = CreateShuffledDeck(arrayItems);

for (var i = 0; i < count; i++) {
var arrayItems= deck.Pop();
listToReturn .Add(item);
}

return listToReturn .ToArray();
}

return arrayItems;
}
We can execute the above code as following:

var countriesArray = new string[] { "Sweden", "Pakistan", "United Kingdom", "Denmark", "Norway", "Finland" };

var newRandomAraay = RandomArrayEntries(countriesArray, 3);

/Adnan
May 16, 2015

Tuesday, 9 December 2014

How to Change the Default Font in Blogger

If you are tired of writing your blog posts in Times New Roman font and don't want to add bloated HTML by choosing the font and size from Blogger's post editor, this post is right place for you. The solution is simple if you know where to look. Below are two ways you can try.
Chrome Browser Settings
As Blogger is a part of Google family, changing the font is actually a Chrome setting, not a Blogger setting.

To change your font setting do the following
1. Chrome browser
2. Settings
3. Show Advanced Settings
4. Web Content: Customize Fonts
Here is a bonus part, though it does set your default font & size in Blogger, it also changes it all over Google.

CSS Way
Alternatively if you don't want to change chrome browser settings, you can make CSS work for you.

1. Sign in to your blogger account.
2. Select your blog.
3. On right hand menu click Templates and then Edit HTML.
4. Locate or search for <b:skin> and copy/paste the following CSS code inside.

* { font-family: Arial!important;}
or
body {
font-family: Arial!important;
}

5. Click Save template.

Now all the old and new blog posts in your blog will be in the font you specified through CSS.


/Adnan
December 09, 2014

Saturday, 6 December 2014

How to Fix HttpContextBase Errors in Facebook OAuth for ASP.NET

This fix applies to legacy ASP.NET Web Forms or ASP.NET MVC applications running on .NET Framework and using DotNetOpenAuth. If you are building a new application on ASP.NET Core, use the built-in external authentication providers instead of this older pattern.

Problem

I was helping a friend wire up Facebook OAuth login in an older ASP.NET application using the DotNetOpenAuth extensions installed from NuGet.

The code looked like this:

Uri ui = new Uri("~/Login.aspx", UriKind.Relative);

var fbClient = new DotNetOpenAuth.AspNet.Clients.FacebookClient("***", "***********");
fbClient.RequestAuthentication(context, ui);

The problem is that RequestAuthentication expects an instance of HttpContextBase.

If you try to pass HttpContext.Current directly, it fails because HttpContext.Current is a HttpContext, not a HttpContextBase.

Why This Happens

HttpContextBase was introduced as an abstraction over HttpContext. This makes ASP.NET code easier to test and easier to work with in components that should not depend directly on the concrete runtime context.

To bridge the gap between the two types, ASP.NET provides HttpContextWrapper.

Solution

Wrap HttpContext.Current in a HttpContextWrapper before calling RequestAuthentication:

var httpContextBase = new HttpContextWrapper(HttpContext.Current);
fbClient.RequestAuthentication(httpContextBase, ui);

Explanation

HttpContextWrapper acts as an adapter. It takes the current ASP.NET request context and exposes it as a HttpContextBase, which is exactly what the DotNetOpenAuth API expects.

So if you are maintaining a legacy ASP.NET application and run into a type mismatch between HttpContext and HttpContextBase, this wrapper is the correct fix.

Modern Note

For new applications, this is no longer the recommended approach. In modern ASP.NET Core applications, external login providers such as Facebook are configured through the built-in authentication middleware, and System.Web, HttpContextBase, and HttpContextWrapper are not part of that model.

December 06, 2014

Thursday, 27 November 2014

How to Render ASP.NET MVC Views to an HTML String

A common need I have in my ASP.NET MVC based projects is to render a complete "View" or "PartialView" to string instead of the HTTP response and then present it or embed it in another rendered view.

You can implement the following code in shared controller or preferably in base Controller so that you can access this function in all controllers across your project. You can access the ControllerContext within controller and pass it to the function. It will return the rendered view in HTML string, the usage is self-explanatory.
public static string RenderViewToString(string viewName, object model) 
{
 if (string.IsNullOrEmpty(viewName)) 
     viewName = ControllerContext.RouteData.GetRequiredString("action");

 ViewData.Model = model;
 using(StringWriter sw = new StringWriter()) 
 {
  ViewEngineResult viewResult = ViewEngines.Engines.FindPartialView(ControllerContext, viewName);
  ViewContext viewContext = new ViewContext(ControllerContext, viewResult.View, ViewData, TempData, sw);
  viewResult.View.Render(viewContext, sw);
  return sw.GetStringBuilder().ToString();
 }
}

ControllerContext can be access using following method.

ControllerContext.RouteData.GetRequiredString("action");

If you want to put the function in a helper class you have to pass the ControllerContext from controller to the function.

public static string RenderViewToString(ControllerContext context, string viewName, object model) 
{
 if (string.IsNullOrEmpty(viewName)) 
     viewName = context.RouteData.GetRequiredString("action");
 
 var viewData = new ViewDataDictionary(model);
 using(var sw = new StringWriter()) 
 {
  var viewResult = ViewEngines.Engines.FindPartialView(context, viewName);
  var viewContext = new ViewContext(context, viewResult.View, viewData, new TempDataDictionary(), sw);
  viewResult.View.Render(viewContext, sw);
  return sw.GetStringBuilder().ToString();
 }
}

Call the function in your Action Method

//Somewhere in HomeController
public ActionResult Index() 
{
 //Second parameter(model) can be null
 var context = ControllerContext.RouteData.GetRequiredString("action");
 var content = RenderViewToString(context, "profile", new ProfileModel());
 //var content = RenderViewToString("profile", new ProfileModel());

 
        //Do something with the content, e.g.get profile specific template and send it to e-mail


 //This does nothing to do with rendered string
 return View();
}


/Adnan

November 27, 2014