Install
npx skillscat add twofoldtech-dakota/claude-marketplace/backend-csharp Install via the SkillsCat registry.
About this skill
This skill provides C# backend patterns for Optimizely CMS projects, covering dependency injection, async programming, and service layer architecture. It helps developers structure their code consistently and follow best practices when building backend services for Optimizely CMS applications. Use this skill when implementing or maintaining C# backend components in Optimizely CMS projects.
SKILL.md
Backend C# Patterns
Overview
This skill covers C# backend patterns commonly used in Optimizely CMS projects including dependency injection, async patterns, and service layer architecture.
Dependency Injection
Service Registration
// Program.cs or Startup.cs
services.AddScoped<IArticleService, ArticleService>();
services.AddScoped<ISearchService, SearchService>();
services.AddSingleton<ICacheService, MemoryCacheService>();
services.AddTransient<IEmailService, EmailService>();
// Options pattern
services.Configure<EmailOptions>(Configuration.GetSection("Email"));
services.Configure<SearchOptions>(Configuration.GetSection("Search"));Constructor Injection
public class ArticleService : IArticleService
{
private readonly IContentLoader _contentLoader;
private readonly IUrlResolver _urlResolver;
private readonly ILogger<ArticleService> _logger;
private readonly IOptions<ArticleOptions> _options;
public ArticleService(
IContentLoader contentLoader,
IUrlResolver urlResolver,
ILogger<ArticleService> logger,
IOptions<ArticleOptions> options)
{
_contentLoader = contentLoader;
_urlResolver = urlResolver;
_logger = logger;
_options = options;
}
}Async Patterns
Async Service Methods
public interface IArticleService
{
Task<ArticleViewModel> GetArticleAsync(
ContentReference contentLink,
CancellationToken cancellationToken = default);
Task<IEnumerable<ArticleViewModel>> GetRelatedArticlesAsync(
ContentReference contentLink,
int count = 5,
CancellationToken cancellationToken = default);
}
public class ArticleService : IArticleService
{
public async Task<ArticleViewModel> GetArticleAsync(
ContentReference contentLink,
CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
var article = _contentLoader.Get<ArticlePage>(contentLink);
return await MapToViewModelAsync(article, cancellationToken);
}
}Parallel Operations
public async Task<PageViewModel> BuildPageModelAsync(
PageData page,
CancellationToken cancellationToken = default)
{
// Run independent operations in parallel
var relatedTask = GetRelatedContentAsync(page.ContentLink, cancellationToken);
var metaTask = GetMetaDataAsync(page, cancellationToken);
var breadcrumbTask = GetBreadcrumbsAsync(page.ContentLink, cancellationToken);
await Task.WhenAll(relatedTask, metaTask, breadcrumbTask);
return new PageViewModel
{
RelatedContent = await relatedTask,
MetaData = await metaTask,
Breadcrumbs = await breadcrumbTask
};
}Service Layer
Interface Definition
public interface IArticleService
{
ArticleViewModel GetArticle(ContentReference contentLink);
IEnumerable<ArticleViewModel> GetRelatedArticles(ContentReference contentLink, int count = 5);
IEnumerable<ArticleViewModel> SearchArticles(string query, int page = 1, int pageSize = 10);
}Implementation
public class ArticleService : IArticleService
{
private readonly IContentLoader _contentLoader;
private readonly IUrlResolver _urlResolver;
private readonly ILogger<ArticleService> _logger;
public ArticleService(
IContentLoader contentLoader,
IUrlResolver urlResolver,
ILogger<ArticleService> logger)
{
_contentLoader = contentLoader;
_urlResolver = urlResolver;
_logger = logger;
}
public ArticleViewModel GetArticle(ContentReference contentLink)
{
try
{
var article = _contentLoader.Get<ArticlePage>(contentLink);
return MapToViewModel(article);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to get article {ContentLink}", contentLink);
throw;
}
}
public IEnumerable<ArticleViewModel> GetRelatedArticles(
ContentReference contentLink,
int count = 5)
{
var article = _contentLoader.Get<ArticlePage>(contentLink);
return _contentLoader.GetChildren<ArticlePage>(article.ParentLink)
.Where(a => a.ContentLink != contentLink)
.OrderByDescending(a => a.StartPublish)
.Take(count)
.Select(MapToViewModel);
}
private ArticleViewModel MapToViewModel(ArticlePage article)
{
return new ArticleViewModel
{
Id = article.ContentLink.ID,
Heading = article.Heading,
Body = article.MainBody?.ToHtmlString(),
Url = _urlResolver.GetUrl(article.ContentLink),
PublishedDate = article.StartPublish
};
}
}Error Handling
Structured Logging
public class ArticleService : IArticleService
{
public ArticleViewModel GetArticle(ContentReference contentLink)
{
using var scope = _logger.BeginScope(new Dictionary<string, object>
{
["ContentLink"] = contentLink,
["Operation"] = "GetArticle"
});
try
{
_logger.LogDebug("Retrieving article {ContentLink}", contentLink);
var article = _contentLoader.Get<ArticlePage>(contentLink);
_logger.LogInformation(
"Successfully retrieved article {ContentLink} - {Title}",
contentLink,
article.Heading);
return MapToViewModel(article);
}
catch (ContentNotFoundException ex)
{
_logger.LogWarning(ex,
"Article not found: {ContentLink}",
contentLink);
throw;
}
catch (Exception ex)
{
_logger.LogError(ex,
"Unexpected error retrieving article {ContentLink}",
contentLink);
throw;
}
}
}Custom Exceptions
public class ArticleNotFoundException : Exception
{
public ContentReference ContentLink { get; }
public ArticleNotFoundException(ContentReference contentLink)
: base($"Article not found: {contentLink}")
{
ContentLink = contentLink;
}
}
public class ArticleValidationException : Exception
{
public IEnumerable<string> Errors { get; }
public ArticleValidationException(IEnumerable<string> errors)
: base("Article validation failed")
{
Errors = errors;
}
}Caching
Memory Cache
public class CachedArticleService : IArticleService
{
private readonly IArticleService _innerService;
private readonly IMemoryCache _cache;
private readonly IContentEvents _contentEvents;
public CachedArticleService(
IArticleService innerService,
IMemoryCache cache,
IContentEvents contentEvents)
{
_innerService = innerService;
_cache = cache;
_contentEvents = contentEvents;
_contentEvents.PublishedContent += OnContentPublished;
}
public ArticleViewModel GetArticle(ContentReference contentLink)
{
var cacheKey = $"article:{contentLink}";
return _cache.GetOrCreate(cacheKey, entry =>
{
entry.SlidingExpiration = TimeSpan.FromMinutes(10);
entry.Priority = CacheItemPriority.Normal;
return _innerService.GetArticle(contentLink);
});
}
private void OnContentPublished(object sender, ContentEventArgs e)
{
_cache.Remove($"article:{e.ContentLink}");
}
}Best Practices
- Use constructor injection for all dependencies
- Make methods async when doing I/O operations
- Include CancellationToken in async method signatures
- Use structured logging with meaningful context
- Implement caching with proper invalidation
- Keep controllers thin - delegate to services
- Use the Options pattern for configuration