Sitecore開発実践ガイド:Helix原則からカスタムコンポーネントまで完全解説(Part4)

52分で読めます
エンハンスド編集部
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開発をマスターするために重要なポイント:

  1. Helix原則の遵守: 保守性の高いソリューション構造
  2. 依存性注入の活用: テスタブルで疎結合な設計
  3. APIの理解: Sitecore APIを効果的に活用
  4. パフォーマンス意識: キャッシングとクエリ最適化
  5. 継続的な学習: Sitecoreの新機能への対応

これらの原則を守ることで、スケーラブルで保守性の高いSitecoreソリューションを構築できます。

次回予告:パフォーマンス最適化編

次回のPart5: Sitecoreパフォーマンス最適化の極意では、Sitecoreのパフォーマンスを最大限に引き出す方法を解説します:

  • キャッシング戦略の詳細
  • データベースクエリの最適化
  • CDNとの連携
  • 負荷テストとモニタリング

高速で安定したSitecoreサイトの構築方法をお楽しみに!


関連記事:

技術的な課題をお持ちですか?

記事でご紹介した技術や実装について、
より詳細なご相談やプロジェクトのサポートを承ります

無料技術相談を申し込む