Skip to content

Top Nop Tips for nopCommerce Development

nopCommerce is a smartly-built and well-maintained open source e-commerce platform for ASP .NET Core.  While offering plenty of capability out-of-the-box, nop is built to provide great resources to add or enhance functionality through the use of “plug-ins.”   In this article I’ll cover my top 8 tips and tricks for efficient, re-usable, plug-in development.

1. Do not reinvent the wheel

To minimize development time, be sure to check out the nopCommerce marketplace and see what themes / extensions are already available.   There’s a healthy variety, and many come with a trial period to allow you to verify your needs are met.  By making use of existing plug-ins, we can make better use of our development time.

2. Do not alter / remove source code

To avoid complications with other extensions and future nopCommerce upgrades, we strongly recommend customization be at the theme / plugin level. For proprietary customizations, if necessary, be sure to use partial classes in separate, easy-to-identify directories.  It’s a good idea to recreate the directory tree in a single subdirectory, this allows an easier approach when upgrading nopCommerce in the future.

In the rare case that you cannot accomplish your need without modifying source code, be sure to submit your change to be included in future versions of nopCommerce.

Augmented entities in Nop.Core project tree

3. Utilize Event Consumers

Where possible, instead of overriding something in nop.services, use an IConsumer as a less invasive method for your customization.  Apart from EntityInserted/Updated/Deleted events, others like OrderPlacedEvent and CustomerLoggedInEvent I find common use for.  To see what’s available, check out usages of IEventPublisher in nopCommerce’s source.

Example consuming OrderPlacedEvent

public class OrderPlacedEventConsumer : IConsumer
{
      public OrderPlacedEventConsumer ( )
      {
      }
      public void HandleEvent (OrderPlacedEvent eventMessage )
      {
          var order = eventMessage . Order ;
          //your extension logic here
      }
}

By utilizing IConsumers we are able to add our functionality without risk of breaking other installed plugins.

4. Utilize Custom Properties Dictionary

Every model that inherits BaseNopModel contains a Dictionary<string, object> property CustomProperties that we can use in situations where you’re unable to extend the model itself.  Here’s a quick example of adding an additional property to each of a list of order models:

public override OrderListModel PrepareOrderListModel (OrderSearchModel searchModel )
{
      var result = base . PrepareOrderListModel (searchModel ) ;
      foreach ( var item in result . Data )
      {
            var additionalData = GetAdditionalData (item . Id ) ;
           item . CustomProperties [ "MyCustomProperty1" ] = additionalData ;
      }
      return result ;
}

Getting these values in a view is fairly verbose, so I like to use an extension method to simplify. Here’s a quick-and-dirty example:

public static T GetCustomProperty ( this BaseNopModel model, string key )
  {
        if (model . CustomProperties . ContainsKey (key ) )
        {
            try
            {
                var retVal = model . CustomProperties [key ] ;
                return (T )retVal ;
            }
            catch { ; }
        }
        return default (T ) ;
}

CustomProperties can also be used in a kendo grid:

columns : [ {
         field : "CustomProperties.MyCustomProperty",
         title : "My Custom Property"
  } ]

5. Utilize AttributesXml

Generic attributes are a quick way to associate non-searchable information to any BaseEntity, but require additional data calls. To remove this performance hit, store information in AttributesXml when possible. The following example can be used to create extension methods on Order / OrderItem / ShoppingCartItem entities for getting & setting custom data. This new xml is invisible to ProductAttributeParser and ProductAttributeFormatter and handles type conversion like a GenericAttribute:

    public static string SetCustomAttribute ( string attributesXml, string key, T value )
        {
            try
            {
                var xmlDoc = new XmlDocument ( ) ;
                if ( String . IsNullOrEmpty (attributesXml ) )
                {
                    var element1 = xmlDoc . CreateElement ( "Attributes" ) ;
                    xmlDoc . AppendChild (element1 ) ;
                }
                else
                {
                    xmlDoc . LoadXml (attributesXml ) ;
                }
                var rootElement = (XmlElement )xmlDoc . SelectSingleNode ( @"//Attributes" ) ;

                //find existing
                var node = xmlDoc . SelectSingleNode ( @"//Attributes/" + key ) ;

                //create new one if not found
                if (node == null )
                {
                    node = xmlDoc . CreateElement (key ) ;
                    rootElement . AppendChild (node ) ;
                }

                node . InnerText = CommonHelper . To ( value ) ;
                return xmlDoc . OuterXml ;

            }
            catch (Exception exc )
            {
                return null ;
            }
        }


        public static T GetCustomAttribute ( string attributesXml, string key )
        {
            try
            {
                var xmlDoc = new XmlDocument ( ) ;
                if ( String . IsNullOrEmpty (attributesXml ) )
                {
                    var element1 = xmlDoc . CreateElement ( "Attributes" ) ;
                    xmlDoc . AppendChild (element1 ) ;
                }
                else
                {
                    xmlDoc . LoadXml (attributesXml ) ;
                }
                //find existing
                var node = xmlDoc . SelectSingleNode ( @"//Attributes/" + key ) ;

                //create new one if not found
                if (node != null )
                {

                    var retVal = CommonHelper . To (node . InnerText ) ;
                    return retVal ;
                }
            }
            catch { ; }

            return default (T ) ;
        }

6. Cache Management

With performance in mind, consider implementing Entity caching with ICacheManager (per request) and model caching with IStaticCacheManager where it makes sense. For implementation, it’s a good rule-of-thumb that your first resource for best-practices be nop source code. nopCommerce’s caching features make it easy for developers to ensure their plugin’s performance.

7. Adding an admin ajax tab (nop 3.8+)

To display custom Order / Customer / Product data in the admin interface, it usually makes sense to create a separate tab as part of its tab-strip. To accomplish this, you’d implement IConsumer<AdminTabStripCreated> and append script conditionally based on the tab-strip name. To benefit page load-times it’s a good idea to get its content asynchronously on tab click – for this I’ve written a handy extension method:

public static void InjectTab ( this AdminTabStripCreated eventMessage, string tabName, string tabTitle, string contentUrl )
        {
            var htmlString = ""
                  + "$(document).ready(function() {"
                      + $ "$(" <li ><a data -tab -name = "{tabName}" data -toggle = "tab" href = "#{tabName}" id = "_tab{tabName}" > "
                             + tabTitle
                             + $"
</a ></li > ").appendTo('#{eventMessage.TabStripName} .nav-tabs:first');"
                              + $ "$(" <div class = "tab-pane" id = "{tabName}" ><div style = "padding:30px;height:200px" > "
                             + $"
<h4 >One moment ...</h4 ></div ></div > ").appendTo('#{eventMessage.TabStripName} .tab-content:first');"
                              + $ "$('#_tab{tabName}').click(function(){{"
                              + $ "if($('#{tabName}').data('tab-loaded')){{ return; }}"
                              + $ "$.get('{contentUrl}', function(result) {{"
                              + $ "$('#{tabName}').html(result).data('tab-loaded',true);"
                              + "});"
                    + "});"
                + "});"
            + "" ;

            eventMessage . BlocksToRender . Add ( new HtmlString (htmlString ) ) ;
        }

Custom Invoice / Payments Tab

8. Automatically copy views to output directory

One of the bottlenecks of plugin development speed is the need to re-build your project after every view / css / script file update. If you’re impatient like me, you’ll want a way around this – so I wrote a console app that monitors specified plugin directories and automatically pushes updates to the output directory upon save, which I’ve created a git repository for – though at the time of writing this was informed that one of our interns had taken it a step further and created a VS extension which deserves a look: https://github.com/alexHayes08/NopyCopy

Hopefully you’ve found some of this helpful or insightful! As an open-source platform that stays on the cutting-edge of .NET, nopCommerce is rich in value, and we’re fond of watching it continue to grow and flourish.