Guido van Tricht https://guidovtricht.nl/ Fri, 16 Jun 2023 12:26:46 GMT http://blogs.law.harvard.edu/tech/rss https://github.com/jpmonette/feed en-US <![CDATA[Automated Sitecore content synchronization using Item as Resources]]> https://guidovtricht.nl/blog/automated-sitecore-content-synchronization-using-item-as-resources https://guidovtricht.nl/blog/automated-sitecore-content-synchronization-using-item-as-resources Fri, 16 Jun 2023 00:00:00 GMT <![CDATA[My SUGCON 2023 takeaways]]> https://guidovtricht.nl/blog/my-sugcon-2023-takeaways https://guidovtricht.nl/blog/my-sugcon-2023-takeaways Sun, 26 Mar 2023 00:00:00 GMT <![CDATA[Sitecore scaled environment loses session]]> https://guidovtricht.nl/blog/sitecore-scaled-environment-loses-session https://guidovtricht.nl/blog/sitecore-scaled-environment-loses-session Sat, 27 Aug 2022 00:00:00 GMT ``` Disabling Owin eventually let to the .ASPXAUTH cookie being created on login, but not fixing the loss of session issue. ## Machine Key To fully fix the issue, I had to specify a machine key in the web.config. This key is used to decrypt the .ASPXAUTH cookie on HTTP Requests. If this key is different from the one used to generate the cookie, then the cookie can't be decrypted and read. As I was using a scaled environment in Kubernetes, my Content Delivery containers did not share the same machine key. By generating machine key variables in my local IIS and specifying them in the web.config transform file, I was able to get everything to work. ```xml ``` I hope this will help others who face this same issue, and if not I at least know where to find the answer next time! *Note: I am not 100% sure if applying the machine key only would also have fixed the issue. This is something I still need to try but time has not allowed me to do so yet.*]]> <![CDATA[Generating unique placeholders in Sitecore Headless]]> https://guidovtricht.nl/blog/generating-unique-placeholders-in-sitecore-headless https://guidovtricht.nl/blog/generating-unique-placeholders-in-sitecore-headless Thu, 31 Mar 2022 00:00:00 GMT GetPlaceholderDefinitions(Rendering ownerRendering, PlaceholderItem placeholderItem) { Assert.ArgumentNotNull(ownerRendering, nameof(ownerRendering)); Assert.ArgumentNotNull(placeholderItem, nameof(placeholderItem)); if (string.IsNullOrEmpty(ownerRendering?.Placeholder)) { var emptyResult = new PlaceholderDefinition[1] { new PlaceholderDefinition() { OwnerRendering = ownerRendering, PlaceholderItem = placeholderItem, Path = placeholderItem.Key } }; return emptyResult; } int placeholderCount = this.GetPlaceholderCount(ownerRendering, placeholderItem); // Store the original DynamicPlaceholder because we can use it as a basis for our generated placeholders. var dynamicResult = new DynamicPlaceholderKeysResolver().GetDynamicKeys(placeholderItem.Key, ownerRendering, placeholderCount, 0).Select(key => { string str = StringUtil.EnsurePrefix('/', FileUtil.MakePath(ownerRendering.Placeholder, key, '/')); return new PlaceholderDefinition() { OwnerRendering = ownerRendering, PlaceholderItem = placeholderItem, Path = str }; }).ToList(); // If "GenerateChildPlaceholders" is set, we want to generate a placeholder for each Child item of the rendering Data Source var generateChildPlaceholders = ownerRendering.RenderingItem?.Parameters?.Contains("GenerateChildPlaceholders=1") ?? false; if (generateChildPlaceholders && dynamicResult.Any() && (ownerRendering.Items?.FirstOrDefault()?.Children.Any() ?? false)) { var basePlaceholderDefinition = dynamicResult.FirstOrDefault(); if (basePlaceholderDefinition != null) { var basePath = basePlaceholderDefinition.Path.Replace(basePlaceholderDefinition.Path.Split('/').Last(), ""); var childList = new List(); // For each child item of the datasource we will add an extra placeholder. These placeholders are based on the original dynamic placeholder, with the only difference being the Path property of the PlaceholderDefinition. foreach (var item in ownerRendering.Items.FirstOrDefault().Children.ToList()) { childList.Add(new PlaceholderDefinition { OwnerRendering = ownerRendering, PlaceholderItem = placeholderItem, Path = $"{basePath}{placeholderItem.Key.TrimEnd('*').ToLower()}-{item.ID.ToString().ToLower()}" }); } return childList; } } return dynamicResult; } ``` In above additional code I check for a setting "GenerateChildPlaceholders". This is set on the parameters field of the rendering item. An improvement would be to make this into an actual checkbox instead of freely adding it to some text field. The second part is making sure the placeholder path is being used in the JSON result instead of the name, as otherwise the placeholders keep overwriting each other in the array.\ These placeholders are added in the Sitecore.JavaScriptServices.ViewEngine.LayoutService.Serialization.PlaceholderTransformer.\ The piece of code we need to change is in the TransformPlaceholders, where the placeholders are added to an array: ```csharp dictionary[placeholder.Name] = ... ``` But we only want to do this for placeholders which were added by our earlier customization, so we also need to add a check in the TransformPlaceholderElement method.\ The result looks something like this: ```csharp public IDictionary CustomTransformPlaceholders(IList placeholders) {     if (placeholders == null)         return new Dictionary();     IDictionary dictionary = new ExpandoObject();     foreach (RenderedPlaceholder placeholder in placeholders)         // Using the placeholder.Path instead of placeholder.Name to be able to use multiple placeholders of the same type in a single component.         dictionary[placeholder.Path.Split('/').Last()] = placeholder.Elements.Select(this.TransformPlaceholderElement);     return dictionary; } public override object TransformPlaceholderElement(RenderedPlaceholderElement element) {     RenderedJsonRendering renderedJsonRendering = element as RenderedJsonRendering;     if (renderedJsonRendering == null)     {         return element;     }     dynamic val = new ExpandoObject();     val.uid = renderedJsonRendering.Uid;     val.componentName = renderedJsonRendering.ComponentName;     val.dataSource = renderedJsonRendering.DataSource;     val.@params = renderedJsonRendering.RenderingParams;     if (renderedJsonRendering.Contents != null)     {         val.fields = renderedJsonRendering.Contents;     }     if (renderedJsonRendering.Placeholders != null && renderedJsonRendering.Placeholders.Count > 0)     {         // If the Rendering Parameters field contains "GenerateChildPlaceholders" we want to apply custom logic         // This can be improved by making it a specific (checkbox) field on the Rendering Template or something similar         if (renderedJsonRendering.RenderingParams.TryGetValue("GenerateChildPlaceholders", out string paramVal)             && !string.IsNullOrEmpty(paramVal) && paramVal.Equals("1"))             val.placeholders = CustomTransformPlaceholders(renderedJsonRendering.Placeholders);         else             val.placeholders = TransformPlaceholders(renderedJsonRendering.Placeholders);     }     return val; } ``` Within this TransformPlaceholderElement method we also want to do a check on the existence of a "GenerateChildPlaceholders" to see if we need to trigger our customized code. However, this is a different source of the setting compared to the GetPlaceholderDefinitions customization because the context is different.\ Within the PlaceholderTransformer we only have knowledge about the rendering, some basic fields about the placeholder and the elements in it. We do not have access or even know which rendering is being rendered here. So, to fix this we need to set this "GenerateChildPlaceholders" as a field on the rendering parameters of the rendering, which can easily be done with a base template including this field as a checkbox which is set to be enabled by default. These changes to the Layout Service result in the following JSON for my Tabs component (each placeholder contains multiple different components): ```json {     "uid": "06487de2-8c65-49d8-a77b-a553d1b40caa",     "componentName": "Tabs",     "dataSource": "{3C357E6A-F6B1-4679-BCD0-0ECC7423EF46}",     "params": {         "GenerateChildPlaceholders": "1"     },     "fields": {         "items": [             {                 "id": "6717e695-25ab-4081-ac44-0feebed3df60",                 "url": "/Shared-Content/Tabs/Tabs-Example/Tab-1",                 "name": "Tab 1",                 "displayName": "Tab 1",                 "fields": {                     "Title": {                         "value": "Tab 1"                     }                 }             },             {                 "id": "a36af741-c233-4787-ad45-757821bfb2cc",                 "url": "/Shared-Content/Tabs/Tabs-Example/Tab-2",                 "name": "Tab 2",                 "displayName": "Tab 2",                 "fields": {                     "Title": {                         "value": "Tab 2"                     }                 }             }         ]     },     "placeholders": {         "dynamic-tab-{6717e695-25ab-4081-ac44-0feebed3df60}": [...],         "dynamic-tab-{a36af741-c233-4787-ad45-757821bfb2cc}": [...]     } } ``` On the Rendering Engine side, we need to make one change as well, which is actually more of a bug fix.\ The problem is that the *sc-placeholder* tag helper at some point, while rendering placeholders, changes its context component to the component it renders inside the placeholder. This is not a problem in the out of the box functionality, but in our case, it is because we want to render more than just one placeholder in a component.\ Due to methods being private in the original Placeholder Tag Helper, we need to copy all of it instead of being able to just override a single method. The result looks like: ```csharp [HtmlTargetElement("custom-placeholder")] public class PlaceholderTagHelper : TagHelper { private readonly IComponentRendererFactory _componentRendererFactory; private readonly IEditableChromeRenderer _chromeRenderer; [HtmlAttributeNotBound] [ViewContext] public ViewContext ViewContext { get; set; } public string Name { get; set; } public PlaceholderTagHelper(IComponentRendererFactory componentFactory, IEditableChromeRenderer chromeRenderer) { _componentRendererFactory = componentFactory; _chromeRenderer = chromeRenderer; } public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output) { string placeholderName = Name; if (string.IsNullOrEmpty(placeholderName)) { output.Content.SetHtmlContent(""); return; } output.TagName = string.Empty; if (ViewContext == null) { throw new NullReferenceException(Resources.Exception_ViewContextCannotBeNull); } ISitecoreRenderingContext renderingContext = ViewContext?.HttpContext.GetSitecoreRenderingContext(); if (renderingContext == null) { throw new NullReferenceException(Resources.Exception_SitecoreLayoutCannotBeNull); } Placeholder placeholderFeatures = GetPlaceholderFeatures(placeholderName, renderingContext); if (placeholderFeatures == null) { output.Content.SetHtmlContent(""); return; } bool foundPlaceholderFeatures = false; foreach (IPlaceholderFeature item in placeholderFeatures) { if (item != null) { foundPlaceholderFeatures = true; Component component = item as Component; IHtmlContent htmlContent; if (component == null) { EditableChrome editableChrome = item as EditableChrome; htmlContent = editableChrome == null ? HtmlString.Empty : _chromeRenderer.Render(editableChrome); } else { // BUGFIX // Take a copy of the Component and re-set it on the renderingContext after rendering the component. // This fixes a bug where the next placeholder in the outer component would have a wrong Component context. var outerComponent = renderingContext.Component; renderingContext.Component = component; htmlContent = await RenderComponent(renderingContext, ViewContext).ConfigureAwait(continueOnCapturedContext: false); renderingContext.Component = outerComponent; } output.Content.AppendHtml(htmlContent); } } if (!foundPlaceholderFeatures) { output.Content.SetHtmlContent(""); } } private Task RenderComponent(ISitecoreRenderingContext renderingContext, \[AllowNull] ViewContext viewContext) { if (renderingContext == null || viewContext == null) { throw new NullReferenceException(Resources.Exception_RenderingContextCannotBeNull); } Component component = renderingContext.Component; if (component == null) { throw new NullReferenceException(Resources.Exception_RenderingContextComponentCannotBeNull); } return _componentRendererFactory.GetRenderer(component).Render(renderingContext, viewContext); } private static Placeholder GetPlaceholderFeatures(string placeholderName, ISitecoreRenderingContext renderingContext) { Placeholder value = null; if (renderingContext.Component != null) { renderingContext.Component!.Placeholders.TryGetValue(placeholderName, out value); } if (value == null || !value.Any()) { (renderingContext.Response?.Content?.Sitecore?.Route)?.Placeholders.TryGetValue(placeholderName, out value); } return value; } } ``` In your components you can then use the tag helper to generate placeholders.\ Using this in my Tabs component results in the following ViewComponent: ```razor @model Tabs
    @foreach(var tab in Model.Children)     {        
           
                               

                           
           
                           
       
    }
``` As you can see, you then need to set the placeholder's name to the same value as was generated on the Layout Service side. This follows the following structure: \[placeholder key]-\[sitecore item ID]]]>
<![CDATA[Defining an OrderCloud platform architecture]]> https://guidovtricht.nl/blog/defining-an-ordercloud-platform-architecture https://guidovtricht.nl/blog/defining-an-ordercloud-platform-architecture Sat, 19 Mar 2022 00:00:00 GMT <![CDATA[Sharing my thoughts about the OrderCloud Headstart application]]> https://guidovtricht.nl/blog/sharing-my-thoughts-about-the-ordercloud-headstart-application https://guidovtricht.nl/blog/sharing-my-thoughts-about-the-ordercloud-headstart-application Sat, 12 Mar 2022 00:00:00 GMT *I want to emphasize that any opinions shared in this or any other article on this site are purely my own and are in no way related to any of the companies I worked for, currently work for or will work for in any future.*]]> <![CDATA[A first look at the OrderCloud Catalyst library]]> https://guidovtricht.nl/blog/a-first-look-at-the-ordercloud-catalyst-library https://guidovtricht.nl/blog/a-first-look-at-the-ordercloud-catalyst-library Sat, 26 Feb 2022 00:00:00 GMT claim.Type == "AccessToken")?.Value;     if (text != null)     {         UserContext = new DecodedToken(text);     }     base.OnActionExecuting(context); } ``` This DecodedToken object stored as UserContext contains some helpful properties about the user making the request, like: * AccessToken; This token can be used in the API calls to OrderCloud through the SDK to impersonate this user. * Username * Roles Once you have the Controller, you can create methods for your API. Those methods can be annotated using attributes. You can use the Microsoft.AspNetCore.Authorization.AuthorizeAttribute attribute for example to specify any authorization rules for the given request.\ OrderCloud has extended this attribute, as OrderCloud.Catalyst.OrderCloudUserAuth, so that you can specify which OrderCloud roles have access to this request. The roles of the context user making the request (UserContext.Roles) are then validated against the list of roles specified in this attribute. ```csharp [HttpGet(Name = "GetAllCatalogs"), OrderCloudUserAuth(ApiRole.Shopper)] public async Task Get() {     var result = await _catalogService.GetAllCatalogs(UserContext.AccessToken);     return !result.Ok ? Api.BadRequest(result) : Api.Ok(result); } ``` This way your application will do the validation before making any subsequent request to OrderCloud instead of OrderCloud returning an error. But you can also just use it for any custom-built functionalities which don't use OrderCloud at all. When you are building APIs which function as a proxy to OrderCloud rather than offering custom built functionalities, you will probably want to specify this OrderCloudUserAuth attribute using the roles which we know have access to the OrderCloud API. You can find these roles in the API reference documentation on ordercloud.io where each request shows which roles have access to that API. ## Caching The second functionality I want to cover is caching. Catalyst offers a very simple caching mechanism which by default is setup using LazyCache, which is a 3rd party library requiring no configuration but is only caching locally using memory. To use this caching mechanism, we can use the ISimpleCache service. This service only has two methods and by default is implemented using LazyCache. ```csharp public interface ISimpleCache { /// /// Get the value directly from the cache if it exists. If not, call addItemFactory() and cache the result for future use. /// /// Unique key pointing to a value in the cache /// Time before the cache is cleared. Also called "Time to Live" /// A function to calculate the value fully. /// Task GetOrAddAsync(string key, TimeSpan expireAfter, Func> addItemFactory); /// /// Remove the value from the cache. /// /// Unique key pointing to a value in the cache Task RemoveAsync(string key); } ``` To use a different caching implementation, we could overwrite the service registration with our own implementation for ISimpleCache. ```csharp public static void AddCustomCache(this IServiceCollection services) { services.AddSingleton(); } ``` This caching functionality might not seem like a lot, but a good reason to use it is because Catalyst uses this feature itself.\ The OrderCloudUserAuth attribute, as mentioned earlier, uses this ISimpleCache service to cache the token validation requests it makes to OrderCloud APIs. This way, when a user performs multiple actions in a short period of time, the token is only verified once every hour instead of it having to be verified on each request. Overall, the Catalyst library seems like a very neat set of functionalities which you could use in any Middleware implementation around OrderCloud. It is both easy to setup as well as use.\ In a next post I will be sharing some insights about the OrderCloud Headstart demo application as well as my unsalted thoughts about it.]]> <![CDATA[Exploring the OrderCloud .NET SDK]]> https://guidovtricht.nl/blog/exploring-the-ordercloud-net-sdk https://guidovtricht.nl/blog/exploring-the-ordercloud-net-sdk Sat, 19 Feb 2022 00:00:00 GMT GetAsync(string productID, string accessToken = null) where TProduct : Product; ``` As you can see, you can specify a TProduct when calling this method. TProduct needs to be based on OrderCloud.SDK.Product. We can use OrderCloud.SDK.Product as Type for these requests. Txp in this case can be any type of object and is assigned to the 'xp' property of the Product: ```csharp // Type parameters: //   Txp: //     Specific type of the xp property. If not using a custom type, use the non-generic //     Product class instead. public class Product : Product {     // Summary:     //     Container for extended (custom) properties of the product.     public new Txp xp     {         get        {             return GetProp("xp");         }         set         {             SetProp("xp", value);         }     } } ``` So, for Txp we can create a new model which specifies the properties our extended properties object should have, for example: ```csharp public class ProductXp {     public string Brand { get; set; } } ``` Now we can use these types to make the Get request to the OrderCloud APIs. The SDK will then deserialize the JSON response from OrderCloud and convert it to the type we specify. ```csharp var result = await client.Products.GetAsync>(productId); ``` This same principle is applied to the PartialProduct model: ```csharp public class PartialProduct : PartialProduct ``` ## Verdict I think the SDK is a very helpful library which really helps you understand and integrate with the APIs of OrderCloud.\ There are still some things I can learn, for example about how authentication in OrderCloudClients works exactly and if I have been using them correctly thus far in my journey.\ The structure of the OrderCloudClient is quite complex, just like the APIs themselves. This does, in my opinion, require you to build a shell around them instead of using the OrderCloudClient directly from your first layer of code. I would probably do that anyway to bring a certain level of abstraction in my application. In a next blog post, I will demonstrate the use of another toolkit provided by OrderCloud, the OrderCloud Catalyst. Stay tuned!]]> <![CDATA[Sitecore OrderCloud - Which SDK should I use?]]> https://guidovtricht.nl/blog/sitecore-ordercloud-which-sdk-should-i-use https://guidovtricht.nl/blog/sitecore-ordercloud-which-sdk-should-i-use Fri, 31 Dec 2021 00:00:00 GMT OrderCloud comes with a couple of different SDKs which you can use to base your application on. In this post I will be taking a look at the following: * Open API * JavaScript SDK * .NET SDK ## Open API You can build your application by directly using OrderCloud's APIs.\ Their APIs have been built based on the Open API standard. The specs can be found here: \ These specs can be imported in Postman to make it easier for you to interpret the APIs and to test them out. The APIs however are quite complex in my opinion, so I would not use them to directly integrate with in my application.\ The Postman collections do however give you a good understanding of what is possible in OrderCloud, so it is still great to have them while you are developing your application. One API you could use directly is the /oauth/token request to log a user in. This API will provide you with a token which you then need to use as a Bearer token to authenticate with the other APIs. ## JavaScript SDK OrderCloud provides a JavaScript SDK which can be used to build a typed integration in any client-side framework. The SDK has been built with TypeScript support, so you can also use it in TypeScript based frameworks. The SDK can easily be installed using NPM: `npm install ordercloud-javascript-sdk --save` The first thing you want to build for your application is a login form. When submitting this form you want to authenticate against OrderCloud's APIs. Doing this using the JS SDK is quite simple as you can do it using typed objects: ```javascript import { Auth, Tokens } from 'ordercloud-javascript-sdk'; const username = 'YOUR_USERNAME'; //username of the user logging in const password = 'YOUR_PASSWORD'; //password of the user logging in const clientID = 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx'; //clientID of the application the user is logging in to (\[sign up for free](https://portal.ordercloud.io/register) const scope = \['FullAccess']; //string array of \[roles](https://ordercloud.io/knowledge-base/security-profiles) the application has access to Auth.Login(username, password, clientID, scope)   .then(response => {       //store token, now any subsequent calls will automatically set this token in the headers for you       const token = response.access_token;       Tokens.SetAccessToken(token)   })   .catch(err => console.log(err)); ``` The Auth.Login method will return an access token. This token should be uses in any subsequent API calls to OrderCloud as it authenticates the user when performing the next request.\ As you can see in above example, this token is stored in the application context using Tokens.SetAccessToken. After doing this any next requests will automatically use the stored token. We can use any of OrderCloud's APIs in the same way as above Login functionality using type objects, like getting a list of products: ```javascript import { Me } from 'ordercloud-javascript-sdk'; // Get products Me.ListProducts()   .then(productList => console.log(productList)) ``` The full JavaScript SDK is available on GitHub and describes the many different options it offers: ## .NET SDK Very similar to the JavaScript SDK, OrderCloud also has a .NET C# based version of their SDK.\ The SDK is available as a NuGet package by the name "OrderCloud.SDK" and works with all the latest .NET versions (I used .NET 6.0). The .NET version of the SDK uses 'Clients' to perform API requests and store access tokens.\ The first thing you want to do is create one of those clients: ```csharp using OrderCloud.SDK; var client = new OrderCloudClient(new OrderCloudClientConfig {     ClientId = "my-client-id",     // client credentials grant flow:     ClientSecret = "my-client-secret"     // OR password grant flow:     Username = u,     Password = p,     Roles = new [] { ApiRole.OrderAdmin } }); ``` Each instance of a client automatically requests an access token on creation and stores it in it's context. As you can see in above code snippet there are 2 ways of authenticating using these clients, using a username-password combination or using a secret.\ This can be used to create a client for each user which authenticates with your application, but if you don't want to do that you can also use the impersonation approach, which is the approach I would choose when building browser based applications.\ With the impersonating approach you can create a single client using a ClientSecret for initial authentication. After that you can specify an access token which should be used for the API requests to OrderCloud. This access token you could get from your client-side application, so you would still need the JavaScript SDK or a direct API integration in your frontend application. Once you have a client, you can use it to perform API requests. ```csharp var orders = await client.Orders.ListAsync(OrderDirection.Incoming, filters: new { Status = OrderStatus.Open }); ``` Above example shows how you could get a list of orders from OrderCloud. The retrieved list will be based on the context user as defined in the client instance.\ If you want to impersonate a user, you can specify the access token in the same method: ```csharp var orders = await client.Orders.ListAsync(OrderDirection.Incoming, filters: new { Status = OrderStatus.Open }, access_token: token); ``` This SDK is also available on GitHub: ## Which approach should I choose? Each of these SDKs are valid options to build your application on, it really depends on the application you want to build, their purpose and your own opinion. In would probably always choose the .NET SDK as a basis for my application, simply because of all the possibilities .NET offers me.\ For the client-side application I might use the JavaScript SDK, but the only purpose of integrating with OrderCloud in my client-side is to authenticate a user and retrieve an access token. Using the full JavaScript SDK for only this purpose might be a bit overkill, so it might be easier to create a simple integration with the authentication API directly.]]> <![CDATA[Building a Flutter App on Sitecore Headless]]> https://guidovtricht.nl/blog/building-a-flutter-app-on-sitecore-headless https://guidovtricht.nl/blog/building-a-flutter-app-on-sitecore-headless Sat, 24 Jul 2021 00:00:00 GMT generateRoute(RouteSettings settings) { return MaterialPageRoute( builder: (_) => [build the response object here]); } } ``` *Router.dart* The RouteSettings object in this generateRoute function contains the path of the request, which we can use to do our Layout Service request to Sitecore. I then started building out a Page object to return to the view. This object would have to trigger the Layout Service call and render different Flutter Widgets based on the response JSON. ```dart class SitecoreLayoutServiceClient { Future requestLayout(String path) async { String apiUrl = "https://" + AppSettings.sitecoreCMHostname + "/sitecore/api/layout/render/jss"; return Requests.get(apiUrl, persistCookies: true, queryParameters: { 'sc_site': AppSettings.sitecoreSite, 'sc_apikey': AppSettings.sitecoreApiKey, 'item': path, 'sc_lang': AppSettings.sitecoreLanguage }); } } ``` *Layoutservice.dart* ```dart class _DefaultPageState extends State with WidgetsBindingObserver { @override Widget build(BuildContext context) { return FutureBuilder( builder: (context, AsyncSnapshot response) { if (response.hasData) { var widgetJson = response.data!.json(); var contextJson = widgetJson["sitecore"]["context"]; var routeJson = widgetJson["sitecore"]["route"]; return Scaffold( appBar: AppBar( title: Text(routeJson["displayName"].toString()), ), drawer: MaterialDrawer( currentPage: routeJson["displayName"].toString()), body: SingleChildScrollView( child: SitecorePlaceholder(routeJson["placeholders"]["main"]), ), ); } else { return Scaffold( body: Center( child: CircularProgressIndicator(), ), ); } }, future: _getWidget(), ); } Future _getWidget() async { return await SitecoreLayoutServiceClient().requestLayout(widget.path); } } ``` *Default_page.dart* Using this approach I was able to return a page with the page's Display Name as title. When navigating to different pages I would get to see the different Display Names of my Sitecore items. After I could see this work, I looked around on the internet to find people who did something similar, because the next part was a bit tricky if you didn't really knew what you were doing (like myself). It was nice to see the Display Name in the Mobile App, but that doesn't mean the PoC was actually done. The next part that had to happen was to actually show the renderings and their content on the screen, meaning I would have to loop through all components in a placeholder and determine which Flutter Widget to render based on some field. And to make it more complicated, I wanted to get it working with multiple levels of placeholders. I am not going to write all that I found and did to make this happen, instead I am sharing the entire PoC repository on GitHub: \[Sitecore Headless Flutter](https://github.com/GuidovTricht/SitecoreHeadlessFlutter) The way it works is that there is a SitecoreWidgetRegistry(sitecore_widget_registry.dart), containing string - Widget builder classes combinations which I would trigger when rendering a rendering from any placeholder. ```dart final _internalBuilders = { SitecoreHeroBannerBuilder.type: SitecoreWidgetBuilderContainer( builder: SitecoreHeroBannerBuilder.fromDynamic), SitecorePromoContainerBuilder.type: SitecoreWidgetBuilderContainer( builder: SitecorePromoContainerBuilder.fromDynamic), SitecorePromoCardBuilder.type: SitecoreWidgetBuilderContainer( builder: SitecorePromoCardBuilder.fromDynamic), SitecoreSectionHeaderBuilder.type: SitecoreWidgetBuilderContainer( builder: SitecoreSectionHeaderBuilder.fromDynamic), SitecoreFooterBuilder.type: SitecoreWidgetBuilderContainer( builder: SitecoreFooterBuilder.fromDynamic), }; ``` The registry would figure out which builder class to use (like sitecore_hero_banner_builder.dart), and the builder class would then return a Widget object to render on the screen. ```dart /// Returns the builder for the requested [type]. This will first search the /// registered custom builders, then if no builder is found, this will then /// search the library provided builders. /// /// If no builder is registered for the given [type] then this will throw an /// [Exception]. SitecoreWidgetBuilderBuilder getWidgetBuilder(String type) { var container = _customBuilders[type] ?? _internalBuilders[type]; if (container == null) { return PlaceholderBuilder.fromDynamic; } var builder = container.builder; return builder; } ``` The Widget builder would receive part of the Layout Service JSON response to render the Widget including the content as retrieved from Sitecore. ```dart class SitecoreHeroBannerBuilder extends SitecoreWidgetBuilder { SitecoreHeroBannerBuilder({ this.image, this.imageUrl, this.title, this.subtitle, }) : super(numSupportedChildren: kNumSupportedChildren); static const kNumSupportedChildren = 0; static const type = 'HeroBanner'; final dynamic image; final String? imageUrl; final String? title; final String? subtitle; static SitecoreHeroBannerBuilder? fromDynamic( dynamic map, { SitecoreWidgetRegistry? registry, }) { SitecoreHeroBannerBuilder? result; if (map != null) { result = SitecoreHeroBannerBuilder( image: map["Image"], title: map["Title"]["value"], subtitle: map["Subtitle"]["value"], ); } return result; } @override Widget buildCustom({ ChildWidgetBuilder? childBuilder, required BuildContext context, required SitecoreWidgetData data, Key? key, }) { return Container( height: 150, width: MediaQuery.of(context).size.width, child: Stack( children: [ Image(image: NetworkImage(image["value"]["src"])), Center( child: Column( children: [ Text( title!.toUpperCase(), style: GoogleFonts.ibmPlexMono( color: Colors.white, fontSize: 30, fontWeight: FontWeight.w500), ), Text( subtitle!.toUpperCase(), style: GoogleFonts.ibmPlexMono( color: Colors.white, fontSize: 15, fontWeight: FontWeight.w500), ) ], ), ) ], )); } } ``` This is all for now, but do let me know if you have any questions through Twitter, LinkedIn or Sitecore Slack.]]> <![CDATA[Exploring Sitecore Headless Services]]> https://guidovtricht.nl/blog/exploring-sitecore-headless-services https://guidovtricht.nl/blog/exploring-sitecore-headless-services Sat, 17 Jul 2021 00:00:00 GMT <![CDATA[Sitecore Rendering Engine - From Request to Response]]> https://guidovtricht.nl/blog/sitecore-rendering-engine-from-request-to-response https://guidovtricht.nl/blog/sitecore-rendering-engine-from-request-to-response Sun, 11 Apr 2021 00:00:00 GMT (httpContext, nameof (httpContext)); if (httpContext.Items.ContainsKey((object) nameof (RenderingEngineMiddleware))) throw new ApplicationException(Sitecore.AspNet.RenderingEngine.Resources.Exception_InvalidRenderingEngineConfiguration); if (httpContext.GetSitecoreRenderingContext() == null) { SitecoreLayoutResponse sitecoreLayoutResponse = await this.GetSitecoreLayoutResponse(httpContext).ConfigureAwait(false); httpContext.SetSitecoreRenderingContext((ISitecoreRenderingContext) new SitecoreRenderingContext() { Response = sitecoreLayoutResponse, RenderingHelpers = new RenderingHelpers(viewComponentHelper, htmlHelper) }); } else httpContext.GetSitecoreRenderingContext().RenderingHelpers = new RenderingHelpers(viewComponentHelper, htmlHelper); foreach (Action postRenderingAction in (IEnumerable>) this._options.PostRenderingActions) postRenderingAction(httpContext); httpContext.Items.Add((object) nameof (RenderingEngineMiddleware), (object) null); await this._next(httpContext).ConfigureAwait(false); } private async Task GetSitecoreLayoutResponse( HttpContext httpContext) { SitecoreLayoutRequest request = this._requestMapper.Map(httpContext.Request); Assert.NotNull(request); return await this._layoutService.Request(request).ConfigureAwait(false); } ``` This ISitecoreLayoutService will in turn trigger a request to the Layout Service and return it's response as a SitecoreLayoutResponse object. As you can see, this response object is then stored in the HttpContext. As the HttpContext is something that lives throughout the entire request lifetime, this means from this moment forth it will be possible to retrieve the raw Layout Service response from the HttpContext. And that is exactly the way Sitecore also uses this response, because as you can see this Middleware component doesn't do anything other than just requesting and storing that Layout Service object. ## Binding the models The next step in order to render a page, is populate the Route object parameter from the Index method in the fallback controller. This is done by using Model Binding (my [previous post](https://guidovtricht.nl/blog/model-binding-in-the-sitecore-rendering-engine) describes what this is).\ Sitecore has chosen to use model binding for pretty much all of the context related models. I am not going to describe the entire process of how these models are registered for usage in model binding, that would be too much information, but I want to highlight two steps/classes in this flow. If we take the Route object for example, then we can find a SitecoreLayoutRouteBindingSource class which is responsible for populating the Route object with details from the ISitecoreRenderingContext object. ```csharp public override object? GetModel( IServiceProvider serviceProvider, ModelBindingContext bindingContext, ISitecoreRenderingContext context) { Assert.ArgumentNotNull(serviceProvider, nameof (serviceProvider)); Assert.ArgumentNotNull(bindingContext, nameof (bindingContext)); Assert.ArgumentNotNull(context, nameof (context)); Type modelType = bindingContext.get_ModelMetadata().get_ModelType(); Route route = context.Response?.Content?.Sitecore?.Route; return route != null && modelType == typeof (Route) ? (object) route : (object) null; } ``` Where does this context object come from? This GetModel method is invoked by the SitecoreLayoutModelBinder, where T in this case if Route of course. This class will retrieve the ISitecoreRenderingContext from the HttpContext and passes it to the SitecoreLayoutRouteBindingSource to have it create the Route object. ```csharp public Task BindModelAsync(ModelBindingContext bindingContext) { Assert.ArgumentNotNull(bindingContext, nameof (bindingContext)); T obj1 = bindingContext.get_BindingSource() as T; if (BindingSource.op_Equality((BindingSource) (object) obj1, (BindingSource) null)) obj1 = Activator.CreateInstance(); ISitecoreRenderingContext renderingContext = bindingContext.get_HttpContext().GetSitecoreRenderingContext(); using (IServiceScope scope = ServiceProviderServiceExtensions.CreateScope(this._serviceProvider)) { object model = obj1.GetModel(scope.ServiceProvider, bindingContext, renderingContext); if (model != null) { ValidationStateDictionary validationState = bindingContext.get_ValidationState(); object obj2 = model; ValidationStateEntry validationStateEntry = new ValidationStateEntry(); validationStateEntry.set_SuppressValidation(true); validationState.Add(obj2, validationStateEntry); bindingContext.set_Result(ModelBindingResult.Success(model)); } else { ... } } return Task.CompletedTask; } ``` This approach is used for many such objects, like the Response, Component, Context and, as we have seen, the Route object. ## Almost there We now finally have a pretty complete picture of how a request to the ASP.NET Core application results in Middleware components being triggered, the Sitecore Layout Service being called and used for Model Binding to finally have a fairly straight forward MVC Controller triggered and it's parameters populated.\ From here, the path to generating the result page is more simple. The populated Route object contains all the details necessary to render a page, it includes the different placeholders on the page, components and their content. If you take a quick peek at the View for this Index method, you will see this will use tag helpers to render different Sitecore placeholder components which, based on the name property given, will render parts of the Layout Service response. I hope these posts have helped you get a better understanding of how the ASP.NET Core Rendering Host works and figure out how you can fully customize it for your needs.]]> <![CDATA[Model binding in the Sitecore Rendering Engine]]> https://guidovtricht.nl/blog/model-binding-in-the-sitecore-rendering-engine https://guidovtricht.nl/blog/model-binding-in-the-sitecore-rendering-engine Wed, 23 Dec 2020 00:00:00 GMT Create([FromBody] Pet pet) ``` You can configure your own model binding sources/providers, which Sitecore also did in the AddSitecoreRenderingEngine() step of the Startup.cs.\ When a page request is then being processed, ASP.NET will check the parameters of your method to see if there are any model binding sources or providers configured which can give a result for the Type of the parameter. Sitecore has registered a couple of BindingSources which can fetch a result for a specific Type. In case of the Route object for example, the model binding process will initiate Sitecore's ModelBinder SitecoreLayoutModelBinder (where T in this case is Route). This ModelBinder will then create an instance of the BindingSource which can get a result for this type T. For the Route object, it will use SitecoreLayoutRouteBindingSource. This BindingSource will then be called upon to get the model which is fetched from the Layout Service response stored in the HttpContext as described in the previous post. ![GetModel example from SitecoreLayoutRouteBindingSource](/images/uploads/sc-model-binding-getmodel.png "GetModel example from SitecoreLayoutRouteBindingSource") ## Why should I know this? You probably won't have to deal with all of this very often, but as this model binding process is most probably(haven't figured that part out yet) also used in populating the Component context I thought it would be a great starting point for an introduction to the concept of model binding.\ Next to that, you might find it useful to use these existing model binding providers. Apart from the Route object you can also use the Sitecore.LayoutService.Client.Response.Model.Context object for example, which contains the details whether or not the page is requested in editing mode, the language of the response and some Site details. I hope to get a little more hands-on to the model binding used for actually rendering a component in a later blog post! ### References * ASP.Net Core model binding: * ASP.NET Core custom model binding: * Previous post about Sitecore Rendering Engine routing: ]]> <![CDATA[Sitecore Rendering Engine routing explained]]> https://guidovtricht.nl/blog/sitecore-rendering-engine-routing-explained https://guidovtricht.nl/blog/sitecore-rendering-engine-routing-explained Mon, 30 Nov 2020 00:00:00 GMT { endpoints.MapDefaultControllerRoute(); endpoints.MapRazorPages(); }); } ``` Using this configuration each request would follow a pipeline process like: 1. UseHttpsRedirection; Redirect HTTP to HTTPS 2. UseStaticFiles; If the request path corresponds to a file on disk, return that file. 3. UseRouting; Enable the usage of endpoints configuration 1. MapDefaultControllerRoute; Check if controller and action exists corresponding with a default MVC route like /{controller}/{action}/{id?} and execute it 2. MapRazorPages; Return a Razor Page result if any correspond to the path Each of these middleware components can terminate the pipeline, so if a static file exists corresponding to the request path it won't trigger the components after that. ## The Sitecore way Now that we know what comes out-of-the-box with ASP.NET Core, what is it that Sitecore built on top of this?\ The Sitecore Rendering Engine inserts its own middleware components, but doesn’t actually use such a component to handle routing directly. Instead they assign a ‘fallback’ controller, resulting in all requests being processed by the same controller and action. ```csharp app.UseEndpoints(endpoints => { endpoints.MapControllerRoute( "error", "error", new { controller = "Default", action = "Error" } ); endpoints.MapControllerRoute( "healthz", "healthz", new { controller = "Default", action = "Healthz" } ); // Enables the default Sitecore URL pattern with a language prefix. endpoints.MapSitecoreLocalizedRoute("sitecore", "Index", "Default"); // Fall back to language-less routing as well, and use the default culture (en). endpoints.MapFallbackToController("Index", "Default"); }); ``` A DefaultController with method Index is created in the web project for this fallback purpose. ```csharp [UseSitecoreRendering] public IActionResult Index(Route route) { ... return View(route); } ``` This Index method has two key characteristic. First it has a parameter of object type Route and second it has a data attribute UseSitecoreRendering. Populating this Route parameter is a two step process: As a first step, the UseSitecoreRendering attribute adds another middleware component to the request configuration. This component then triggers a request to the Sitecore Layout Service to fetch all the page and rendering data for the context page. This Layout Service request is performed by an ISitecoreLayoutClient service, which is configured in the ConfigureServices method of the Startup.cs: ```csharp // Register the Sitecore Layout Service Client, which will be invoked by the Sitecore Rendering Engine. services.AddSitecoreLayoutService() // Set default parameters for the Layout Service Client from our bound configuration object. .WithDefaultRequestOptions(request => { request .SiteName(Configuration.DefaultSiteName) .ApiKey(Configuration.ApiKey); }) .AddHttpHandler("default", Configuration.LayoutServiceUri) .AsDefaultHandler(); ``` What is important to note here, is that this is the place the site name and API key for the requests are set. These settings are added to the request query string for the Layout Service to determine the context site of the request and to check if the application actually has the authorization to perform this request based on the API key. Once the middleware component has received the response, it creates a SitecoreRenderingContext object including the Layout Service response and stores it in the HttpContext. The second step then uses this SitecoreRenderingContext object. Sitecore has added a BindingSource called SitecoreLayoutRouteBindingSource, which then takes the Route object from the SitecoreRenderingContext in the HttpContext and returns is. Through model binding this Route object is that the source for populating the Route object parameter of the Index method. ## In action So let's see this in action. Taking the MVP-Site example setup (), when I open the About page of the MVP site I get to see the following page. ![About page of MVP site](/images/uploads/about-page.png "About page of MVP site") During the request processing in the Rendering Engine, the ISitecoreLayoutClient would make a GET request using the following URL:\ https://mvp-cd.sc.localhost/sitecore/api/layout/render/jss?item=/About&sc_lang=en&sc_apikey={E2F3D43E-B1FD-495E-B4B1-84579892422A}&sc_site=mvp-site Note that the URL contains the request path in the query string as "item=/About", just like it contains the language, API key and site name. If we were to open this URL in directly in a browser, it would show us the raw Layout Service response. ![Layout Service JSON result of about page](/images/uploads/about-json.png "Layout Service JSON result of about page") Based on this JSON response the Rendering Engine then renders the entire page. Let's take a closer look at how this rendering process works in a next blog post!]]> <![CDATA[Extending SXA asset rendering]]> https://guidovtricht.nl/blog/extending-sxa-asset-rendering https://guidovtricht.nl/blog/extending-sxa-asset-rendering Sun, 26 Jul 2020 00:00:00 GMT ``` Each processor in this pipeline looks for assets needed and adds them to the AssetsList in the processor args. In the pipeline we also have access to the PageContext, which contains a list of all renderings on that page. ```csharp foreach (Rendering rendering in GetRenderingsList()) ``` By adding an extra Treelist field to the Controller rendering template, we can associate Base Themes to specific renderings. ```csharp Sitecore.Data.Fields.MultilistField baseThemes = item.Fields[Templates.RenderingAssets.Fields.BaseThemes]; foreach (var theme in baseThemes.GetItems()) { if (theme == null || assetsList.Any(i => i is ThemeInclude assetInclude && assetInclude.ThemeId == theme.ID)) continue; renderingThemes.Add(new ThemeInclude { Name = theme.Name, ThemeId = theme.ID, Theme = theme }); } ``` Within a custom processor in the assetService pipeline we can then pull those Base Themes and add the assets to the AssetsList if not already present. The end result: ```csharp public class AddRenderingAssets : AddAssetsProcessor { public override void Process(AssetsArgs args) { var assetsList = args.AssetsList as List ?? new List(); foreach (Rendering rendering in GetRenderingsList()) { var item = GetRenderingItem(rendering); if (item == null) continue; var cacheKey = $"{Context.Database.Name}#{item.ID}#{this.Context.Device.ID}#RenderingAssets"; var renderingThemes = (List)HttpRuntime.Cache.Get(cacheKey); if (renderingThemes == null) { renderingThemes = new List(); Sitecore.Data.Fields.MultilistField baseThemes = item.Fields[Templates.RenderingAssets.Fields.BaseThemes]; foreach (var theme in baseThemes.GetItems()) { if (theme == null || assetsList.Any(i => i is ThemeInclude assetInclude && assetInclude.ThemeId == theme.ID)) continue; renderingThemes.Add(new ThemeInclude { Name = theme.Name, ThemeId = theme.ID, Theme = theme }); } if (global::Sitecore.Context.PageMode.IsNormal && rendering.RenderingItem.Caching.Cacheable) this.CacheRenderingAssets(cacheKey, renderingThemes); } assetsList.AddRange(renderingThemes); } } private List GetRenderingsList() { return global::Sitecore.Mvc.Presentation.PageContext.Current.PageDefinition.Renderings; } private Item GetRenderingItem(Rendering rendering) { if (rendering.RenderingItem == null) { Log.Warn($"rendering.RenderingItem is null for {rendering.RenderingItemPath}", this); return null; } return rendering.RenderingItem.InnerItem; } private void CacheRenderingAssets(string key, List assets) { if (assets == null || !assets.Any()) return; HttpRuntime.Cache.Add(key, assets, null, Cache.NoAbsoluteExpiration, Cache.NoSlidingExpiration, CacheItemPriority.Normal, null); } } ``` Small changes like these help SXA become even more flexible and bend it to the needs of your website and team.]]> <![CDATA[A new JAMstack based website]]> https://guidovtricht.nl/blog/a-new-jamstack-based-website https://guidovtricht.nl/blog/a-new-jamstack-based-website Mon, 25 May 2020 00:00:00 GMT * * ## Netlify Reading everything about JAMstack begged the question whether or not I really needed a database for my blog. There are quite a couple of 'headless' CMS options out there which would do the trick as well. One offered by Netlify as well, Netlify CMS. So I decided to setup a Netlify website based on Netlify CMS. The easiest approach was just to use a starter kit, but then again there are a couple of options available around which frontend framework to use. In this case I looked at both Nuxt.js and Gatsby and eventually chose for NuxtJS based on Vue and TypeScript(I would later regret the TypeScript option..). Interested in other options? Check out these helpful sites: * Static Site Generators: * Headless CMS options: Netlify CMS uses Git as kind of the storage for your pages. The master branch is always deployed to the Netlify CI/CD and feature branches are being used for creating new pages and creating a simple publishing workflow. All pages are then stored as JSON files inside your repository. You can define different fields and types of fields, but eventually all of them are stored inside one JSON file which will then be read out to create/generate your website. ## Learnings So the stack I ended up with is as follows: * Hosting: [Netlify](https://www.netlify.com/) * CMS: [Netlify CMS](https://www.netlifycms.org/) * JavaScript framework: [NuxtJS](https://nuxtjs.org) * CSS framework: [Tailwind CSS](https://tailwindcss.com/) It is a very easy to setup stack and easy to get started with hosting it in Netlify, however I did hit some bumps along the way. One thing that really took me some time is understand TypeScript and how to use VueJS(NuxtJS is based on VueJS) with TypeScript. There is very little documentation on how to setup your VueJS app with TypeScript. Everything is described in much detail for JavaScript, but for TypeScript you basically have to figure it out yourself. So if you are not a seasoned TypeScript developer, I would suggest going with JavaScript if you are going to work with VueJS.]]> <![CDATA[Extending Sitecore Experience Commerce]]> https://guidovtricht.nl/blog/extending-sitecore-experience-commerce https://guidovtricht.nl/blog/extending-sitecore-experience-commerce Sat, 09 Nov 2019 00:00:00 GMT \ A couple of these repositories are also described in my other blog posts.]]> <![CDATA[Sitecore Experience Commerce – Vertex tax calculation]]> https://guidovtricht.nl/blog/sitecore-experience-commerce-vertex-tax-calculation https://guidovtricht.nl/blog/sitecore-experience-commerce-vertex-tax-calculation Tue, 05 Nov 2019 00:00:00 GMT ]]> <![CDATA[Sitecore Experience Commerce – SyncForce product import]]> https://guidovtricht.nl/blog/sitecore-experience-commerce-syncforce-product-import https://guidovtricht.nl/blog/sitecore-experience-commerce-syncforce-product-import Sun, 06 Oct 2019 00:00:00 GMT \ This Plugin should be enough to get started and create the base structure of your Catalog. If you decide to use this you might want to extend the SynchronizeProduct Pipeline to import your specific product details.]]> <![CDATA[Sitecore Experience Commerce – Product images]]> https://guidovtricht.nl/blog/sitecore-experience-commerce-product-images https://guidovtricht.nl/blog/sitecore-experience-commerce-product-images Sat, 01 Jun 2019 00:00:00 GMT > GetData() { var list = new List(); return Task.FromResult>(list.AsQueryable()); } } ``` This MediaItemDataModel inherits the EntityIdentity as used in the default Item Service. We don’t need anything else, because all we need to know in XC is the Sitecore Item ID. It is a bit weird that we have to create a IReadOnlyEntityRepository as we don’t actually do anything with it, but this is only because the OData controller we are creating needs a IReadOnlyEntityRepository. ```csharp [AnonymousUserFilter (AllowAnonymous = AllowAnonymousOptions.Disallow)] public class MediaItemController : ServiceBaseODataController { private readonly IReadOnlyEntityRepository _repository; public MediaItemController () : base (ServiceLocator.ServiceProvider.GetRequiredService> ()) { _repository = ServiceLocator.ServiceProvider.GetRequiredService> (); } public IHttpActionResult Get ([FromODataUri] string imageUrl, [FromODataUri] string sitecorePath) { if (string.IsNullOrEmpty (imageUrl)) return BadRequest ("Specify the imageUrl property."); if (string.IsNullOrEmpty (sitecorePath)) return BadRequest ("Specify the sitecorePath property."); if (!sitecorePath.StartsWith ("/sitecore/media library/")) sitecorePath = sitecorePath.StartsWith ("/") ? $"/sitecore/media library{sitecorePath}" : $"/sitecore/media library/{sitecorePath}"; var fileNameInclExtension = imageUrl.Substring (imageUrl.LastIndexOf ("/", StringComparison.InvariantCultureIgnoreCase) + 1, (imageUrl.Length - imageUrl.LastIndexOf ("/", StringComparison.InvariantCultureIgnoreCase)) - 1).Split ('?').FirstOrDefault (); if (string.IsNullOrEmpty (fileNameInclExtension)) return BadRequest ("No fileName could be extracted from the imageUrl."); var fileName = fileNameInclExtension.Split ('.').FirstOrDefault () ?? fileNameInclExtension; var newItem = CreateMediaItem (imageUrl, sitecorePath, fileName, fileNameInclExtension); PublishItem (newItem); return Ok (new MediaItemDataModel () { Id = newItem.ID.ToString () }); } private Item CreateMediaItem (string imageUrl, string sitecorePath, string mediaItemName, string fullMediaItemName) { var destination = sitecorePath.EndsWith ("/") ? $"{sitecorePath}{mediaItemName}" : $"{sitecorePath}/{mediaItemName}"; var options = new Sitecore.Resources.Media.MediaCreatorOptions () { FileBased = false, IncludeExtensionInItemName = false, OverwriteExisting = true, Versioned = false, Destination = destination, Database = Sitecore.Configuration.Factory.GetDatabase ("master") }; using (var wc = new System.Net.WebClient ()) { var daa = wc.DownloadData (imageUrl); using (var memoryStream = new MemoryStream (daa)) { using (new SecurityDisabler ()) { var mediaItem = Sitecore.Resources.Media.MediaManager.Creator.CreateFromStream (memoryStream, fullMediaItemName, options); memoryStream.Dispose (); return mediaItem; } } } } private void PublishItem (Item item) { try { PublishOptions po = new PublishOptions (Sitecore.Configuration.Factory.GetDatabase ("master"), Sitecore.Configuration.Factory.GetDatabase ("web"), PublishMode.SingleItem, Sitecore.Context.Language, DateTime.Now); po.RootItem = item; po.Deep = true; // Publishing subitems (new Publisher (po)).Publish (); } catch (Exception ex) { Sitecore.Diagnostics.Log.Error ("Exception publishing items from custom pipeline! : " + ex, this); } } } ``` In this example implementation, we get a URL to the image we want to upload and a Sitecore Path which will be the parent of the new Sitecore Item. Using these two variables we download the image, create a Media Item for it and eventually publish the new Item. Note the attribute we use on this Controller. If you don’t specifically set any authentication/authorization on these services they will default to be available to anybody, and that’s something we don’t want of course. The manager we will use on Sitecore XC side already sends authorization properties out-of-the-box, so we don’t actually have to do anything for that matter. Next we need to register the repository: ```csharp public class CustomServiceConfigurator : IServicesConfigurator { public void Configure(IServiceCollection serviceCollection) { var assemblies = new[] { GetType().Assembly }; serviceCollection.AddWebApiControllers(assemblies); serviceCollection.AddSingleton, MediaItemRepository>(); } } ``` ```xml ``` And last but not least we have to actually register the OData Service: ```csharp public class MediaItemServiceDescriptor : AggregateDescriptor { public MediaItemServiceDescriptor() : base( "media", //route name "media", //route prefix new DefaultEdmModelBuilder(new [] { new EntitySetDefinition(typeof(MediaItemDataModel), "MediaItem") })) { } } ``` ## Sitecore XC implementation Now that we have done all of that we are done on the Sitecore XP part and we can start using it in our XC implementation. Sitecore XC has a SitecoreConnectionManager which they use to get Item data from Sitecore XP. We can use this same manager to call our custom OData Service.\ All we have to provide the manager with is a CommerceContext, OData service URL, a method and a ItemModel. This ItemModel input is something Sitecore uses specifically for calling the Item Service. Instead of using that, we can also add our properties to the action input.\ Eventually the implementation will look something like this: ```csharp /// /// Use the OData Sitecore Service to upload the asset to the Sitecore Media Library and return the Item ID. /// /// The Commerce Context. /// The asset url. /// The Sitecore path in which the asset should be uploaded. /// The uploaded Item ID. private async Task UploadAsset(CommerceContext context, string url, string sitecorePath) { try { var action = $"sitecore/api/ssc/aggregate/media/MediaItem?imageUrl={HttpUtility.UrlEncode(url)}&sitecorePath={sitecorePath}"; var httpResponseMessage = await _sitecoreConnectionManager.ProcessRequestAsync(context, action, "GET", null); if (httpResponseMessage == null || !httpResponseMessage.IsSuccessStatusCode) return ""; return JsonConvert.DeserializeObject(await httpResponseMessage.Content.ReadAsStringAsync())?.Id ?? ""; } catch (Exception ex) { context.LogException(nameof(SynchronizeSellableItemDigitalAssetsBlock), ex); return ""; } } ``` And with that, you are done! We now have all the building blocks to automatically import Media Items from inside a Sitecore Commerce process instead of somehow uploading them separately.]]> <![CDATA[Sitecore Experience Commerce – Creating a catalog]]> https://guidovtricht.nl/blog/sitecore-experience-commerce-creating-a-catalog https://guidovtricht.nl/blog/sitecore-experience-commerce-creating-a-catalog Tue, 23 Oct 2018 00:00:00 GMT (Note that this example doesn't contain the List and Minion to create the SellableItems) But I do want to point out some steps and code snippets. For example, on how to create an Entity. In the following snippet creating a Category. ```csharp public override async Task Run(SynchronizeCategoryArgument arg, CommercePipelineExecutionContext context) { Sitecore.Commerce.Plugin.Catalog.Category category = null; var categoryId = $"{CommerceEntity.IdPrefix()}{arg.Catalog.Name}-{arg.ImportCategory.CategoryId.ProposeValidId()}"; if (await _doesEntityExistPipeline.Run( new FindEntityArgument(typeof(Sitecore.Commerce.Plugin.Catalog.Category), categoryId), context.CommerceContext.GetPipelineContextOptions())) { category = await _findEntityPipeline.Run(new FindEntityArgument(typeof(Sitecore.Commerce.Plugin.Catalog.Category), categoryId), context.CommerceContext.GetPipelineContextOptions()) as Sitecore.Commerce.Plugin.Catalog.Category; } else { var createResult = await _createCategoryPipeline.Run( new CreateCategoryArgument(arg.Catalog.Id, arg.ImportCategory.CategoryId.ProposeValidId(), arg.ImportCategory.CategoryName, ""), context.CommerceContext.GetPipelineContextOptions()); category = createResult?.Categories?.FirstOrDefault(c => c.Id.Equals(categoryId)); } Condition.Requires(category) .IsNotNull($"{this.Name}: The Category could not be created."); arg.Category = category; return arg; } ``` Notice that we used a couple of standard Helpers/Extensions like CommerceEntity.IdPrefix<>(), which returns the default prefix like we have seen in the database, and ProposeValidId(), which removes any non alphanumeric characters from the (id)string. In the following snippet you can see how easy it is to add a list/enumerable of strings to an XC List: ```csharp List ids; var addArg = new ListEntitiesArgument(ids, "ProductUpdatesList"); await _addListEntitiesPipeline.Run(addArg, context); ``` And in the last snippet what a Minion would look like which reads 10 Id's from the List each time it is run and then uses it to run a pipeline. ```csharp public class UpdateProductsMinion : Minion { private ISynchronizeProductPipeline _synchronizeProductPipeline; private IRemoveListEntitiesPipeline _removeListEntitiesPipeline; public override void Initialize(IServiceProvider serviceProvider, ILogger logger, MinionPolicy policy, CommerceEnvironment environment, CommerceContext globalContext) { base.Initialize(serviceProvider, logger, policy, environment, globalContext); _synchronizeProductPipeline = this.ResolvePipeline(); _removeListEntitiesPipeline = this.ResolvePipeline(); } public override async Task Run() { var listCount = await this.GetListCount(this.Policy.ListToWatch); this.Logger.LogInformation($"{this.Name}-Review List {this.Policy.ListToWatch}: Count:{listCount}"); var list = (await this.GetListIds(this.Policy.ListToWatch, this.Policy.ItemsPerBatch)).IdList; foreach (var id in list) { this.Logger.LogDebug($"{this.Name}-Reviewing Pending Product Update: {id}"); try { await _synchronizeProductPipeline.Run( new SynchronizeProductArgument { ExternalProductId = id }, MinionContext.GetPipelineContextOptions()); this.Logger.LogInformation($"{this.Name}: Product with id {id} has been updated"); await _removeListEntitiesPipeline.Run(new ListEntitiesArgument(new List(){ id }, this.Policy.ListToWatch), MinionContext.GetPipelineContextOptions()); } catch (Exception ex) { this.Logger.LogError(ex, $"{this.Name}: Product with id {id} could not be updated"); } } return new MinionRunResultsModel { ItemsProcessed = this.Policy.ItemsPerBatch, HasMoreItems = listCount > this.Policy.ItemsPerBatch }; } } ```]]> <![CDATA[Sitecore Experience Commerce – Conditional Configuration]]> https://guidovtricht.nl/blog/sitecore-experience-commerce-conditional-configuration https://guidovtricht.nl/blog/sitecore-experience-commerce-conditional-configuration Sat, 15 Sep 2018 00:00:00 GMT ]]>