Sitecore開発実践ガイド:Helix原則からカスタムコンポーネントまで完全解説(Part4)
Sitecore開発Helix.NETC#アーキテクチャ
Sitecoreでの本格的な開発手法を学びます。Helix原則に基づいたソリューション設計から、カスタムコンポーネント開発、API活用まで、実践的なコード例とともに解説します。
はじめに:Sitecore開発の世界へ
前回の記事(Part3: Sitecoreパーソナライゼーション編)では、Sitecoreの強力なパーソナライゼーション機能について学びました。今回は開発者向けに、実際にSitecoreでカスタム機能を開発する方法を詳しく解説します。
この記事を読むことで、以下のスキルが身につきます:
- Helix原則に基づいた保守性の高いソリューション設計
- カスタムコンポーネントの開発方法
- Sitecore APIの効果的な活用
- 開発のベストプラクティス
開発環境のセットアップ
必要なツール
# 開発環境チェックリスト
- Visual Studio 2022 (または2019)
- .NET Framework 4.8
- Sitecore Installation Framework (SIF)
- SQL Server 2019以降
- IIS 10以降
- Sitecore DevEx CLI
- Node.js (JSS開発の場合)
Sitecore CLIのインストール
# Sitecore CLIのグローバルインストール
dotnet tool install -g Sitecore.CLI
# プロジェクトの初期化
dotnet sitecore init
# プラグインのインストール
dotnet sitecore plugin add -n Sitecore.DevEx.Extensibility.Serialization
dotnet sitecore plugin add -n Sitecore.DevEx.Extensibility.Publishing
Docker環境の構築(推奨)
# docker-compose.yml
version: '3.8'
services:
sitecore-xm1:
image: scr.sitecore.com/sxp/sitecore-xm1-cm:10.3-ltsc2019
environment:
- SITECORE_LICENSE=${SITECORE_LICENSE}
- SQL_SA_PASSWORD=${SQL_SA_PASSWORD}
- SITECORE_ADMIN_PASSWORD=${SITECORE_ADMIN_PASSWORD}
ports:
- "44001:80"
depends_on:
- sql
- solr
sql:
image: mcr.microsoft.com/mssql/server:2019-latest
environment:
- SA_PASSWORD=${SQL_SA_PASSWORD}
- ACCEPT_EULA=Y
ports:
- "14330:1433"
solr:
image: solr:8.11
ports:
- "8983:8983"
Helix原則:モジュラー設計の極意
Helix の3層構造
Solution Structure
│
├── Foundation Layer (基盤層)
│ ├── Serialization
│ ├── DependencyInjection
│ └── Extensions
│
├── Feature Layer (機能層)
│ ├── Navigation
│ ├── Search
│ └── Forms
│
└── Project Layer (プロジェクト層)
├── Website
└── API
依存関係のルール
// ✅ 正しい依存関係
// Project → Feature → Foundation
// ProjectレイヤーでFeatureを使用
using Feature.Navigation.Controllers;
using Feature.Search.Services;
// ❌ 間違った依存関係
// Foundation → Feature (逆方向の依存)
// Feature → Project (上位層への依存)
実際のプロジェクト構造
MySitecore.Solution/
├── src/
│ ├── Foundation/
│ │ ├── Foundation.Core/
│ │ │ ├── Code/
│ │ │ ├── App_Config/
│ │ │ └── Foundation.Core.csproj
│ │ └── Foundation.Serialization/
│ │ ├── Code/
│ │ └── Foundation.Serialization.csproj
│ │
│ ├── Feature/
│ │ ├── Feature.Products/
│ │ │ ├── Code/
│ │ │ │ ├── Controllers/
│ │ │ │ ├── Models/
│ │ │ │ ├── Repositories/
│ │ │ │ └── Views/
│ │ │ ├── Serialization/
│ │ │ └── Feature.Products.csproj
│ │ └── Feature.Navigation/
│ │ └── ...
│ │
│ └── Project/
│ └── Project.Website/
│ ├── Code/
│ ├── Views/
│ └── Project.Website.csproj
│
├── docker/
├── deploy/
└── MySitecore.sln
カスタムコンポーネント開発
1. コントローラーレンダリングの作成
// Feature.Products/Controllers/ProductListController.cs
namespace Feature.Products.Controllers
{
public class ProductListController : Controller
{
private readonly IProductRepository _productRepository;
private readonly ISitecoreContext _sitecoreContext;
public ProductListController(
IProductRepository productRepository,
ISitecoreContext sitecoreContext)
{
_productRepository = productRepository;
_sitecoreContext = sitecoreContext;
}
public ActionResult ProductList()
{
// データソースアイテムを取得
var dataSourceId = RenderingContext.Current.Rendering.DataSource;
var dataSource = _sitecoreContext.GetItem<IProductListSettings>(dataSourceId);
if (dataSource == null)
{
return View("~/Views/Products/ProductList.Empty.cshtml");
}
// 商品を取得
var products = _productRepository.GetProducts(
category: dataSource.Category,
maxItems: dataSource.MaxItems,
sortOrder: dataSource.SortOrder
);
// ビューモデルを構築
var viewModel = new ProductListViewModel
{
Title = dataSource.Title,
Products = products.Select(p => new ProductViewModel
{
Id = p.Id,
Name = p.Name,
Price = p.Price,
ImageUrl = MediaManager.GetMediaUrl(p.Image),
Url = LinkManager.GetItemUrl(p)
}).ToList(),
ShowPrices = dataSource.ShowPrices,
ColumnsCount = dataSource.ColumnsCount
};
return View("~/Views/Products/ProductList.cshtml", viewModel);
}
}
}
2. Glass Mapperを使用したモデル定義
// Feature.Products/Models/IProduct.cs
namespace Feature.Products.Models
{
[SitecoreType(TemplateId = "{PRODUCT-TEMPLATE-GUID}")]
public interface IProduct : IGlassBase
{
[SitecoreField("Product Name")]
string Name { get; set; }
[SitecoreField("Description")]
string Description { get; set; }
[SitecoreField("Price")]
decimal Price { get; set; }
[SitecoreField("Product Image")]
Image Image { get; set; }
[SitecoreField("Category")]
ICategory Category { get; set; }
[SitecoreField("Features")]
IEnumerable<IProductFeature> Features { get; set; }
[SitecoreField("In Stock")]
bool InStock { get; set; }
}
[SitecoreType(TemplateId = "{PRODUCT-LIST-SETTINGS-GUID}")]
public interface IProductListSettings : IGlassBase
{
[SitecoreField("Title")]
string Title { get; set; }
[SitecoreField("Category")]
ICategory Category { get; set; }
[SitecoreField("Max Items")]
int MaxItems { get; set; }
[SitecoreField("Sort Order")]
string SortOrder { get; set; }
[SitecoreField("Show Prices")]
bool ShowPrices { get; set; }
[SitecoreField("Columns Count")]
int ColumnsCount { get; set; }
}
}
3. ビューの実装
@model Feature.Products.Models.ProductListViewModel
<div class="product-list" data-columns="@Model.ColumnsCount">
@if (!string.IsNullOrEmpty(Model.Title))
{
<h2 class="product-list__title">@Model.Title</h2>
}
<div class="product-list__grid">
@foreach (var product in Model.Products)
{
<article class="product-card">
<a href="@product.Url" class="product-card__link">
<div class="product-card__image">
<img src="@product.ImageUrl"
alt="@product.Name"
loading="lazy" />
</div>
<div class="product-card__content">
<h3 class="product-card__name">@product.Name</h3>
@if (Model.ShowPrices)
{
<div class="product-card__price">
¥@product.Price.ToString("N0")
</div>
}
</div>
</a>
</article>
}
</div>
</div>
<style>
.product-list__grid {
display: grid;
grid-template-columns: repeat(var(--columns, 3), 1fr);
gap: 2rem;
}
.product-list[data-columns="2"] {
--columns: 2;
}
.product-list[data-columns="4"] {
--columns: 4;
}
@media (max-width: 768px) {
.product-list__grid {
grid-template-columns: 1fr;
}
}
</style>
4. 依存性注入の設定
// Foundation.DependencyInjection/ServicesConfigurator.cs
namespace Foundation.DependencyInjection
{
public class ServicesConfigurator : IServicesConfigurator
{
public void Configure(IServiceCollection services)
{
// Repositoryの登録
services.AddScoped<IProductRepository, ProductRepository>();
services.AddScoped<ICategoryRepository, CategoryRepository>();
// Servicesの登録
services.AddScoped<ISearchService, SolrSearchService>();
services.AddScoped<ICacheService, RedisCacheService>();
// Glass Mapperの設定
services.AddScoped<ISitecoreContext>(provider =>
{
var glassContext = new SitecoreContext();
return glassContext;
});
// Sitecoreサービスの登録
services.AddSingleton<ITrackerService, TrackerService>();
services.AddScoped<IPersonalizationService, PersonalizationService>();
}
}
}
Sitecore APIの活用
1. Item APIの基本操作
public class ItemApiExamples
{
// アイテムの取得
public void GetItems()
{
// IDで取得
var item = Sitecore.Context.Database.GetItem("{ITEM-GUID}");
// パスで取得
var homeItem = Sitecore.Context.Database.GetItem("/sitecore/content/home");
// 言語とバージョン指定
var japaneseItem = Sitecore.Context.Database.GetItem(
"/sitecore/content/home",
LanguageManager.GetLanguage("ja-JP"),
Version.Latest
);
// 子アイテムの取得
var children = homeItem.Children;
var descendants = homeItem.Axes.GetDescendants();
}
// アイテムの作成・更新
public void CreateAndUpdateItems()
{
var parentItem = Sitecore.Context.Database.GetItem("/sitecore/content/home");
var template = Sitecore.Context.Database.GetTemplate("{TEMPLATE-ID}");
// アイテムの作成
using (new SecurityDisabler())
{
var newItem = parentItem.Add("New Product", template);
// フィールドの更新
using (new EditContext(newItem))
{
newItem["Title"] = "新商品";
newItem["Price"] = "19800";
newItem["Description"] = "商品説明";
newItem[FieldIDs.PublishDate] = DateUtil.IsoNow;
}
}
}
}
2. Search APIの実装
public class SearchService : ISearchService
{
private readonly ISearchIndex _searchIndex;
public SearchService()
{
_searchIndex = ContentSearchManager.GetIndex("sitecore_web_index");
}
public SearchResults<Product> SearchProducts(SearchCriteria criteria)
{
using (var context = _searchIndex.CreateSearchContext())
{
var query = context.GetQueryable<SearchResultItem>()
.Where(item => item.TemplateId == new ID("{PRODUCT-TEMPLATE-ID}"))
.Where(item => item.Language == Context.Language.Name);
// カテゴリフィルター
if (criteria.CategoryId.HasValue)
{
query = query.Where(item => item["Category"] == criteria.CategoryId.Value.ToString());
}
// 価格範囲フィルター
if (criteria.MinPrice.HasValue || criteria.MaxPrice.HasValue)
{
var minPrice = criteria.MinPrice ?? 0;
var maxPrice = criteria.MaxPrice ?? decimal.MaxValue;
query = query.Where(item =>
item["Price"].Between(minPrice, maxPrice, Inclusion.Both));
}
// フリーテキスト検索
if (!string.IsNullOrEmpty(criteria.SearchText))
{
var predicate = PredicateBuilder.True<SearchResultItem>();
predicate = predicate.Or(item => item.Name.Contains(criteria.SearchText));
predicate = predicate.Or(item => item["Description"].Contains(criteria.SearchText));
query = query.Where(predicate);
}
// ソート
switch (criteria.SortOrder)
{
case "price-asc":
query = query.OrderBy(item => item["Price"]);
break;
case "price-desc":
query = query.OrderByDescending(item => item["Price"]);
break;
case "name":
query = query.OrderBy(item => item.Name);
break;
default:
query = query.OrderByDescending(item => item.CreatedDate);
break;
}
// ページング
var totalResults = query.Count();
var results = query
.Skip((criteria.Page - 1) * criteria.PageSize)
.Take(criteria.PageSize)
.Select(item => new Product
{
Id = item.ItemId,
Name = item.Name,
Price = decimal.Parse(item["Price"] ?? "0"),
Description = item["Description"],
ImageUrl = item["Product Image"]
})
.ToList();
return new SearchResults<Product>
{
Results = results,
TotalCount = totalResults,
Page = criteria.Page,
PageSize = criteria.PageSize
};
}
}
}
3. Experience APIの活用
public class PersonalizationService : IPersonalizationService
{
public void TrackCustomGoal(string goalName, decimal value)
{
if (Tracker.IsActive && Tracker.Current?.Session != null)
{
// カスタムゴールの登録
var goalItem = Sitecore.Context.Database.GetItem($"/sitecore/system/Marketing Control Panel/Goals/{goalName}");
if (goalItem != null)
{
var goal = new PageEventData(goalName, goalItem.ID.ToGuid())
{
Value = (int)value,
Text = $"Goal triggered: {goalName}",
Data = JsonConvert.SerializeObject(new
{
Timestamp = DateTime.UtcNow,
UserId = Tracker.Current.Contact.ContactId,
AdditionalData = GetContextualData()
})
};
Tracker.Current.CurrentPage.Register(goal);
}
}
}
public void UpdateVisitorProfile(Dictionary<string, double> profileValues)
{
if (Tracker.IsActive)
{
foreach (var profile in profileValues)
{
var profileItem = Sitecore.Context.Database.GetItem($"/sitecore/system/Marketing Control Panel/Profiles/{profile.Key}");
if (profileItem != null)
{
Tracker.Current.Interaction.Profiles[profileItem.Name].Score(profile.Value);
}
}
// パターンマッチングの実行
var pattern = Tracker.Current.Interaction.Profiles.GetTopPattern();
if (pattern != null)
{
ApplyPersonalizationRules(pattern);
}
}
}
}
パフォーマンス最適化
1. キャッシング戦略
public class CachedProductRepository : IProductRepository
{
private readonly IProductRepository _innerRepository;
private readonly ICacheManager _cacheManager;
public IEnumerable<Product> GetProducts(string category, int maxItems)
{
var cacheKey = $"products_{category}_{maxItems}_{Context.Language.Name}";
return _cacheManager.GetOrAdd(cacheKey, () =>
{
return _innerRepository.GetProducts(category, maxItems);
}, TimeSpan.FromMinutes(10));
}
}
// カスタムキャッシュクリア
public class ProductCacheClearer : ICacheClearer
{
public void ClearCache(Item[] items)
{
foreach (var item in items)
{
if (item.TemplateID == new ID("{PRODUCT-TEMPLATE-ID}"))
{
CacheManager.ClearCache("products_*");
}
}
}
}
2. レンダリングキャッシュの設定
<!-- レンダリングキャッシュ設定 -->
<configuration>
<sitecore>
<pipelines>
<mvc.renderRendering>
<processor type="Feature.Products.Pipelines.SetProductListCaching, Feature.Products">
<param desc="cacheKey">productlist</param>
<param desc="varyBy">datasource|device|language|user</param>
<param desc="duration">600</param>
</processor>
</mvc.renderRendering>
</pipelines>
</sitecore>
</configuration>
開発のベストプラクティス
1. テスタブルなコード設計
// インターフェースベースの設計
public interface IProductService
{
Task<IEnumerable<Product>> GetRecommendedProductsAsync(string userId);
}
// テスト可能な実装
public class ProductService : IProductService
{
private readonly IProductRepository _repository;
private readonly IPersonalizationEngine _personalization;
private readonly ILogger<ProductService> _logger;
public ProductService(
IProductRepository repository,
IPersonalizationEngine personalization,
ILogger<ProductService> logger)
{
_repository = repository;
_personalization = personalization;
_logger = logger;
}
public async Task<IEnumerable<Product>> GetRecommendedProductsAsync(string userId)
{
try
{
var userProfile = await _personalization.GetUserProfileAsync(userId);
var recommendations = await _repository.GetProductsByProfileAsync(userProfile);
_logger.LogInformation($"Retrieved {recommendations.Count()} recommendations for user {userId}");
return recommendations;
}
catch (Exception ex)
{
_logger.LogError(ex, "Error getting recommendations for user {UserId}", userId);
throw;
}
}
}
2. エラーハンドリング
public class GlobalExceptionHandler : IExceptionProcessor
{
public void Process(ExceptionArgs args)
{
var exception = args.Exception;
// カスタムエラーページの表示
if (Context.PageMode.IsNormal)
{
var errorItem = Context.Database.GetItem("/sitecore/content/error-pages/500");
if (errorItem != null)
{
Context.Item = errorItem;
}
}
// ログ記録
Log.Error($"Unhandled exception: {exception.Message}", exception, this);
// Application Insightsへの送信
TelemetryClient.TrackException(exception, new Dictionary<string, string>
{
["ItemPath"] = Context.Item?.Paths.FullPath,
["User"] = Context.User?.Name,
["Url"] = HttpContext.Current?.Request.Url.ToString()
});
}
}
3. 開発時の便利ツール
// デバッグ用のパイプラインプロセッサー
public class DebugRenderingInfo : RenderRenderingProcessor
{
public override void Process(RenderRenderingArgs args)
{
if (Context.PageMode.IsDebugging)
{
var rendering = args.Rendering;
var output = new StringBuilder();
output.AppendLine("<!-- Rendering Debug Info");
output.AppendLine($"Rendering ID: {rendering.RenderingID}");
output.AppendLine($"Datasource: {rendering.DataSource}");
output.AppendLine($"Cacheable: {rendering.Cacheable}");
output.AppendLine($"Cache Key: {rendering.CacheKey}");
output.AppendLine("-->");
args.Writer.Write(output.ToString());
}
}
}
まとめ:プロフェッショナルなSitecore開発へ
Sitecore開発をマスターするために重要なポイント:
- Helix原則の遵守: 保守性の高いソリューション構造
- 依存性注入の活用: テスタブルで疎結合な設計
- APIの理解: Sitecore APIを効果的に活用
- パフォーマンス意識: キャッシングとクエリ最適化
- 継続的な学習: Sitecoreの新機能への対応
これらの原則を守ることで、スケーラブルで保守性の高いSitecoreソリューションを構築できます。
次回予告:パフォーマンス最適化編
次回のPart5: Sitecoreパフォーマンス最適化の極意では、Sitecoreのパフォーマンスを最大限に引き出す方法を解説します:
- キャッシング戦略の詳細
- データベースクエリの最適化
- CDNとの連携
- 負荷テストとモニタリング
高速で安定したSitecoreサイトの構築方法をお楽しみに!
関連記事: