V minulém díle jsme se podívali trochu podrobněji na datovou vrstvu systému NopCommerce. V dnešním díle navážeme na předchozí znalosti, aby se naše pochopení systému zase o něco víc prohloubilo. Zaměříme se na dvě důležité oblasti a to Nop.Core projekt, který udržuje nejen doménu, ale obsahuje i infrastrukturní prvky. Dále se podíváme na projekt Nop.Service, který obsahuje obchodní logiku.
Nop.Core
Projekt Nop.Core tvoří jádro systému NopCommerce. Nalezneme zde jak důležité rozhraní pro datovou vrstvu, tak i infrastrukturní záležitosti jako cache, události systému nebo nejrůznější pomocné třídy pro reflexi, stránkování a mnohé další.
V tomto díle se budeme věnovat těm nejzákladnějším třídám, které potřebujete znát, abyste byli schopní vyvíjet pod NopCommerce. V minulém díle jsme s představili způsob, jakým NopCommerce přistupuje k nastavení jednotlivých částí a modulů pomocí rozhraní ISettings. Neméně důležitá třída, která souvisí s nastavením systému, je NopConfig. V případě, že budete chtít přistoupit ke konfigurační sekci NopConfig ze souboru web.config, je vhodné přistupovat do nastavení prostřednictvím třídy NopConfig. Zároveň pokud přidáte vlastnost do konfigurace je nutné udělat změnu i ve třídě.
Rozhraní IEngine
Jedno ze základních a velmi důležitých rozhraní pro celý systém je IEngine. Jak je možné z kódu vidět, jeho hlavní zodpovědnost je zpřístupnění závislostí systému a jednotlivých služeb. Pro lepší představu a pochopení je dobré se podívat na třídu ContainerManager. Třída ContainerManager slouží jako wrapper nad AutoFac IoC kontainerem. Takže ostatní metody z rozhraní IEngine přes tento wraper přistupují k IoC kontaineru a systému vrací dostupné instance pro konkrétní rozhraní.
using System;
using Nop.Core.Configuration;
using Nop.Core.Infrastructure.DependencyManagement;
namespace Nop.Core.Infrastructure
{
/// <summary>
/// Classes implementing this interface can serve as a portal for the
/// various services composing the Nop engine. Edit functionality, modules
/// and implementations access most Nop functionality through this
/// interface.
/// </summary>
public interface IEngine
{
/// <summary>
/// Container manager
/// </summary>
ContainerManager ContainerManager { get; }
/// <summary>
/// Initialize components and plugins in the nop environment.
/// </summary>
/// <param name="config">Config</param>
void Initialize(NopConfig config);
/// <summary>
/// Resolve dependency
/// </summary>
/// <typeparam name="T">T</typeparam>
/// <returns></returns>
T Resolve<T>() where T : class;
/// <summary>
/// Resolve dependency
/// </summary>
/// <param name="type">Type</param>
/// <returns></returns>
object Resolve(Type type);
/// <summary>
/// Resolve dependencies
/// </summary>
/// <typeparam name="T">T</typeparam>
/// <returns></returns>
T[] ResolveAll<T>();
}
}
Třída, která nese implementaci IEngine, je NopEngine. Když chcete získat ve vyšších vrstvách instanci objektů, tak je nutné přistupovat k NopEngine přes EngineContext. EngineContext využívá návrhový singelton, aby instance NopEngine byla v systému opravdu pouze jedna. V kódu je vidět, jak dostat instanci rozhraní IDataProvider.
IWorkContext
Dalším velmi důležitým rozhraním systému je IWorkContext. Tento pracovní kontext je dostupný při každém http požadavku. A jak je vidět z kódu, obsahuje velmi důležité informace jako jsou aktuální uživatel, měna, jazyková mutace a další.
pace Nop.Core
{
/// <summary>
/// Work context
/// </summary>
public interface IWorkContext
{
/// <summary>
/// Gets or sets the current customer
/// </summary>
Customer CurrentCustomer { get; set; }
/// <summary>
/// Gets or sets the original customer (in case the current one is impersonated)
/// </summary>
Customer OriginalCustomerIfImpersonated { get; }
/// <summary>
/// Gets or sets the current vendor (logged-in manager)
/// </summary>
Vendor CurrentVendor { get; }
/// <summary>
/// Get or set current user working language
/// </summary>
Language WorkingLanguage { get; set; }
/// <summary>
/// Get or set current user working currency
/// </summary>
Currency WorkingCurrency { get; set; }
/// <summary>
/// Get or set current tax display type
/// </summary>
TaxDisplayType TaxDisplayType { get; set; }
/// <summary>
/// Get or set value indicating whether we're in admin area
/// </summary>
bool IsAdmin { get; set; }
}
}
Projekt Nop.Core obsahuje mnoho dalších pomocných tříd pro validace dat, stránkování a jiných. Ovšem pro nás jsou v tento okamžik, abychom pochopili princip systému, důležitější jiné oblasti. Například cache systému NopCommerce.
Dependency Injection NopCommerce
Systém NopCommerce využívá AutoFac kontainer pro správu závislostí. Nad tímto kontainerem je třída ContainerManager, kterou jsme si výše ukázali a vysvětlili její funkci. Pro komplexní pochopení je však nutné pochopit nejen, kde se dají získat instance objektů, ale i jakým způsobem se registrují závislosti do systému.
V projektu Nop.Core se nachází rozhraní IDependencyRegistrar, které je zodpovědné za registraci závislostí v systému.
Implementace rozhraní IDependencyRegister se bude nacházet ve většině větších modulů nebo jednotlivých komponentách systému. Celý proces probíhá při startu webové aplikace. V metodě Application_Start se přes EngineContext spustí inicializace. Ty prohledají všechna assebmlies, která implementují rozhraní IDependencyRegistrar a postupně je zaregistruje do systému.
using Autofac;
using Nop.Core.Configuration;
namespace Nop.Core.Infrastructure.DependencyManagement
{
/// <summary>
/// Dependency registrar interface
/// </summary>
public interface IDependencyRegistrar
{
/// <summary>
/// Register services and interfaces
/// </summary>
/// <param name="builder">Container builder</param>
/// <param name="typeFinder">Type finder</param>
/// <param name="config">Config</param>
void Register(ContainerBuilder builder, ITypeFinder typeFinder, NopConfig config);
/// <summary>
/// Order of this dependency registrar implementation
/// </summary>
int Order { get; }
}
}
Vlastností order dokážete řídit pořadí, v jakém má být závislost zaregistrována do systému. Takže pokud chcete přepsat základní závislosti systému NopCommerce, je nutné nastavit do vlastnosti order větší číslo než nula.
Cachování NopCommerce
Důležitým rozhraním pro Cache je rozhraní ICacheManager. Jedná se o jednoduché rozhraní, které obsahuje metody pro správu cache v systému. V NopCommerce je dostupných několik implementací rozhraní ICacheManager. Nás bude především zajímat implementace ve třídě MemoryCacheManager. Je však nutné říct, že je například dostupná i implementace pro systém Redis. Pro zapnutí distribuované cache přes systém Redis je nutné pouze nastavit konfigurační data v souboru web.config.
namespace Nop.Core.Caching
{
/// <summary>
/// Cache manager interface
/// </summary>
public interface ICacheManager : IDisposable
{
/// <summary>
/// Gets or sets the value associated with the specified key.
/// </summary>
/// <typeparam name="T">Type</typeparam>
/// <param name="key">The key of the value to get.</param>
/// <returns>The value associated with the specified key.</returns>
T Get<T>(string key);
/// <summary>
/// Adds the specified key and object to the cache.
/// </summary>
/// <param name="key">key</param>
/// <param name="data">Data</param>
/// <param name="cacheTime">Cache time</param>
void Set(string key, object data, int cacheTime);
/// <summary>
/// Gets a value indicating whether the value associated with the specified key is cached
/// </summary>
/// <param name="key">key</param>
/// <returns>Result</returns>
bool IsSet(string key);
/// <summary>
/// Removes the value with the specified key from the cache
/// </summary>
/// <param name="key">/key</param>
void Remove(string key);
/// <summary>
/// Removes items by pattern
/// </summary>
/// <param name="pattern">pattern</param>
void RemoveByPattern(string pattern);
/// <summary>
/// Clear all cache data
/// </summary>
void Clear();
}
}
Rozhraní je jednoduché, takže si klidně můžete zkusit vytvořit vlastní implementaci. Když se podíváme na třídu MemoryCacheManager, tak zjistíme, že využívá standartní prostředky .NET frameworku a Cache je realizována pomocí objektu ObjectCache. Pro unit testování se místo implementace ve třídě MemoryCacheManager využívá třída NopNullCache, která by se dala využít i jako null object pattern.
V systému NopCommerce je dostupné rozšíření CacheExtensions nad rozhraním ICacheManager, které usnadňuje práci s Cache ještě trochu více. Jedná se spíše o syntax sugar, ale jeho využití uvidíte na mnoha místech servisní vrsty.
Obchodní logika
Již je ten správný čas podívat se na další zřejmě největší část systému. Tou je servisní vrstva, kde se odehrává veškerá obchodní logika systému. Záměrně se začínáme o této vrstvě bavit až teď, protože ze služeb budete využívat jak datovou vrstvu v podobě repositářů, tak i další potřebné komponenty z Nop.Core
Konvence při pojmenování nových služeb jsou jednoduché. Jak jsme zvyklí již z projektu Nop.Core z oblasti doménových objektů, princip a pojmenování složek je stejné, aby se zvýšila přehlednost a struktura. Rozhraní i třída, která ji implementuje, jsou na stejné úrovni, akorát název rozhraní začíná písmenem I.
Customer service
Všechny závislosti jsou do jednotlivých služeb vždy předávány před konstruktor a jejich režii za nás řeší IoC kontainer. Typická služba bude obsahovat repositáře, přes které můžete přistupovat do databáze. Může obsahovat třídy, které implementují ISetting a zpřístupňují nastavení různých komponent. V případě, že používáte ve službě cache, tak určitě budete potřebovat jedinečné klíče pro uložení. A v neposlední řadě IEventPublisher, který je zodpovědný za rozesílání zpráv napříč systémem.
namespace Nop.Services.Customers
{
/// <summary>
/// Customer service
/// </summary>
public partial class CustomerService : ICustomerService
{
#region Constants
/// <summary>
/// Key for caching
/// </summary>
/// <remarks>
/// {0} : show hidden records?
/// </remarks>
private const string CUSTOMERROLES_ALL_KEY = "Nop.customerrole.all-{0}";
/// <summary>
/// Key for caching
/// </summary>
/// <remarks>
/// {0} : system name
/// </remarks>
private const string CUSTOMERROLES_BY_SYSTEMNAME_KEY = "Nop.customerrole.systemname-{0}";
/// <summary>
/// Key pattern to clear cache
/// </summary>
private const string CUSTOMERROLES_PATTERN_KEY = "Nop.customerrole.";
#endregion
#region Fields
private readonly IRepository<Customer> _customerRepository;
private readonly IRepository<CustomerPassword> _customerPasswordRepository;
private readonly IRepository<CustomerRole> _customerRoleRepository;
private readonly IRepository<GenericAttribute> _gaRepository;
private readonly IRepository<Order> _orderRepository;
private readonly IRepository<ForumPost> _forumPostRepository;
private readonly IRepository<ForumTopic> _forumTopicRepository;
private readonly IRepository<BlogComment> _blogCommentRepository;
private readonly IRepository<NewsComment> _newsCommentRepository;
private readonly IRepository<PollVotingRecord> _pollVotingRecordRepository;
private readonly IRepository<ProductReview> _productReviewRepository;
private readonly IRepository<ProductReviewHelpfulness> _productReviewHelpfulnessRepository;
private readonly IGenericAttributeService _genericAttributeService;
private readonly IDataProvider _dataProvider;
private readonly IDbContext _dbContext;
private readonly ICacheManager _cacheManager;
private readonly IEventPublisher _eventPublisher;
private readonly CustomerSettings _customerSettings;
private readonly CommonSettings _commonSettings;
#endregion
#region Ctor
public CustomerService(ICacheManager cacheManager,
IRepository<Customer> customerRepository,
IRepository<CustomerPassword> customerPasswordRepository,
IRepository<CustomerRole> customerRoleRepository,
IRepository<GenericAttribute> gaRepository,
IRepository<Order> orderRepository,
IRepository<ForumPost> forumPostRepository,
IRepository<ForumTopic> forumTopicRepository,
IRepository<BlogComment> blogCommentRepository,
IRepository<NewsComment> newsCommentRepository,
IRepository<PollVotingRecord> pollVotingRecordRepository,
IRepository<ProductReview> productReviewRepository,
IRepository<ProductReviewHelpfulness> productReviewHelpfulnessRepository,
IGenericAttributeService genericAttributeService,
IDataProvider dataProvider,
IDbContext dbContext,
IEventPublisher eventPublisher,
CustomerSettings customerSettings,
CommonSettings commonSettings)
{
this._cacheManager = cacheManager;
this._customerRepository = customerRepository;
this._customerPasswordRepository = customerPasswordRepository;
this._customerRoleRepository = customerRoleRepository;
this._gaRepository = gaRepository;
this._orderRepository = orderRepository;
this._forumPostRepository = forumPostRepository;
this._forumTopicRepository = forumTopicRepository;
this._blogCommentRepository = blogCommentRepository;
this._newsCommentRepository = newsCommentRepository;
this._pollVotingRecordRepository = pollVotingRecordRepository;
this._productReviewRepository = productReviewRepository;
this._productReviewHelpfulnessRepository = productReviewHelpfulnessRepository;
this._genericAttributeService = genericAttributeService;
this._dataProvider = dataProvider;
this._dbContext = dbContext;
this._eventPublisher = eventPublisher;
this._customerSettings = customerSettings;
this._commonSettings = commonSettings;
}
....
V jednoduchosti je krása. Proto, jak brzy zjistíte, většina metod je velmi krátkých a přehledných. V případě, že se podíváme do kódu na vytvoření zákazníka, tak z pohledu servisní vrstvy se jedná o velmi jednoduchý úkol. Validace uživatelského vstupu probíhá na vyšší úrovni, kde se zároveň ViewModel namapuje na doménový objekt. Servisní vrstva pracuje pouze s doménovými objekty.
/// <summary>
/// Insert a customer
/// </summary>
/// <param name="customer">Customer</param>
public virtual void InsertCustomer(Customer customer)
{
if (customer == null)
throw new ArgumentNullException("customer");
_customerRepository.Insert(customer);
//event notification
_eventPublisher.EntityInserted(customer);
}
Takže metoda pouze zkontroluje, zda je objekt nastaven na instanci a pomocí repositáře uloží do databáze. Po uložení záznamu se vyšle událost do systému, na kterou mohou reagovat jiné části systému.
Servisní vrstva je velmi rozsáhlá a budeme se jí proto věnovat i v dalších dílech. Dnes jsem chtěl pouze nastínit základní principy, které se budou objevovat v mnoha službách v servisní vrstvě. Nejlepším způsobem, jakým se naučit a pochopit systém NopCommerce, je právě z kódu. Doporučuji si otevřít několik jednodušších služeb a prostudovat kód. Tímto způsobem nejrychleji pochopíte, jak celý systém funguje.