Dynamics 365 for Retail (Часть 4)

Начало

Часть 2

Часть 3

Dynamics 365 Retail Server Extensibility

D365 for Retail позволяет расширять функциональность Retail Server.

Controller Extensibility

OData controller

Это класс, который обрабатывает HTTP-запросы. Отдельный контроллер создается для каждого объекта, который управляет действиями создания, чтения, обновления и удаления (CRUD). Функционал контроллера, который уже существует в системе, можно расширить, добавив новые функции, необходимые для бизнес-процесса. Чтобы расширить существующий контроллер, нужно создать новый класс-расширение для существующего контроллера. В новом классе необходимо использовать атрибут ExtendedController, который указывает, что данный класс является расширением для существующего контроллера. После создания нового контроллера, переопределяющего исходный контроллер, то необходимо использовать новый контроллер вместо исходного.

В приведенном ниже примере, класс ExtendedCustomersController является расширением класса CustomersController и принимает тип объекта и ключевое поле сущности Customer как параметры.

В примере создадим расширение для существующего контроллера Customer и переопределим метод Get, который возвращает список клиентов.

namespace Microsoft.Dynamics.RetailServer.ExtensionSamples
{
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Runtime.InteropServices;
    using Microsoft.Dynamics.Commerce.Runtime.DataModel;
    using Microsoft.Dynamics.Retail.StoreServerServiceLibrary;
    using Microsoft.Dynamics.Retail.StoreServerServiceLibrary.ODataControllers;

    [ExtendedController("Customers")]

    [ComVisible(false)]
    public class ExtendedCustomersController : CustomersController
    {
        public override IQueryable<Customer> Get()
        {
            List<Customer> customers = new List<Customer>();

            for (int i = 0; i < 10; i++)
            {
                var customer = new Customer();
                customer.AccountNumber = "customer" + i;
                customer.Name = "Name" + i;
                customers.Add(customer);
            }

            return customers.AsQueryable();
        }
    }
}

Web API controller

По умолчанию, Retail Server использует только OData. Если хотите использовать контроллер, который использует традиционный Web API, то можно создать свой собственный контроллер и расширить Web API конфигурацию.

Приведенный пример показывает, как создать Web API контроллер.

namespace Microsoft.Dynamics.RetailServer.Samples.Extensions
{
    using System.Collections.Generic;
    using System.Runtime.InteropServices;
    using System.Web.Http;
    using Commerce.Runtime.DataModel;
    using Retail.StoreServerServiceLibrary;

    [ComVisible(false)]
    [ExtendedController("Values")]
    [CommerceAuthorization(AllowedRetailRoles = new string[] { CommerceRoles.Anonymous }, CheckRetailOperation = false)]
    public class ValuesController : ApiController
    {
        // GET /api/values
        public IEnumerable<string> Get()
        {
            return new string[] { "value1", "value2" };
        }
    }
}

Для совместного использования Web API и OData, необходимо создать новый класс, который имеет атрибут Export, в котором указываем интерфейс IWebApiConfig. Данный интерфейс имеет только один метод Register и его необходимо переопределить. После переопределения метода, можно будет использовать базовый класс OData контроллера.

Metadata Extensibility

Метаданные, в технологии OData, определяют клиент-серверный контракт. Контракт объявляет объекты и действия, так что, когда производятся изменения на стороне сервера, необходимо сгенерировать proxy-код для клиентской стороны. Для этого можно использовать специальный инструмент, который позволяет сократить время разработки.

Чтобы создать новый или изменить существующий объект, или действие, то необходимо использовать расширяемость контракта.

Retail Server имеет родительский контроллер для контрактов, который называется CommerceModelFactory. Чтобы расширить данный контроллер, необходимо создать новый класс с атрибутом Export, где указать интерфейс IEdmModelFactory. Есть возможность как добавить, так и изменить существующий код.

Например, нужно добавить новый набор объектов и новое действие. Для этого создается новый класс ExtendedEdmModelFactory, расширение контроллера CommerceModelFactory, который принадлежит интерфейсу IEdmModelFactory. Добавляется новое действие «NewAction» и новый набор данных «NewEntities»

namespace Microsoft.Dynamics.RetailServer.Samples.Extensions
{
    using System.ComponentModel.Composition;
    using Microsoft.Dynamics.Retail.StoreServerServiceLibrary;
    [Export(typeof(IEdmModelFactory))]
    public class ExtendedEdmModelFactory : CommerceModelFactory
    {
        protected override void BuildNonBindableActions()
        {
            base.BuildNonBindableActions();
            var NewAction = BindAction("NewAction");
            NewAction.Returns<string>();
        }

        protected override void BuildEntitySets()
        {
            base.BuildEntitySets();
            BuildEntitySet<NewEntity>("NewEntities");
        }
    }
}

Commerce Runtime Extensibility

CRT содержит основную бизнес логику. Если необходимо добавить или изменить какую-то бизнес логику, то нужно изменить CRT. Весь CRT код написан на C#, поэтому при компиляции происходит сборка библиотек (сборки .Net).

Каждый CRT сервис включает в себя группу из одного или нескольких запросов и ответов. Во время работы, POS отправляет запрос на Retail Server, а Retail Server вызывает CRT. CRT, когда обработает информацию, отправит ее обратно на POS.

Например, сервис по работе с клиентами содержит все клиент-связанные запросы и ответы, которые используются в различных процессах.

Поддержка CRT различных шаблонов расширяемости

Прежде, чем поговорить о шаблонах расширяемости, стоит понять, как CRT расширение может быть создано. CRT это просто коллекция библиотек классов написанных на C#. Можно создать проект на C# и сделать все расширения используя шаблоны. Всегда используйте примеры, которые Microsoft предоставляет, как шаблоны для собственных расширений, потому что эти примеры имеют правильные ссылки на сборки, версию Microsoft .NET Framework, типы параметров и результатов. Дополнительно, все параметры настраиваемые.

Используем расширяемость свойств для CRT объекта.

Одним из способов добавления новых данных в существующий CRT является использование расширяемости свойств. Свойства — это пары ключ-значение для объекта. По умолчанию эти пары ключ-значение не сохраняются в базе данных.

Свойства предоставляют самый простой и, можно утверждать, наиболее удобный способ расширения объектов. Вы всегда должны использовать этот шаблон, когда это возможно.

Хотя возможно использовать полиморфизм/наследование для добавления простых членов данных к сущностям, этот подход обычно вызывает больше проблем, чем решает. Тем не менее, этот подход может потребоваться для конкретных случаев.

Чтобы добавить свойство расширения, вы должны использовать этот синтаксис:

entity.SetProperty("EXTENSION_PROPERTY_ADDED", true);

Для того чтобы получить значение свойства, надо воспользоваться подобным синтаксисом:

bool? property = (bool?)entity.GetProperty("EXTENSION_PROPERTY_ADDED");

Если такого свойства не будет, property будет иметь значение null.

Любой свойство, которое было добавлено в объект хранится в памяти пока объект существует. Свойства могут быть использованы в разных компонентах. Например, если добавить свойство в MPOS и затем вызвать функцию на Retail Server/CRT, то свойство будет доступно в этом процессе. Если объект с добавленным свойством будет использоваться в время обмена данными в режиме реального времени, то свойство также будет доступно в этом процессе.

Используем расширяемость свойств для CRT объекта с сохранением.

Чтобы сохранить новое свойство, можно воспользоваться двумя подходами:

  • Добавить новую колонку в текущую таблицу.
  • Добавить новую таблицу со ссылкой на родительскую и использовать соединение.

Рекомендуется использовать второй подход при расширении. Для этого нужно узнать все места, где происходит запись и изменить SQL код (хранимые процедуры) и чтение и изменить SQL коде (представление SQL).          

Важно помнить, что новый SQL объект должны иметь корректные права. Например, если новая таблица должна быть включена в CDХ и будет выполняться ее синхронизация, то она должна обладать ролью DataSyncUsersRole.

Пример:

IF (SELECT OBJECT_ID('ax.RETAILCUSTPREFERENCE')) IS NULL 
BEGIN
    CREATE TABLE [ax].[RETAILCUSTPREFERENCE](
    // removed . . . 
    ) ON [PRIMARY]
    // removed . . . 
END
GO
-- grant Read/Insert/Update/Delete permission to DataSyncUserRole so CDX can function
GRANT SELECT ON OBJECT::[ax].[RETAILCUSTPREFERENCE] TO [DataSyncUsersRole]
GO
GRANT INSERT ON OBJECT::[ax].[RETAILCUSTPREFERENCE] TO [DataSyncUsersRole]
GO
GRANT UPDATE ON OBJECT::[ax].[RETAILCUSTPREFERENCE] TO [DataSyncUsersRole]
GO
GRANT DELETE ON OBJECT::[ax].[RETAILCUSTPREFERENCE] TO [DataSyncUsersRole]
GO

После необходимо данный SQL скрипт зарегистрировать в файле Customization.setting, который находится в RetailSDK/BuildTools.

Используем расширяемость свойств классов запросов и ответов в CRT

Как и объекты CRT, запросы и ответы можно расширять.

Пример:

request.SetProperty("BoolPropertyName", true);

response.SetProperty("BoolPropertyName2", true);

bool? BoolPropertyName = (bool?)request.GetProperty("BoolPropertyName");

bool? BoolPropertyName2 = (bool?)response.GetProperty("BoolPropertyName2");

Создание нового сервиса CRT, который управляет несколькими запросам

Типичный случай, когда нужно добавить новый сервис. Сначала необходимо создать новый классы запроса и ответа.

Для того, чтобы класс мог быть сериализован, необходимо указать аттрибуты [DataContract] и [DataMemeber]. Создадим класс запроса.

Пример:

using System.Runtime.Serialization;
using Microsoft.Dynamics.Commerce.Runtime.Messages;

[DataContract]
public sealed class GetStoreHoursDataRequest : Request
{
    public GetStoreHoursDataRequest(string storeNumber)
    {
        this.StoreNumber = storeNumber;
    }

    [DataMember]
    public string StoreNumber { get; private set; }
}

Создадим класс ответа.

Пример:

[DataContract]
public sealed class GetStoreHoursDataResponse : Response
{
    public GetStoreHoursDataResponse(PagedResult dayHours)
    {
        this.DayHours = dayHours;
    }

    [DataMember]
    public PagedResult DayHours { get; private set; }
}

Далее необходимо создать класс сервиса, который будет использовать созданные раннее классы запроса и ответа.

Пример:

public class StoreHoursDataService : IRequestHandler
{
    public IEnumerable SupportedRequestTypes
    {
        get
        {
            return new[]
            {
                typeof(GetStoreHoursDataRequest),
            };
        }
    }
}
public Response Execute(Request request);

Интерфейс IRequestHandler включает в себя два метода:

  1. SupportedRequestTypes — возвращает список всех запросов, которые сервис поддерживает.
  2. Execute – это метод, который вызывает CRT, если запрос может быть обработан данным сервисом.

Создание нового сервиса CRT, который управляет одним запросом

Пример:

public class CrossLoyaltyCardService : SingleRequestHandler

Все остальные действия описаны в предыдущем пункте.

Создание нового CRT объекта

При создании нового объекта будем наследоваться от класса CommerceEntity. Использование системного объекта позволяет использовать существующую функциональность, определенную на более низком уровне. В примере создадим объект, привязанный к базе данных.

Пример:

public class DayHours : CommerceEntity
{
    private const string DayColumn = "DAY";
    private const string OpenTimeColumn = "OPENTIME";
    private const string CloseTimeColumn = "CLOSINGTIME";
    private const string IdColumn = "RECID";

    public StoreDayHours()
        : base("StoreDayHours")
    {
    }

    [DataMember]
    [Column(DayColumn)]
    public int DayOfWeek
    {
        get { return (int)this[DayColumn]; }
        set { this[DayColumn] = value; }
    }

    [DataMember]
    [Column(OpenTimeColumn)]
    public int OpenTime
    {
        get { return (int)this[OpenTimeColumn]; }
        set { this[OpenTimeColumn] = value; }
    }

    [DataMember]
    [Column(CloseTimeColumn)]
    public int CloseTime
    {
        get { return (int)this[CloseTimeColumn]; }
        set { this[CloseTimeColumn] = value; }
    }

    [Key]
    [DataMember]
    [Column(IdColumn)]
    public long Id
    {
        get { return (long)this[IdColumn]; }
        set { this[IdColumn] = value; }
    }
}

Атрибут Column указывает на физическое имя поля таблицы. Атрибут Key, что поле является ключевым, у каждого объекта должен быть ключ.

В следующем примере прочитаем данные из базы в объект.

Пример:

private GetStoreHoursDataResponse GetStoreDayHours(GetStoreHoursDataRequest request)
{
    ThrowIf.Null(request, "request");
    using (DatabaseContext databaseContext = new DatabaseContext(request.RequestContext))
    {
        var query = new SqlPagedQuery(request.QueryResultSettings)
        {
            DatabaseSchema = "crt",
            Select = new ColumnSet("DAY", "OPENTIME", "CLOSINGTIME", "RECID"),
            From = "ISVRETAILSTOREHOURSVIEW",
            Where = "STORENUMBER = @storeNumber",
        };

        query.Parameters["@storeNumber"] = request.StoreNumber;
        return new GetStoreHoursDataResponse(databaseContext.ReadEntity(query));
    }
}

В методе GetStoreDayHours CRT автоматически сгенерирует запрос к базе данных розничного канала через зарегистрированный Data adapter и вернет результат.

Определим pre- и post-триггеры для запроса

В некоторых случаях, некоторые процессы должны выполняться до или после обработки запроса. Для этого используются два события, которые называются pre-triggers и post-triggers. Создадим новый триггер и привяжем его к запросу.

Пример:

1. Класс должен принадлежать интерфейсу IRequestTrigger.

public class GetCrossLoyaltyCardRequestTrigger : IRequestTrigger

2. В IRequest.SupportedRequestTypes находится список запросов, для которых будет вызываться данный триггер.

public IEnumerable SupportedRequestTypes
{
    get
    {
        return new[] { typeof(GetCrossLoyaltyCardRequest) };
    }
}

3. Объявляем методы, которые будут вызываться до и после выполнения запроса

public void OnExecuted(Request request, Response response)
{
    ThrowIf.Null(request, "request");
    ThrowIf.Null(response, "response");

    //Custom logic
}
public void OnExecuting(Request request)
{
    //Custom logic
}

Scenarios Extensibility

Добавим новое действие для существующего контроллера. Для этого необходимо определить действие в контроллере и переопределить методы в model factory.

Добавим в существующий контроллер новое действие POST.

Пример:

public class MyCustomersController : CustomersController
{
    [HttpPost]
    [CommerceAuthorization(AllowedRetailRoles = new string[] { CommerceRoles.Customer, CommerceRoles.Employee })]
    public decimal GetCrossLoyaltyCardDiscountAction(ODataActionParameters parameters)
    {
        if (parameters == null)
        {
            throw new ArgumentNullException("parameters");
        }

        var runtime = CommerceRuntimeManager.CreateRuntime(this.CommercePrincipal);
        string loyaltyCardNumber = (string)parameters["LoyaltyCardNumber"];
        GetCrossLoyaltyCardResponse resp = runtime.Execute(new GetCrossLoyaltyCardRequest(loyaltyCardNumber), null);

        string logMessage = "GetCrossLoyaltyCardAction successfully handled with card number '{0}'. Returned discount '{1}'.";
        RetailLogger.Log.ExtendedInformationalEvent(logMessage, loyaltyCardNumber, resp.Discount.ToString());
        return resp.Discount;
    }
}

Здесь видим новый атрибут CommerceAuthorization, который определяет доступность метода при его вызове.

Переопределим в model factory

Пример:

[Export(typeof(IEdmModelFactory))]
[ComVisible(false)]
public class CustomizedEdmModelFactory : CommerceModelFactory
{
    protected override void BuildActions()
    {
        base.BuildActions();
        var var1 = CommerceModelFactory.BindEntitySetAction("GetCrossLoyaltyCardDiscountAction");
        var1.Parameter("LoyaltyCardNumber");
        var1.Returns();
    }
}

Добавим новый простой контроллер для объекта.

Предположим, что есть простой объект и требуется, чтобы контроллер извлекал данные для него. Чтобы создать новый контроллер, наследуемся от CommerceController, чтобы стала доступна вся низкоуровневая функциональность. Важно, чтобы имя контроллера соответствовало имени объекта.

Пример:

[ComVisible(false)]
public class StoreHoursController : CommerceController
{
    public override string ControllerName
    {
        get { return "StoreHours"; }
    }

    [HttpPost]
    [CommerceAuthorization(AllowedRetailRoles = new string[] { CommerceRoles.Anonymous, CommerceRoles.Customer, CommerceRoles.Device, CommerceRoles.Employee })]
    public System.Web.OData.PageResult GetStoreDaysByStore(ODataActionParameters parameters)
    {
        if (parameters == null)
        {
            throw new ArgumentNullException("parameters");
        }

        var runtime = CommerceRuntimeManager.CreateRuntime(this.CommercePrincipal);

        QueryResultSettings queryResultSettings = QueryResultSettings.SingleRecord;
        queryResultSettings.Paging = new PagingInfo(10);

        var request = new GetStoreHoursDataRequest((string)parameters["StoreNumber"]) { QueryResultSettings = queryResultSettings };
        PagedResult hours = runtime.Execute(request, null).DayHours;
        return this.ProcessPagedResults(hours);
    }
}

Для нового объекта переопределим метод BuildEntitySets() в model factory.

Пример:

[Export(typeof(IEdmModelFactory))]
[ComVisible(false)]
public class CustomizedEdmModelFactory : CommerceModelFactory
{
    protected override void BuildActions()
    {
        base.BuildActions();
        var action = CommerceModelFactory.BindEntitySetAction("GetStoreDaysByStore");
        action.Parameter("StoreNumber");
        action.ReturnsCollectionFromEntitySet("StoreHours");
    }

    protected override void BuildEntitySets()
    {
        base.BuildEntitySets();
        CommerceModelFactory.BuildEntitySet("StoreHours");
    }
}

 

Как будет вызван новый Retail Server API в POS?

Пример:

Cross loyalty

var request: Commerce.Proxy.Common.IDataServiceRequest = this._context.customers().getCrossLoyaltyCardDiscountAction(loyaltyCardNumber);

return request.execute<number>();

Store hours

var request: Commerce.Proxy.Common.IDataServiceRequest = this._context.storeHours().getStoreDaysByStore(storeId);

return request.execute<Commerce.Proxy.Entities.StoreDayHours[]>();

 

microsoft.com

Comments

So empty here ... leave a comment!

Добавить комментарий

Sidebar