Plugin with data access

In this tutorial I'll be using the nopCommerce plugin architecture to implement a product view tracker. Before we begin with the development it is very important that you have read, understood, and successfully completed the tutorials listed below. I'll be skipping over some explanations covered in the previous articles, but you can recap using the links provided.

Frequently asked development questions
Updating an existing entity. How to add a new property.
How to write a nopCommerce plugin

We will start coding with the data access layer, move on to the service layer, and finally end on dependency injection.

Note: The practical application of this plugin is questionable, but I couldn't think of a feature that didn't come with nopCommerce and would fit in a reasonable size post. If you use this plugin in a production environment I offer no warranties. I am always interested in success stories and I would be happy to hear that the post provided more than just an educational value.

Getting started

Create a new class library project "Nop.Plugin.Other.ProductViewTracker".

Plugin with data access

Add the following folders and decription.txt file.

Plugin with data access

You can view the description.txt file in the image below.

Plugin with data access

Also add the following references and set their "Copy Local" property to "False":

  • Nop.Core.dll
  • Nop.Data.dll
  • Nop.Services.dll
  • Nop.Web.Framework.dll
  • EntityFramework.dll
  • System.Data.Entity.dll
  • System.Web.dll
  • System.Web.Mvc.dll
  • Autofac.dll
  • Autofac.Configuration.dll
  • Autofac.Integration.Mvc.dll

The Data Access Layer (A.K.A. Creating new entities in nopCommerce)

Inside of the "domain" namespace we're going to create a public class named TrackingRecord. This class extends BaseEntity, but it is otherwise a very boring file. Something to remember is that all properties are marked as virtual and it isn't just for fun. Virtual properties are required on database entities because of how Entity Framework instantiates and tracks classes. One other thing to note is that we do not have navigation properties (relational properties), and I'll cover those in more detail later.

namespace Nop.Plugin.Other.ProductViewTracker.Domain
{
    public class TrackingRecord : BaseEntity
    {
        public virtual int ProductId { get; set; }
        public virtual string ProductName { get; set; }
        public virtual int CustomerId { get; set; }
        public virtual string IpAddress { get; set; }
        public virtual bool IsRegistered { get; set; }
    }
}

The next class to create is the Entity Framework mapping class. Inside of the mapping class we map the columns, table relationships, and the database table.


namespace Nop.Plugin.Other.ProductViewTracker.Data
{
    public class TrackingRecordMap : EntityTypeConfiguration<TrackingRecord>
    {
        public TrackingRecordMap()
        {
            ToTable("ProductViewTracking");

            //Map the primary key
            HasKey(m => m.Id);
            //Map the additional properties
            Property(m => m.ProductId);
            //Avoiding truncation/failure 
            //so we set the same max length used in the product tame
            Property(m => m.ProductName).HasMaxLength(400);
            Property(m => m.IpAddress);
            Property(m => m.CustomerId);
            Property(m => m.IsRegistered);
        }
    }
}

The next class is the most complicated and the most important class in the data access layer. The Entity Framework Object Context is a pass-through class that gives us database access and helps track entity state (e.g. add, update, delete). The context is also used to generate the database schema or update an existing schema. In custom context classes we cannot reference previously existing entities because those types are already associated to another object context. That is also why we do not have complex navigation properties in our tracking record.



namespace Nop.Plugin.Other.ProductViewTracker.Data
{
    public class TrackingRecordObjectContext : DbContext, IDbContext
    {
        public TrackingRecordObjectContext(string nameOrConnectionString) : base(nameOrConnectionString) { }

        #region Implementation of IDbContext

        #endregion

        protected override void OnModelCreating(DbModelBuilder modelBuilder)
        {
            modelBuilder.Configurations.Add(new TrackingRecordMap());

            base.OnModelCreating(modelBuilder);
        }

        public string CreateDatabaseInstallationScript()
        {
            return ((IObjectContextAdapter)this).ObjectContext.CreateDatabaseScript();
        }

        public void Install()
        {
            //It's required to set initializer to null (for SQL Server Compact).
            //otherwise, you'll get something like "The model backing the 'your context name' context has changed since the database was created. Consider using Code First Migrations to update the database"
            Database.SetInitializer<TrackingRecordObjectContext>(null);

            Database.ExecuteSqlCommand(CreateDatabaseInstallationScript());
            SaveChanges();
        }

        public void Uninstall()
        {
            var dbScript = "DROP TABLE ProductViewTracking";
            Database.ExecuteSqlCommand(dbScript);
            SaveChanges();
        }
        
        public new IDbSet<TEntity> Set<TEntity>() where TEntity : BaseEntity
        {
            return base.Set<TEntity>();
        }

        public System.Collections.Generic.IList<TEntity> ExecuteStoredProcedureList<TEntity>(string commandText, params object[] parameters) where TEntity : BaseEntity, new()
        {
            throw new System.NotImplementedException();
        }
        
        public System.Collections.Generic.IEnumerable<TElement> SqlQuery<TElement>(string sql, params object[] parameters)
        {
            throw new System.NotImplementedException();
        }

        public int ExecuteSqlCommand(string sql, bool doNotEnsureTransaction = false, int? timeout = null, params object[] parameters)
        {
            throw new System.NotImplementedException();
        }
    }
}

File Locations: To figure out where certain files should exist analyze the namespace and create the file accordingly.

Service layer

The service layer connects the data access layer and the presentation layer. Since it is bad form to share any type of responsibility in code each layer needs to be isolated. The service layer wraps the data layer with business logic and the presentation layer depends on the service layer. Because our task is very small our service layer does nothing but communicate with the repository (the repository in nopCommerce acts as a facade to the object context).


namespace Nop.Plugin.Other.ProductViewTracker.Services
{
    public interface IViewTrackingService
    {
        /// <summary>
        /// Logs the specified record.
        /// </summary>
        /// <param name="record">The record.</param>
        void Log(TrackingRecord record);
    }
}

namespace Nop.Plugin.Other.ProductViewTracker.Services
{
    public class ViewTrackingService : IViewTrackingService
    {
        private readonly IRepository<TrackingRecord> _trackingRecordRepository;

        public ViewTrackingService(IRepository<TrackingRecord> trackingRecordRepository)
        {
            _trackingRecordRepository = trackingRecordRepository;
        }

        /// <summary>
        /// Logs the specified record.
        /// </summary>
        /// <param name="record">The record.</param>
        public void Log(TrackingRecord record)
        {
            _trackingRecordRepository.Insert(record);
        }
    }
}

Dependency Injection

Martin Fowler has written a great description of dependency injection or Inversion of Control. I'm not going to duplicate his work, and you can find his article here. Dependency injection manages the lifecycle of objects and provides instances for dependent objects to use. First we need to configure the dependency container so it understands which objects it will control and what rules might apply to the creation of those objects.



namespace Nop.Plugin.Other.ProductViewTracker
{
    public class ProductViewTrackerDependencyRegistrar : IDependencyRegistrar
    {
        private const string CONTEXT_NAME = "nop_object_context_product_view_tracker";

        public void Register(ContainerBuilder builder, ITypeFinder typeFinder)
        {
            //Load custom data settings
            var dataSettingsManager = new DataSettingsManager();
            DataSettings dataSettings = dataSettingsManager.LoadSettings();

            //Register custom object context
            builder.Register<IDbContext>(c => RegisterIDbContext(c, dataSettings)).Named<IDbContext>(CONTEXT_NAME).InstancePerHttpRequest();
            builder.Register(c => RegisterIDbContext(c, dataSettings)).InstancePerHttpRequest();

            //Register services
            builder.RegisterType<ViewTrackingService>().As<IViewTrackingService>();

            //Override the repository injection
            builder.RegisterType<EfRepository<TrackingRecord>>().As<IRepository<TrackingRecord>>().WithParameter(ResolvedParameter.ForNamed<IDbContext>(CONTEXT_NAME)).InstancePerHttpRequest();
        }

        public int Order
        {
            get { return 0; }
        }

        /// <summary>
        /// Registers the I db context.
        /// </summary>
        /// <param name="componentContext">The component context.</param>
        /// <param name="dataSettings">The data settings.</param>
        /// <returns></returns>
        private TrackingRecordObjectContext RegisterIDbContext(IComponentContext componentContext, DataSettings dataSettings)
        {
            string dataConnectionStrings;

            if (dataSettings != null && dataSettings.IsValid())
            {
                dataConnectionStrings = dataSettings.DataConnectionString;
            }
            else
            {
                dataConnectionStrings = componentContext.Resolve<DataSettings>().DataConnectionString;
            }

            return new TrackingRecordObjectContext(dataConnectionStrings);
        }
    }
}

In the code above we register different types of objects so they can later be injected into controllers, services, and repositories. Now that we've covered the new topics I'll bring back some of the older ones so we can finish the plugin.

The controller


namespace Nop.Plugin.Other.ProductViewTracker.Controllers
{
    public class TrackingController : Controller
    {
        private readonly IProductService _productService;
        private readonly IViewTrackingService _viewTrackingService;
        private readonly IWorkContext _workContext;

        public TrackingController(IWorkContext workContext, 
            IViewTrackingService viewTrackingService, 
            IProductService productService,
            IPluginFinder pluginFinder)
        {
            _workContext = workContext;
            _viewTrackingService = viewTrackingService;
            _productService = productService;
        }

        [ChildActionOnly]
        public ActionResult Index(int productId)
        {
            //Read from the product service
            Product productById = _productService.GetProductById(productId);

            //If the product exists we will log it
            if (productById != null)
            {
                //Setup the product to save
                var record = new TrackingRecord();
                record.ProductId = productId;
                record.ProductName = productById.Name;
                record.CustomerId = _workContext.CurrentCustomer.Id;
                record.IpAddress = _workContext.CurrentCustomer.LastIpAddress;
                record.IsRegistered = _workContext.CurrentCustomer.IsRegistered();

                //Map the values we're interested in to our new entity
                _viewTrackingService.Log(record);
            }

            //Return the view, it doesn't need a model
            return Content("");
        }
    }
}

The route provider


namespace Nop.Plugin.Other.ProductViewTracker
{
    public class ProductViewTrackerRouteProvider : IRouteProvider
    {
        public void RegisterRoutes(RouteCollection routes)
        {
            routes.MapRoute("Nop.Plugin.Other.ProductViewTracker.Log", "tracking/productviews/{productId}", new { controller = "Tracking", action = "Index" }, new[] { "Nop.Plugin.Other.ProductViewTracker.Controllers" });
        }
        public int Priority
        {
            get { return 0; }
        }
    }
}

The plugin installer


namespace Nop.Plugin.Other.ProductViewTracker
{
    public class ProductViewTrackerPlugin : BasePlugin
    {
        private readonly TrackingRecordObjectContext _context;

        public ProductViewTrackerPlugin(TrackingRecordObjectContext context)
        {
            _context = context;
        }

        public override void Install()
        {
            _context.Install();
            base.Install();
        }

        public override void Uninstall()
        {
            _context.Uninstall();
            base.Uninstall();
        }
    }
}

The usage

The tracking code should be added to ProductTemplate.SingleVariant.cshtml and ProductTemplate.VariantsInGrid.cshtml files. These ones are product templates. @Html.Action("Index", "Tracking", new { productId = Model.Id })

P.S. You can also implement it as a widget. In this case you won't need to edit a cshtml file.

Download

You can download the completed project here (built for nopCommerce 3.20).

Author: Skyler Severns

nopCommerce on facebook