Skip to content

Instantly share code, notes, and snippets.

@PNergard
Created January 30, 2026 13:06
Show Gist options
  • Select an option

  • Save PNergard/818677955ae0e2c933f30a3186e0fe37 to your computer and use it in GitHub Desktop.

Select an option

Save PNergard/818677955ae0e2c933f30a3186e0fe37 to your computer and use it in GitHub Desktop.
Optimizely scheduled job for deletion of a content type and all related content instances
using EPiServer;
using EPiServer.Core;
using EPiServer.DataAbstraction;
using EPiServer.PlugIn;
using EPiServer.Scheduler;
using EPiServer.Security;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace Pure.MVC.Business.ScheduledJobs
{
/// <summary>
/// !!! WARNING: THIS JOB IS DESTRUCTIVE !!!
///
/// This job permanently deletes content types and ALL content associated with them.
/// It is highly recommended to TAKE A DATABASE BACKUP before running this job.
///
/// functionality:
/// 1. Takes a list of ContentTypeIDs.
/// 2. Finds all content usages for each type.
/// 3. Force deletes all found content instances (bypassing access checks).
/// 4. Deletes the Content Type definition itself.
///
/// Configure the 'ContentTypeIdsToDelete' constant below with the IDs you wish to obliterate.
/// </summary>
[ScheduledPlugIn(
DisplayName = "!!! DANGEROUS Content Type Obliterator",
Description = "WARNING: Permanently deletes specified Content Types and ALL existing content of those types. This is a definite action.")]
public class ContentTypeObliteratorJob : ScheduledJobBase
{
private bool _stopSignaled;
private readonly IContentTypeRepository _contentTypeRepository;
private readonly IContentModelUsage _contentModelUsage;
private readonly IContentRepository _contentRepository;
// Configuration: Comma-separated list of ContentTypeIds to OBLITERATE
// Example: "42, 105, 88"
private const string ContentTypeIdsToDelete = "28";
public ContentTypeObliteratorJob(
IContentTypeRepository contentTypeRepository,
IContentModelUsage contentModelUsage,
IContentRepository contentRepository)
{
_contentTypeRepository = contentTypeRepository;
_contentModelUsage = contentModelUsage;
_contentRepository = contentRepository;
IsStoppable = true;
}
public override void Stop()
{
_stopSignaled = true;
}
public override string Execute()
{
// Parse configuration
var contentTypesIds = ParseContentTypeIds(ContentTypeIdsToDelete);
if (!contentTypesIds.Any())
{
return "No ContentType IDs configured in code. Nothing was deleted.";
}
OnStatusChanged($"Starting obliteration of {contentTypesIds.Count} content types.");
var resultBuilder = new StringBuilder();
resultBuilder.AppendLine("Obliteration Report:");
int typesProcessed = 0;
foreach (var contentTypeId in contentTypesIds)
{
if (_stopSignaled) return "Job stopped by user.";
typesProcessed++;
var contentType = _contentTypeRepository.Load(contentTypeId);
if (contentType == null)
{
resultBuilder.AppendLine($"[ID: {contentTypeId}] - Content Type not found. Skipped.");
continue;
}
OnStatusChanged($"Processing type {typesProcessed}/{contentTypesIds.Count}: {contentType.Name} (ID: {contentType.ID})");
try
{
// 1. Find all usages
var usages = _contentModelUsage.ListContentOfContentType(contentType);
var usageList = usages?.ToList() ?? new List<ContentUsage>();
int successDeleteCount = 0;
int failDeleteCount = 0;
var contentErrors = new List<string>();
// 2. Delete all content instances
foreach (var usage in usageList)
{
if (_stopSignaled) return "Job stopped by user.";
try
{
// Delete with force=true and AccessLevel.NoAccess to bypass security
_contentRepository.Delete(usage.ContentLink, forceDelete: true, access: AccessLevel.NoAccess);
successDeleteCount++;
}
catch (Exception ex)
{
failDeleteCount++;
// Capture first few errors to avoid flooding
if (contentErrors.Count < 5)
{
contentErrors.Add($"Content {usage.ContentLink.ID}: {ex.Message}");
}
}
}
// 3. Delete the Content Type itself
string typeDeletionStatus;
try
{
// Only attempt to delete the type if we think we cleared the content,
// though we try regardless in case previous runs left it empty.
_contentTypeRepository.Delete(contentType.ID);
typeDeletionStatus = "Type Deleted";
}
catch (Exception ex)
{
typeDeletionStatus = $"Type Delete Failed: {ex.Message}";
}
// Report for this type
resultBuilder.AppendLine($"[ID: {contentType.ID}] {contentType.Name}:");
resultBuilder.AppendLine($" - Content Items Deleted: {successDeleteCount}");
if (failDeleteCount > 0)
{
resultBuilder.AppendLine($" - Content Deletion Failures: {failDeleteCount}");
foreach (var error in contentErrors)
{
resultBuilder.AppendLine($" * {error}");
}
}
resultBuilder.AppendLine($" - Type Status: {typeDeletionStatus}");
resultBuilder.AppendLine("--------------------------------------------------");
}
catch (Exception ex)
{
resultBuilder.AppendLine($"[ID: {contentTypeId}] FATAL ERROR processing type: {ex.Message}");
}
}
return resultBuilder.ToString();
}
private HashSet<int> ParseContentTypeIds(string contentTypeIds)
{
var result = new HashSet<int>();
if (string.IsNullOrWhiteSpace(contentTypeIds))
return result;
foreach (var idStr in contentTypeIds.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries))
{
if (int.TryParse(idStr.Trim(), out int id))
{
result.Add(id);
}
}
return result;
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment