Created
January 30, 2026 13:06
-
-
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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