Enhance Kentico Xperience development efforts with a fluent, intuitive API designed for efficient content querying. XperienceCommunity.DataContext simplifies the process of building queries against Kentico’s ContentItemQueryBuilder by abstracting its complexity behind a fluent, strongly-typed interface. Built on .NET 8 and fully integrated with Xperience by Kentico, this library improves productivity during local development and testing, allowing teams to streamline content management operations and accelerate project delivery.
$ dotnet add package XperienceCommunity.DataContextEnhance your Kentico Xperience development with a fluent API for intuitive and efficient query building. This project abstracts the built-in ContentItemQueryBuilder, leveraging .NET 6/.NET 8/.NET 9 and integrated with Xperience By Kentico, to improve your local development and testing workflow.
IContentItemContext<T> - For content hub items with strongly-typed queryingIPageContentContext<T> - For web pages with channel and path filtering capabilitiesIReusableSchemaContext<T> - For reusable schemas supporting both classes and interfacesIXperienceDataContext for centralized access to all context typesReusableSchemaContext for maximum flexibilityXperienceCommunity.DataContext provides three specialized contexts for different content scenarios:
IContentItemContext<T>)T must implement IContentItemFieldSourceIPageContentContext<T>)T must implement IWebPageFieldsSourceIReusableSchemaContext<T>)IXperienceDataContext)ForContentType<T>(), ForPageContentType<T>(), ForReusableSchema<T>()Before you begin, ensure you have met the following requirements:
To integrate XperienceCommunity.DataContext into your Kentico Xperience project, follow these steps:
NuGet Package: Install the NuGet package via the Package Manager Console:
Install-Package XperienceCommunity.DataContext
Or via the .NET CLI:
dotnet add package XperienceCommunity.DataContext
Or add it directly to your .csproj file:
<PackageReference Include="XperienceCommunity.Data.Context" Version="[latest-version]" />
Configure Services:
Prerequisites: Ensure Xperience by Kentico services are registered first:
// Required Kentico services (typically already configured in Xperience projects)
builder.Services.AddKentico();
builder.Services.AddKenticoFeatures();
Register XperienceCommunity.DataContext:
// Method 1: Simple registration with optional cache timeout
builder.Services.AddXperienceDataContext(cacheInMinutes: 30);
// Method 2: Fluent builder with processors
builder.Services.AddXperienceDataContext()
.AddContentItemProcessor<BlogPost, BlogPostProcessor>()
.AddPageContentProcessor<LandingPage, LandingPageProcessor>()
.SetCacheTimeout(30);
// Method 3: Basic registration (uses default 15-minute cache)
builder.Services.AddXperienceDataContext();
Required Dependencies: The library depends on these Kentico services being available:
IProgressiveCache - For caching query resultsIWebsiteChannelContext - For channel and preview contextIContentQueryExecutor - For executing Kentico content queriesThese are automatically provided when you call AddKentico() in a standard Xperience by Kentico project.
IContentItemContext into a ClassTo leverage the IContentItemContext in your classes, you need to inject it via dependency injection. The IContentItemContext requires a class that implements the IContentItemFieldSource interface. For instance, you might have a GenericContent class designed for the Content Hub.
Assuming you have a GenericContent class that implements IContentItemFieldSource, you can inject the IContentItemContext<GenericContent> into your classes as follows:
public class MyService
{
private readonly IContentItemContext<GenericContent> _contentItemContext;
public MyService(IContentItemContext<GenericContent> contentItemContext)
{
_contentItemContext = contentItemContext;
}
// Example method using the _contentItemContext
public async Task<GenericContent?> GetContentItemAsync(Guid contentItemGUID)
{
return await _contentItemContext
.FirstOrDefaultAsync(x => x.SystemFields.ContentItemGUID == contentItemGUID);
}
}This setup allows you to utilize the fluent API provided by IContentItemContext to interact with content items in a type-safe manner, enhancing the development experience with Kentico Xperience.
Here's a quick example to show how you can use XperienceCommunity.DataContext in your project:
var result = await _context
.WithLinkedItems(1)
.FirstOrDefaultAsync(x => x.SystemFields.ContentItemGUID == selected.Identifier, HttpContext.RequestAborted);This example demonstrates how to asynchronously retrieve the first content item that matches a given GUID, with a single level of linked items included, using the fluent API provided by XperienceCommunity.DataContext.
Assuming you have a GenericPage class that implements IWebPageFieldsSource, you can inject the IPageContentContext<GenericPage> into your classes as follows:
public class GenericPageController : Controller
{
private readonly IPageContentContext<GenericPage> _pageContext;
private readonly IWebPageDataContextRetriever _webPageDataContextRetriever;
public GenericPageController(
IPageContentContext<GenericPage> pageContext,
IWebPageDataContextRetriever webPageDataContextRetriever)
{
_pageContext = pageContext;
_webPageDataContextRetriever = webPageDataContextRetriever;
}
// Example method using the _pageContext
public async Task<IActionResult> IndexAsync()
{
var page = _webPageDataContextRetriever.Retrieve().WebPage;
if (page == null)
{
return NotFound();
}
var content = await _pageContext
.FirstOrDefaultAsync(x => x.SystemFields.WebPageItemID == page.WebPageItemID, HttpContext.RequestAborted);
if (content == null)
{
return NotFound();
}
return View(content);
}
}This example demonstrates how to asynchronously retrieve the first page content item that matches a given ID, using the fluent API provided by XperienceCommunity.DataContext.
To demonstrate how to use the IXperienceDataContext interface, consider the following example:
public class ContentService
{
private readonly IXperienceDataContext _dataContext;
public ContentService(IXperienceDataContext dataContext)
{
_dataContext = dataContext;
}
public async Task<GenericContent?> GetContentItemAsync(Guid contentItemGUID)
{
var contentItemContext = _dataContext.ForContentType<GenericContent>();
return await contentItemContext.FirstOrDefaultAsync(x => x.SystemFields.ContentItemGUID == contentItemGUID);
}
public async Task<GenericPage?> GetPageContentAsync(Guid pageGUID)
{
var pageContentContext = _dataContext.ForPageContentType<GenericPage>();
return await pageContentContext.FirstOrDefaultAsync(x => x.SystemFields.WebPageItemGUID == pageGUID);
}
}In this example, the ContentService class uses the IXperienceDataContext interface to get contexts for content items and page content. This setup allows you to leverage the fluent API provided by IContentItemContext and IPageContentContext to interact with content items and page content in a type-safe manner.
The IReusableSchemaContext<T> is the most flexible context, supporting both classes and interfaces. This is particularly useful for reusable content schemas:
// Define an interface for shared content
public interface ISharedContent
{
string Title { get; set; }
string Description { get; set; }
DateTime PublishDate { get; set; }
}
// Use the interface with ReusableSchemaContext
public class SharedContentService
{
private readonly IReusableSchemaContext<ISharedContent> _schemaContext;
public SharedContentService(IReusableSchemaContext<ISharedContent> schemaContext)
{
_schemaContext = schemaContext;
}
public async Task<IEnumerable<ISharedContent>> GetRecentContentAsync()
{
return await _schemaContext
.Where(x => x.PublishDate >= DateTime.Now.AddDays(-30))
.OrderByDescending(x => x.PublishDate)
.ToListAsync();
}
}The library includes an extensible processor system for custom content transformations. There are specialized processor interfaces for different content types:
For content hub items, implement IContentItemProcessor<T>:
// Custom processor for content items
public class BlogPostProcessor : IContentItemProcessor<BlogPost>
{
public int Order => 1; // Execution order
public async Task ProcessAsync(BlogPost content, CancellationToken cancellationToken = default)
{
// Custom processing logic for blog posts
// E.g., update search index, generate thumbnails, etc.
content.ProcessedDate = DateTime.UtcNow;
// Async processing example
await SomeAsyncOperation(content, cancellationToken);
}
}For web pages, implement IPageContentProcessor<T>:
// Custom processor for page content
public class LandingPageProcessor : IPageContentProcessor<LandingPage>
{
public int Order => 2; // Execution order
public async Task ProcessAsync(LandingPage content, CancellationToken cancellationToken = default)
{
// Custom processing logic for landing pages
// E.g., analytics tracking, personalization, etc.
content.ViewCount++;
await UpdateAnalytics(content, cancellationToken);
}
}For custom LINQ expression handling, implement IExpressionProcessor<T>:
// Custom expression processor for specialized queries
public class CustomMethodProcessor : IExpressionProcessor<MethodCallExpression>
{
private readonly IExpressionContext _context;
public CustomMethodProcessor(IExpressionContext context)
{
_context = context;
}
public bool CanProcess(Expression expression)
{
// Define when this processor should handle expressions
return expression is MethodCallExpression method &&
method.Method.Name == "HasCustomTag";
}
public void Process(MethodCallExpression expression)
{
// Transform the expression for your specific needs
// Implementation details depend on your requirements
var methodCall = expression;
var tagValue = GetTagValue(methodCall);
_context.AddParameter("CustomTag", tagValue);
_context.AddWhereAction(where => where.WhereEquals("Tags", tagValue));
}
}Register your custom processors in the DI container using the fluent configuration:
// Register content processors with fluent builder
services.AddXperienceDataContext()
.AddContentItemProcessor<BlogPost, BlogPostProcessor>()
.AddPageContentProcessor<LandingPage, LandingPageProcessor>();
// Or register expression processors directly
services.AddScoped<IExpressionProcessor, CustomMethodProcessor>();
// Alternative: Register with cache configuration
services.AddXperienceDataContext(cacheInMinutes: 30);
// Then register processors separately
services.AddScoped<IContentItemProcessor<BlogPost>, BlogPostProcessor>();
services.AddScoped<IPageContentProcessor<LandingPage>, LandingPageProcessor>();Using IXperienceDataContext for centralized content management:
public class ContentManagementService
{
private readonly IXperienceDataContext _dataContext;
public ContentManagementService(IXperienceDataContext dataContext)
{
_dataContext = dataContext;
}
public async Task<T?> GetContentByIdAsync<T>(int id) where T : class, IContentItemFieldSource
{
return await _dataContext
.ForContentType<T>()
.FirstOrDefaultAsync(x => x.SystemFields.ContentItemID == id);
}
public async Task<T?> GetPageByPathAsync<T>(string path) where T : class, IWebPageFieldsSource
{
return await _dataContext
.ForPageContentType<T>()
.FirstOrDefaultAsync(x => x.SystemFields.WebPageUrlPath == path);
}
public async Task<IEnumerable<T>> GetSchemaContentAsync<T>() where T : class
{
return await _dataContext
.ForReusableSchema<T>()
.ToListAsync();
}
}The library includes built-in caching with automatic cache dependency management:
public class OptimizedContentService
{
private readonly IContentItemContext<Article> _contentContext;
public OptimizedContentService(IContentItemContext<Article> contentContext)
{
_contentContext = contentContext;
}
public async Task<IEnumerable<Article>> GetFeaturedArticlesAsync()
{
// Caching is automatically handled with proper cache dependencies
return await _contentContext
.WithLinkedItems(2) // Include linked items up to 2 levels
.Where(x => x.IsFeatured == true)
.OrderByDescending(x => x.PublishDate)
.ToListAsync();
}
}XperienceCommunity.DataContext provides comprehensive debugging and diagnostic capabilities to help developers troubleshoot issues and gain insights into query execution:
All key classes include rich [DebuggerDisplay] attributes for better debugging experience:
// ExpressionContext shows: Parameters: 3, Members: User.Name, WhereActions: 2
// BaseDataContext shows: ContentType: BlogPost, Language: en-US, Parameters: 2, HasQuery: true
var context = dataContext.ForContentType<BlogPost>()
.Where(x => x.Title.Contains("Tutorial"));
// Examine context in debugger to see detailed state informationEnable detailed execution tracking and performance monitoring:
// Enable diagnostics globally
DataContextDiagnostics.DiagnosticsEnabled = true;
DataContextDiagnostics.TraceLevel = LogLevel.Debug;
// Or enable for specific operations
var context = dataContext.ForContentType<BlogPost>()
.EnableDiagnostics(LogLevel.Debug)
.Where(x => x.IsPublished);
// Get diagnostic reports
string report = context.GetDiagnosticReport("ExpressionProcessing");
var stats = context.GetPerformanceStats();Built-in performance counters track query execution metrics:
// Access performance statistics
var avgTime = ProcessorSupportedQueryExecutor<BlogPost, IProcessor<BlogPost>>.AverageProcessingTimeMs;
var totalQueries = ProcessorSupportedQueryExecutor<BlogPost, IProcessor<BlogPost>>.TotalExecutions;
// Detailed timing for specific operations
var results = await context.ExecuteWithDiagnostics(
"ComplexQuery",
async ctx => await ctx.Where(x => x.Tags.Contains("tutorial")).ToListAsync()
);Automatic OpenTelemetry/Activity support for distributed tracing:
// Activities are created with detailed tags:
// - contentType, processorCount, executionTimeMs, resultCount, error status
// ActivitySource name: "XperienceCommunity.Data.Context.QueryExecution"Add your own diagnostic logging:
var context = dataContext.ForContentType<BlogPost>()
.LogDiagnostic("Starting complex operation")
.Where(x => x.Category == "Technology")
.LogDiagnostic("Applied filters", LogLevel.Debug);
// Get detailed debug information
Console.WriteLine(context.ToDebugString());For comprehensive debugging guidance, see Debugging Guide
Problem: The type 'T' cannot be used as type parameter 'T' in the generic type or method
Solution: Ensure your types implement the required interfaces:
IContentItemContext<T> requires T : IContentItemFieldSourceIPageContentContext<T> requires T : IWebPageFieldsSourceIReusableSchemaContext<T> has no constraints - supports any typeProblem: Need to use interfaces instead of concrete classes
Solution: Use IReusableSchemaContext<T> which supports both classes and interfaces:
// This works with interfaces
IReusableSchemaContext<IMyInterface> context;
// This also works with classes
IReusableSchemaContext<MyClass> context;Problem: Custom processors not being recognized
Solution: Register your processors in the DI container:
services.AddScoped<IExpressionProcessor, CustomProcessor>();
services.AddXperienceDataContext(); // Call after registering processorsBest Practices:
WithLinkedItems() only when neededFirstOrDefaultAsync() over ToListAsync().FirstOrDefault()Recommendation: Use IXperienceDataContext for easier mocking in unit tests:
// Easy to mock
public class MyService
{
private readonly IXperienceDataContext _dataContext;
public MyService(IXperienceDataContext dataContext)
{
_dataContext = dataContext;
}
}The library uses a three-tier architecture:
BaseDataContext<T, TExecutor> and ProcessorSupportedQueryExecutor<T, TProcessor>IXperienceDataContext for centralized accessThis design reduces code duplication by 70%+ while maintaining type safety and flexibility.
We use SemVer for versioning. For the versions available, see the tags on this repository.
This project is licensed under the MIT License - see the LICENSE file for details