Události a logování v systému NopCommerce

Posted on Posted in NopCommerce

V minulém díle seriálu jsme se podívali trochu podrobněji na servisní vrstvu, kde je obchodní logika systému. V dnešním díle si představíme, jakým způsobem jsou distribuovány události napříč systémem. Podíváme se na publikování událostí a jejich odběr. Zjistíme, jakým způsobem se postavit před výzvu opakovaných úloh a na závěr dílu si popíšeme, jak se logují chyby a další stavy systému.
V každém větším modulárním systému musíte mít systém, jakým budete předávat nejrůznější události mezi jednotlivými moduly nebo částmi systému. Události můžou být nejrůznějšího druhu, a to od uživatelské aktivity (přihlášení do systému, vložení zboží do košíku) až po systémové události.

Události

Existují dva způsoby, jakými bude chtít vývojář pracovat s událostmi. Vývojář bude chtít publikovat události, které konzumují posluchači v podobě ostatních modulů. Nebo se přihlásit k odběru událostí, které systém obsahuje nebo je obsahují jiná rozšíření.
Nejdřív se zaměříme na to, jak vypadá v systému NopCommerce událost. Vzorovým příkladem může být přihlášení zákazníka do systému. Jediná konvence při vytváření událostí v systému NopCommerce je pouze dodržení logického pojmenování třídy. Mělo by končit slovem Event, aby bylo zřejmé již z názvu třídy, že je jedná právě o událost. Všechny události systému jsou uloženy v projektu Nop.Core ve složce s doménovými objekty. Událost by si měla s sebou nést hlavně informace o události a doménový objekt, se kterým se událost váže.

    /// <summary>
    /// Customer logged-in event
    /// </summary>
    public class CustomerLoggedinEvent
    {
        public CustomerLoggedinEvent(Customer customer)
        {
            this.Customer = customer;
        }

        /// <summary>
        /// Customer
        /// </summary>
        public Customer Customer
        {
            get; private set;
        }
    }
    /// <summary>
    /// "Customer is logged out" event
    /// </summary>
    public class CustomerLoggedOutEvent
    {
        public CustomerLoggedOutEvent(Customer customer)
        {
            this.Customer = customer;
        }

        /// <summary>
        /// Get or set the customer
        /// </summary>
        public Customer Customer { get; private set; }
    }

    /// <summary>
    /// Customer registered event
    /// </summary>
    public class CustomerRegisteredEvent
    {
        public CustomerRegisteredEvent(Customer customer)
        {
            this.Customer = customer;
        }

        /// <summary>
        /// Customer
        /// </summary>
        public Customer Customer
        {
            get; private set;
        }
    }

    /// <summary>
    /// Customer password changed event
    /// </summary>
    public class CustomerPasswordChangedEvent
    {
        public CustomerPasswordChangedEvent(CustomerPassword password)
        {
            this.Password = password;
        }

        /// <summary>
        /// Customer password
        /// </summary>
        public CustomerPassword Password { get; private set; }
    }

IEventPublisher

Pro publikování události do systému stačí použít implementaci rozhraní IEventPublisher. Když se podíváme podrobněji do kódu, tak zjistíme, že třída obsahuje pouze několik metod. Služba ISubscriptionService je zodpovědná za získání seznamu všech odběratelů konkrétní události. Generická metoda Publish najde všechny odběratele události a pomocí metody PublushToConsumer zavolá metodu, která slouží ke zpracování události.

    /// <summary>
    /// Evnt publisher
    /// </summary>
    public class EventPublisher : IEventPublisher
    {
        private readonly ISubscriptionService _subscriptionService;

        /// <summary>
        /// Ctor
        /// </summary>
        /// <param name="subscriptionService"></param>
        public EventPublisher(ISubscriptionService subscriptionService)
        {
            _subscriptionService = subscriptionService;
        }

        /// <summary>
        /// Publish to cunsumer
        /// </summary>
        /// <typeparam name="T">Type</typeparam>
        /// <param name="x">Event consumer</param>
        /// <param name="eventMessage">Event message</param>
        protected virtual void PublishToConsumer<T>(IConsumer<T> x, T eventMessage)
        ...
        /// <summary>
        /// Find a plugin descriptor by some type which is located into its assembly
        /// </summary>
        /// <param name="providerType">Provider type</param>
        /// <returns>Plugin descriptor</returns>
        protected virtual PluginDescriptor FindPlugin(Type providerType)
        ...

        /// <summary>
        /// Publish event
        /// </summary>
        /// <typeparam name="T">Type</typeparam>
        /// <param name="eventMessage">Event message</param>
        public virtual void Publish<T>(T eventMessage)
        {
            var subscriptions = _subscriptionService.GetSubscriptions<T>();
            subscriptions.ToList().ForEach(x => PublishToConsumer(x, eventMessage));
        }

    }

IConsumer

Již známe způsob, jakým se distribuují události do systému, takže teď je nejvhodnější čas zjistit, jakým způsobem dokážeme zpracovávat jednotlivé události. Pokud chceme zachytit událost v systému, je nutné implementovat rozhraní IConsumer. Kde T je třída pro konkrétní událost, na kterou chceme reagovat.

namespace Nop.Services.Events
{
    public interface IConsumer<T>
    {
        void HandleEvent(T eventMessage);
    }
}

V případě, že bychom chtěli provést jakoukoliv akci po přihlášení uživatele do systému, můžeme to udělat například takto:

    public class HandleCustomerLoggedinEvent: IConsumer<CustomerLoggedinEvent>
	{
	    public void HandleEvent(CustomerLoggedinEvent eventMessage)
	    {
	      //DO something 	
	    }
	}

Získání seznamu všech odběratelů události probíhá pomocí IoC kontaineru. Metoda GetSubscriptions získá všechny instance, které v našem případě implementují IConsumer.

    public class SubscriptionService : ISubscriptionService
    {
        /// <summary>
        /// Get subscriptions
        /// </summary>
        /// <typeparam name="T">Type</typeparam>
        /// <returns>Event consumers</returns>
        public IList<IConsumer<T>> GetSubscriptions<T>()
        {
            return EngineContext.Current.ResolveAll<IConsumer<T>>();
        }
    }

EntityUpdated <T>, EntityInserted<T>, EntityDeleted<T>

Systém NopCommerce má tři specifické události, které slouží pro informování systému o změnách doménových objektů. Může se jednat o vložení, editaci nebo smazání doménového objektu. Často se využívají například pro správu cache. V případě, že jste doménový objekt nějakým způsobem změnili, je nutné informovat cache systému, aby byl záznam z cache odstraněn, případně může být hned aktualizován.

        //categories
        public void HandleEvent(EntityInserted<Category> eventMessage)
        {
            _cacheManager.RemoveByPattern(PRODUCT_CATEGORY_IDS_PATTERN_KEY);
        }
        public void HandleEvent(EntityUpdated<Category> eventMessage)
        {
            _cacheManager.RemoveByPattern(PRODUCT_CATEGORY_IDS_PATTERN_KEY);
        }
        public void HandleEvent(EntityDeleted<Category> eventMessage)
        {
            _cacheManager.RemoveByPattern(PRODUCT_CATEGORY_IDS_PATTERN_KEY);
        }

Můžete je však použít i v mnoha dalších scénářích. V případě, že používáte externí fakturační systém, tak se Vám může hodit událost změny údajů zákazníka. Váš modul může ve chvíli, kdy proběhne editace zákazníka, provést synchronizaci do fakturačního systému.

ITask

U většiny e-commerce systémů se velmi často se dostáváme do situace, kdy potřebujeme periodicky spouštět nejrůznější systémové a jiné úlohy. Může se jednat o synchronizační procesy nebo jen vygenerování nového produktového feedu.
V NopCommerce je pro tyto případy připravené rozhraní ITask, které nám s naplánovanými úlohami pomůže. Implementace rozhraní ITask je velmi jednoduchá, protože obsahuje pouze jednu metodu, a to metodu Execute. Metoda Execute obsahuje logiku prováděné úlohy. Můžeme se například podívat, jak jednoduše je možné implementovat úlohu pro mazání logovacích záznamů.

namespace Nop.Services.Logging
{
    /// <summary>
    /// Represents a task to clear [Log] table
    /// </summary>
    public partial class ClearLogTask : ITask
    {
        private readonly ILogger _logger;

        public ClearLogTask(ILogger logger)
        {
            this._logger = logger;
        }

        /// <summary>
        /// Executes a task
        /// </summary>
        public virtual void Execute()
        {
            _logger.ClearLog();
        }
    }
}

Rozhraní ITask slouží pouze pro implementaci výkonné logiky. Ale aby systém dokázal pracovat s úlohami, potřebuje i metadata, která nesou informace o periodicitě spouštění a nastavení úlohy. Pro tento účel je v systému NopCommerce doménový objekt ScheduleTask, který uchovává nastavení úlohy. Pokud tedy vytvoříte novou úlohu je nutné přidat i nastavení úlohy. Nastavení se dá přidat manuálně přímo do databáze do tabulky [ScheduleTask] a nebo využít službu ScheduleTaskService, která to dokáže udělat automaticky přímo z kódu. Službu ScheduleTaskService budeme typicky volat při instalaci a odinstalaci modulu.

ILogger, ICustomerActivityService

Důležitou součástí každého systému je jeho logování. V NopCommerce je logování realizováno pomocí rozhraní ILogger a ICustomerActivityService. DefaultLogger je třída, která implementuje ILogger rozhraní a zaměřuje se hlavně na logování chyb v systému. Všechny logovací záznamy jsou uloženy v databázi. ICustomerActivityService je oproti ILogger je rozhraní, které se používá hlavně pro logování změn v administračním rozhraním a zákaznických aktivit v systému.