Change Order Number generator

By default, the orders numbers are generated by an instance of EPiServer.Commerce.Order.IOrderNumberGenerator.

The default number generation is:

 public string GenerateOrderNumber(IOrderGroup orderGroup)
 {
         int num = new Random().Next(100, 999);
         return $"PO{orderGroup.OrderLink.OrderGroupId}{num}";
 }

So, to change this, first we need to inject a custom implementation and remove the default one. On an initialization module that depends on EPiServer.Web.InitializationModule have the following:

(IServiceConfigurationProvider)services.RemoveAll<IOrderNumberGenerator>();
(IServiceConfigurationProvider)services.AddSingleton<IOrderNumberGenerator, MyCustomOrderNumberGenerator>();

And my custom order generator, can be like this:

 public class MyCustomOrderNumberGenerator: IOrderNumberGenerator
    {
        private readonly string prefix;
        private readonly string version;

        public MyCustomOrderNumberGenerator()
        {
            this.prefix = ConfigurationManager.AppSettings["OrderNumberGenerator:prefix"] ?? "PO";
            this.version = ConfigurationManager.AppSettings["OrderNumberGenerator:version"] ?? "00";
        }

        public string GenerateOrderNumber(IOrderGroup orderGroup)
        {
            return $"{prefix}{version}{new Random().Next(100, 999)} 
                                     {orderGroup.OrderLink.OrderGroupId}";
        }
    }

Add migration step with sql script

Scenario: Wanted to add a new schema, table and stored procedure to the same database where CMS is installed. The script should run only once after deployment to an environment.

Using IMigrationSet can do this job, although is different from a Initialization Module that run every time the site starts up.

Interface IMigrationStep documentation: https://world.episerver.com/csclasslibraries/commerce/EPiServer.Commerce.Internal.Migration.Steps.IMigrationStep?version=13

Step 1. Create a folder in your project that is referenced by Episerver CMS website.

Step 2. Create two files: a class and sql script. For exemple:

Step 3. Define the content for you sql script. For good practices, we must use a different schema from the dbo where all the Episerver objects are defined. So, we need to create the following script :

--beginvalidatingquery
IF OBJECT_ID(N'[myschema].[myTable]', N'U') IS NOT NULL
   SELECT 0, 'Already installed'
else
   SELECT 1, 'Adding tables'
--endvalidatingquery

go

create schema [myschema]
go

create table [ownschema].[myTable] 
(
	[Id] [bigint] IDENTITY(1,1) not null,
	[Text] [nvarchar](200) not null,
	constraint [PK_myTable] primary key ([Id])
)
go

Step 4. Mark you script file as Embedded Resource.

Step 5. Define your migration step on your c# class.

using EPiServer.Commerce.Internal.Migration.Steps;
using EPiServer.Data.Providers.Internal;
using EPiServer.Data.SchemaUpdates;
using EPiServer.Logging;
using EPiServer.ServiceLocation;
using Mediachase.Commerce.Shared;
using System;
using System.IO;
using System.Reflection;

namespace Common.Migrations.CreateTablesStoredProcedures
{
    [ServiceConfiguration(typeof(IMigrationStep))]
    public class CreateTablesAndStoredProceduresMigrationStep : IMigrationStep
    {
        private static readonly ILogger Logger = 
            LogManager.GetLogger(typeof(CreateTablesAndStoredProceduresMigrationStep));
        private readonly IDatabaseConnectionResolver _databaseConnectionResolver;
        private readonly ScriptExecutor _scriptExecutor;

        public CreateTablesAndStoredProceduresMigrationStep(
            IDatabaseConnectionResolver databaseConnectionResolver,
            ScriptExecutor scriptExecutor)
        {
            _databaseConnectionResolver = databaseConnectionResolver;
            _scriptExecutor = scriptExecutor;
        }

        public int Order => 2000;

        public string Name => "Create Tables And StoredProcedures";

        public string Description => "Create Tables and Stored Procedures using Embedded Resource sql file ";

        public bool Execute(IProgressMessenger progressMessenger)
        {
            try
            {
                using (Stream installScript = GetInstallScript())
                {
                    _scriptExecutor.ExecuteScript(
                        _databaseConnectionResolver.Resolve().ConnectionString,
                        installScript);
                }

                return true;
            }
            catch (Exception ex)
            {
                Logger.Error("Error occurred while creating the tables and stored procedures.", ex);

                return false;
            }
        }

        private static Stream GetInstallScript()
        {
            const string scriptFqdn = "Common.Migrations.CreateTablesAndStoredProceduresMigrationStep.script.sql";

            Assembly assembly = typeof(CreateTablesAndStoredProceduresMigrationStep).Assembly;

            return assembly.GetManifestResourceStream(scriptFqdn);
        }
    }
}

Step 6. Deploy.

Note that this migration step will run only once if no exception occurred, this is, if the return value of the Execute method is true. If returned false, this migration step will be executed next time the site started.

An workaround on development mode to repeat this migration is always return false, but ensure that this does not go to production.

I got the case, that need to repeat this step, but already returned success. Used a not documented and not official way of removing the success result of this migration by deleting the row from [dbo].[tblBigTable]. I just found it during my tests, just warning you. So, I advise to this only on you local development database.

SELECT pkId, String01 as [Name]
  FROM [dbo].[tblBigTable] where StoreName = 'EPiServer.Commerce.Internal.Migration.MigrationStepInfo'
	and [String01] = 'Common.Migrations.CreateTablesAndStoredProceduresMigrationStep'


delete [dbo].[tblBigTable] where StoreName = 'EPiServer.Commerce.Internal.Migration.MigrationStepInfo'
	and [String01] = 'Common.Migrations.CreateTablesAndStoredProceduresMigrationStep'

Download images from assets from HTML Anchor tag

Suppose the front-end code wants supply a way to download images from the assets folder. Consider the following code:

<a href="http://127.0.0.1:5500/globalassets/images/sample.png" download="sample.png">Download this file</a>

Assuming that 127.0.0.1:5500 is your EPiServer instance, you can say that the image will be downloaded.

Note that download anchor attribute will work if the url is from the same origin of the http request.

For Internet Explorer, the download attribute is not supported, so the image is open on the same tab.

We want to ensure that the response header Content-Disposition: attachment; filename="sample.png"; filename*=UTF-8''sample.png is returned, so each browser saves the image instead of present it on the same tab.

Using the DownloadMediaRouter.DownloadSegment on the request, EPiServer will return the image with the Content-Disposition with attachment value.

So, we can replace the html anchor definition by:

<a href="http://127.0.0.1:5500/globalassets/images/sample.png/download">Download this file</a>

Missing Group on Set Access Rights

Context:

  • Episerver authentication integrated with Azure Active Directory B2C;
  • A fresh installation of Episerver;
  • Virtual roles configured to map Roles configured on AD;
  • Logged only with an account that was mapped for a AD role named ‘SpecialMegaRole’ 😉
  • Another virtual role was mapped for an AD role named ‘NotSoSpecialRole’

Objective : Set the access rights for the ‘NotSoSpecialRole’

Problem: The ‘NotSoSpecialRole’ was not been presented at Set Access Rights page.


Possible Solution: After the first login with an AD user that belong to that “NotSoSpecialRole’, the role became available to set the permitions.

Add Metafields to OrderSummary view

I need to add extra information on each LineItem of the order. But also to see it on the OrderSummary view on the Commerce UI. Please comment if i can implement a better solution than following approach, because i thought that it could be a fast solution by changing the xml file were we define the order ui like PurchaseOrder-ObjectView.xml

First, defined the Meta Fields and associated with the LineItemEx:

When creating the LineItem, don’t forget to had the new attribute the Properties collection:

 ILineItem lineItem = cart.CreateLineItem(entryContent.Code, _orderGroupFactory);
 lineItem.DisplayName = entryContent.DisplayName;
 lineItem.Properties[MetaFieldNames.LineItem.ReasonCode] = inventoryRecord.ReasonCode;

Change the CommerceManager/Apps/Order/Modules/OrderSummary.ascx file in order to define a new code behind and add a new BoundColumn with DataField=”ReasonCode” that will bind the new MetaField

<%@ Control Language="C#" AutoEventWireup="true" CodeBehind="OrderSummary.ascx.cs" Inherits="MyProjectNamespaceSegment.CommerceManager.Apps.Order.Modules.OrderSummary" %>
<div style="padding:10px;">

	...

 <table cellpadding="0" cellspacing="0" width="100%" style="table-layout:fixed;">
    <tr>
        <td colspan="2" style="border: solid 1px #999999;">
            <asp:DataGrid runat="server" ID="MainGrid" .......>
                <HeaderStyle BackColor="#eeeeee" />
                <Columns>
                    <asp:BoundColumn DataField="OrderFormId".....></asp:BoundColumn>

                    <asp:BoundColumn DataField="ReasonCode" HeaderText="Reason Code" ItemStyle-CssClass="ibn-vb2" HeaderStyle-CssClass="ibn-vh2"></asp:BoundColumn>
                </Columns>
            </asp:DataGrid>
        </td>
    </tr>
</table>

Add to the solution a new OrderSummary.ascx.cs file with the same code has the episerver OrderSummary.ascx.cs existing file. Used ILSpy to get the code from Mediachase.Commerce.Manager.Apps.Order.Modules.OrderSummary.

On the private method BindData, change DataTable colums’ definition to include the new MetaField and the rows added to include LineItem.Properties[New MetaField Name] value.

private void BindData()
{
    DataTable dataTable = new DataTable
    {
        Locale = CultureInfo.InvariantCulture
    };
    dataTable.Columns.Add(new DataColumn("OrderFormId", typeof(int)));
    dataTable.Columns.Add(new DataColumn("LineItemId", typeof(int)));
    dataTable.Columns.Add(new DataColumn("Code", typeof(string)));
...
    dataTable.Columns.Add(new DataColumn(MetaFieldNames.LineItem.ReasonCode, typeof(string)));
....

    using (IEnumerator<IOrderForm> enumerator = orderGroup.Forms.Where((IOrderForm x) => x.Name != OrderForm.ReturnName).GetEnumerator())
    {
        if (enumerator.MoveNext())
        {
            IOrderForm current = enumerator.Current;
            foreach (ILineItem allLineItem in current.GetAllLineItems())
            {
                decimal num = orderGroup.Currency.Round(allLineItem.GetEntryDiscountValue());
                decimal num2 = orderGroup.Currency.Round(allLineItem.GetOrderDiscountValue());
                DataRow dataRow = dataTable.NewRow();
                ...
                dataRow[MetaFieldNames.LineItem.ReasonCode] =
                                    allLineItem.Properties[MetaFieldNames.LineItem.ReasonCode];
                ...

Now, i can see the new field on the OrderSummary view:

The view ‘index’ or its master was not found or no view engine supports the searched locations. From IISExpress to IIS

Consider the following scenario: Developing with debug compilation and using IIS Express. Everything works fine without license. Episerver allows development under those circumstances.

But if you deploy to a new IIS site or even to Azure web app and you set compilation without debug, because you probably published with Release configuration:

 <system.web>
    <httpRuntime targetFramework="4.7.2" requestValidationMode="2.0" fcnMode="Single" />
    <compilation targetFramework="4.7.2" optimizeCompilations="true" debug="false"/>

The site will show up with this error:

If you change the compilation for debug

 <system.web>
    <httpRuntime targetFramework="4.7.2" requestValidationMode="2.0" fcnMode="Single" />
    <compilation targetFramework="4.7.2" optimizeCompilations="true" debug="true" />

Now you get the site, but with the episerver warning stating that license does not exists.

Add the license.config file to root of the site or to another folder and update the file EPiServerFramework.config with:

   <licensing licenseFilePath="License.config" />
</episerver.framework>

Refresh the site and is back to work:

Start from empty folder to a running Episerver CMS and ECommerce

Assumptions: this post was written at 23/10/2019 with Visual Studio 2019 and Episerver CMS Visual Studio Extension version 11.5.0.412.

The idea is to create a running empty site with CMS and ECommerce.

Ensure that you have Episerer nuget feed configure to https://nuget.episerver.com/feed/packages.svc/

Lets create a new empty Episerver CMS site:


With this we start to create the CMS web application

Choose empty template:

And the simple site folders and files are created with default values.

Lets update the nuget packages by running the following command at the Package Manager Console:

Update-Package EPiServer.CMS -ToHighestMinor

Do the same for the EPiServer.CMS.UI and EPiServer.CMS.TinyMce packages

Using package manage console and selecting only ToHighestMinor allows Episerver packages to be safely installed because it won’t upgrade to a higher major version that would have breaking changes, and non-Episerver dependencies will be updated, as well as the Episerver ones but it won’t accidently install newer but incompatible packages.

By default, this site is using LocalDb database files for demo purpose. Lets create two database using SQLExpress and SQL Server Management Studio.

Create epidemo.cms and epidemo.commerce database.

Now, it’s time to change the connections string to set the new database. For your new epidemo.cms database we can use the following:

.\SQLExpress;Database=epidemo.cms;Trusted_Connection=True;MultipleActiveResultSets=true

Create a new file on the root of the project and named connectionStrings.config with the following content:

<connectionStrings>
  <clear />
  <add name="EPiServerDB" connectionString="Server=.\SQLExpress;Database=epidemo.cms;Trusted_Connection=True;MultipleActiveResultSets=true" providerName="System.Data.SqlClient" />
</connectionStrings>

Find the connectionStrings section at Web.Config and replace it by:

<connectionStrings configSource="connectionStrings.config" />

You may ask if this is necessary for the post purpose, no its not, but i think is always good to learn something else if you did not know how to split the web.config in multiple files.

Now we have conditions to initialize your database, for now it’s only an empty database:

At Visual Studio Package Manager Console, execute the following command:

initialize-epidatabase

Now, the database was populated with necessary for support Episerver CMS.

After initialize the database, its a good idea to update to the latest changes, by executing the following:

update-epidatabase

In this demo, i don’t want to use Windows Integrated authentication, that is set by default. I would like to use SQL database to store the users.

If you look at the tables that were created at database, there is no support for managing the users. So, let’s change the default provider for membership and roleManager. Find those section at web config and change the defaultProvider to SqlServerMembershipProvider and SqlServerRoleProvider.

With this we use Membership. On another post i will change this to use Identity. Here is a tutorial to use Identity

https://world.episerver.com/documentation/developer-guides/CMS/security/episerver-aspnetidentity/

Since we don’t have yet a user created, we can use an initialization module to create a administrator user with the respective role. So, on Business folder create a new folder named Initialization and a class named RegisterAdminInitializationModule

Add the following code to that class:

using EPiServer.Framework;
using EPiServer.Framework.Initialization;
using System.Configuration;
using System.Web.Security;

namespace EpiserverDemo.CMS.Business.Initialization
{
    [InitializableModule]
    [ModuleDependency(typeof(EPiServer.Web.InitializationModule))]
    public class RegisterAdminInitializationModule : IInitializableModule
    {
        private const string roleName = "Administrators";
        private const string userName = "admin";
        private const string password = "store";
        private const string email = "admin@EpiserverAdventures.com";
        public void Initialize(InitializationEngine context)
        {
            string enabledString =
                ConfigurationManager.AppSettings["EpiserverAdventures:RegisterAdmin"];
            bool enabled;
            if (bool.TryParse(enabledString, out enabled))
            {
                if (enabled)
                {
                    #region Use ASP.NET Membership classes to create the role and user
                    // if the role does not exist, create it
                    if (!Roles.RoleExists(roleName))
                    {
                        Roles.CreateRole(roleName);
                    }
                    // if the user already exists, delete it
                    MembershipUser user = Membership.GetUser(userName);
                    if (user != null)
                    {
                        Membership.DeleteUser(userName);
                    }
                    // create the user with password and add it to role
                    Membership.CreateUser(userName, password, email);
                    Roles.AddUserToRole(userName, roleName);
                    #endregion
                }
            }
        }
        public void Uninitialize(InitializationEngine context) { }
    }
}

Change the Web.Config by adding the following to appSettings:

<add key="EpiserverAdventures:RegisterAdmin" value="true" />

Change the membership provider SqlServerMembershipProvider, to allow a password with minimum 5 letter so we could use the password “store”.

minRequiredPasswordLength="5"

By the way, it is also on this provider that is defined which connection string is used.

Build the project and press Ctrl+F5

To pass by this error, add <clear/> right before the add tag for the profile providers in Web.config

Refresh the page and you will check an 404 error page. Thats good, because since we didn’t yet create the root page, we don’t have a landing page.

At the browser url, add the /episerver after the port number and login with “admin” and “store” as password.


Now it’s time to setup the Ecommerce site.

First we can start to add components to the CMS project. Add the nuget package Episerver.Commerce to the CMS

Add also Episerver.Commerce.UI.ManagerIntegration nuget package to the CMS project.

Review the connection strings that were set on the site:

<connectionStrings>
  <clear />
  <add name="EPiServerDB" connectionString="Server=.\SQLExpress;Database=epidemo.cms;Trusted_Connection=True;MultipleActiveResultSets=true" providerName="System.Data.SqlClient" />
  
<add name="EcfSqlConnection" connectionString="Data Source=(LocalDb)\MSSQLLocalDB;AttachDbFilename=|DataDirectory|EcfSqlConnection_137cc4e0.mdf;Initial Catalog=EcfSqlConnection_137cc4e0;Connection Timeout=60;Integrated Security=True;MultipleActiveResultSets=True" providerName="System.Data.SqlClient" />
</connectionStrings>

Change the EcfSqlConnection connection string to point to the epidemo.commerce database:

<connectionStrings>
  <clear />
  <add name="EPiServerDB" connectionString="Server=.\SQLExpress;Database=epidemo.cms;Trusted_Connection=True;MultipleActiveResultSets=true" providerName="System.Data.SqlClient" />
  <add name="EcfSqlConnection" connectionString="Server=.\SQLExpress;Database=epidemo.commerce;Trusted_Connection=True;MultipleActiveResultSets=true" providerName="System.Data.SqlClient" />
</connectionStrings>

Now its time to create the new backend site for the Ecommerce.

Create a new empty asp.net web application:

Add to the EpiserverDemo.Commerce project the nuget package Episerver.CommerceManager

Dont cancel the copy of files

For some reason, the project file was not updated:

Include all files into the project, except obj and bin folders

Also add the nuget package EPiServer.ServiceLocation.StructureMap to the Commerce Project.

Copy from the CMS project the connectionString.config into the Commerce project and replace on the Web.Config (Commerce project) the connectionString section with:

<connectionStrings configSource="connectionStrings.config" />

Now run on the Package Manage Console the two commands:

Initialize-EPiDatabase
Update-EPiDatabase 

Now the epidemo.cms and epidemo.commerce have content:

Set the CMS site as start up project and press Ctrl+F5

Login into the episerver (if requested) and click on “Execute all pending steps”.

After migration, go to episerver

For CMS:

For ECommerce:

Now set as startup project the Commerce. Press Ctrl+F5.

If you got the following, please add <clear/> to the Commerce web.config:

Run the Commerce site again: use by default “admin” username and “store” as password

Setting icons for content types

When creating new types instances, such as pages or blocks, we get a lot of help from the CMS UI identifying which type we want.

But, to offer better experience to the editors, it would be much nice that these types could be present with an image.

Episerver recomend 120×90 dimention for each icon, but if you give different it will be fit to that.

Episerver has an attribute named ImageUrlAttribute . Create a derived type that has a default constructor that sets the path to a default image file:

public class StartPageImageUrlAttribute : ImageUrlAttribute
{
  public StartPageImageUrlAttribute() : base("/static/contenticons/StartPage.png")
  {  }

  public StartPageImageUrlAttribute(string path) : base(path)
  {  }
}

Apply this attribute to your content type classes to show a default icon:

[ContentType(DisplayName = "Start Page", GUID = "BD710683-6F9D-4DA5-A2DC-5CFEF74723A8", Description = "Site start page", GroupName = PagesGroups.Content)]
    [StartPageImageUrl]
    public class StartPage : StandardPage
    {
    }

Or you can add the attribute ImageUrl to the class:

[ContentType(DisplayName = "Standard Page", GUID = "1D1A9C91-3F47-4ACE-90E2-06CBDFABC81A", Description = "Non specific page. Base page for all page content.", GroupName = PagesGroups.Content)]
    [ImageUrl("/static/contenticons/StandardPage.png")]
    public class StandardPage : BasePageData
    {
        [CultureSpecific]
        [Display(Name = "Blocks", GroupName = SystemTabNames.Content, Order = 1000)]
        public virtual ContentArea Blocks { get; set; }
    }

After add the images to the site, you can have the fancy icons on each type: