Tools: Design Patterns Explained in C# & nopCommerce

Tools: Design Patterns Explained in C# & nopCommerce

Source: Dev.to

Types of Design Patterns ## Nop.Core ## Singleton Pattern ## Observer Pattern ## Nop.Data ## Repository Pattern ## Nop.Services ## Dependency Injection Pattern When we work on large applications, we often face similar problems again and again. Design patterns help us solve these recurring problems using proven and reusable approaches. A design pattern is not a piece of code that you copy and paste. Instead, it is a general solution or idea that you can adapt to your own project based on its needs. You follow the concept of the pattern and implement it in a way that fits your application. There are mainly three types of design patterns: In real-world applications, these patterns are not used in isolation. They are often combined to build scalable and maintainable systems. A good real-world example of this is nopCommerce. nopCommerce uses many design patterns across different layers of its architecture. In this article, I will explain common C# design patterns by showing how they are actually used inside nopCommerce. nopCommerce is divided into three main layers: I’ll explain two design patterns from each layer. Nop.Core contains the core abstractions and infrastructure of nopCommerce. Design patterns used: The Singleton pattern is a creational design pattern that ensures a class has only one instance and provides a global access point to that instance. Singleton in nopCommerce In nopCommerce, there is a custom Singleton class. This implementation is slightly different from the classic GoF Singleton. It works more like a central registry for shared instances during the application’s lifetime. Core Singleton Implementation Singleton stores a single instance of type T The instance is stored in a static field All singleton instances are tracked using a shared dictionary This ensures one shared instance per type for the entire app domain This approach gives nopCommerce a consistent and controlled way to manage shared objects across the system. The Observer pattern is a behavioral design pattern that allows objects to subscribe to events and get notified automatically when something changes. Observer in nopCommerce nopCommerce uses the Observer pattern heavily through its event system. This is especially useful when entities are: Instead of tightly coupling logic everywhere, nopCommerce publishes events and lets consumers react to them. This keeps the code modular, decoupled, and maintainable. Core Components of the Observer Pattern in nopCommerce Events – represent something that happened Event Publisher – publishes events Event Consumers – handle the events Example: Entity Deleted Event This event carries the deleted entity so consumers know exactly what was deleted. nopCommerce also provides: The publisher sends events to all subscribed consumers, either synchronously or asynchronously. Nop.Data is responsible for data access in nopCommerce. Design patterns used: The Builder pattern is a creational pattern that helps construct complex objects step by step. It allows different representations of an object using the same construction process. Builder in nopCommerce In nopCommerce, the Builder pattern is commonly used with FluentMigrator to define database schemas in a readable and structured way. Example: BlogPostBuilder Why this is Builder Pattern The object (table schema) is built step by step Fluent syntax improves readability The construction logic is separated from usage This is a good example of Builder + Fluent Interface working together. The Repository pattern abstracts data access logic and provides a clean API for the business layer. In nopCommerce, repositories: Hide database details Promote separation of concerns Repositories are commonly used with: Example: Repository Used Inside a Service Repository handles data access Service handles business logic Nop.Services contains the business logic layer. Design patterns used: The Service pattern organizes business logic and acts as a bridge between: This makes the application: Service Interface and Implementation The service defines what the system can do, while repositories handle how data is accessed. Dependency Injection (DI) is used everywhere in nopCommerce. Instead of creating dependencies manually, they are injected through constructors. nopCommerce uses DI in: Disadvantages of Design Patterns While design patterns are powerful, they are not always the best solution. Complexity – can make code harder to understand Learning curve – requires solid understanding Overengineering – patterns used where simple code is enough Rigidity – may reduce flexibility Increased development time – more planning and structure Misapplication – wrong pattern causes bad architecture Dependency complexity – harder to refactor Documentation overhead – patterns must be well documented Design patterns should be used as tools, not rules. nopCommerce is a great real-world example of how C# design patterns are used in production systems. help keep the codebase scalable, testable, and maintainable. The key lesson is not to use patterns everywhere, but to use them when they actually solve a problem. Thanks for reading! You can find me on GitHub and LinkedIn Templates let you quickly answer FAQs or store snippets for re-use. Are you sure you want to hide this comment? It will become hidden in your post, but will still be visible via the comment's permalink. Hide child comments as well For further actions, you may consider blocking this person and/or reporting abuse COMMAND_BLOCK: namespace Nop.Core.Infrastructure { /// <summary> /// A statically compiled "singleton" used to store objects /// throughout the lifetime of the app domain. /// </summary> /// <typeparam name="T">The type of object to store.</typeparam> public partial class Singleton<T> : BaseSingleton { private static T instance; /// <summary> /// The singleton instance for the specified type T. /// </summary> public static T Instance { get => instance; set { instance = value; AllSingletons[typeof(T)] = value; } } } } Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK: namespace Nop.Core.Infrastructure { /// <summary> /// A statically compiled "singleton" used to store objects /// throughout the lifetime of the app domain. /// </summary> /// <typeparam name="T">The type of object to store.</typeparam> public partial class Singleton<T> : BaseSingleton { private static T instance; /// <summary> /// The singleton instance for the specified type T. /// </summary> public static T Instance { get => instance; set { instance = value; AllSingletons[typeof(T)] = value; } } } } COMMAND_BLOCK: namespace Nop.Core.Infrastructure { /// <summary> /// A statically compiled "singleton" used to store objects /// throughout the lifetime of the app domain. /// </summary> /// <typeparam name="T">The type of object to store.</typeparam> public partial class Singleton<T> : BaseSingleton { private static T instance; /// <summary> /// The singleton instance for the specified type T. /// </summary> public static T Instance { get => instance; set { instance = value; AllSingletons[typeof(T)] = value; } } } } COMMAND_BLOCK: namespace Nop.Core.Events { public partial class EntityDeletedEvent<T> where T : BaseEntity { public EntityDeletedEvent(T entity) { Entity = entity; } public T Entity { get; } } } Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK: namespace Nop.Core.Events { public partial class EntityDeletedEvent<T> where T : BaseEntity { public EntityDeletedEvent(T entity) { Entity = entity; } public T Entity { get; } } } COMMAND_BLOCK: namespace Nop.Core.Events { public partial class EntityDeletedEvent<T> where T : BaseEntity { public EntityDeletedEvent(T entity) { Entity = entity; } public T Entity { get; } } } CODE_BLOCK: EntityInsertedEvent<T> EntityUpdatedEvent<T> Event Publisher Interface namespace Nop.Core.Events { public partial interface IEventPublisher { Task PublishAsync<TEvent>(TEvent @event); void Publish<TEvent>(TEvent @event); } } Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: EntityInsertedEvent<T> EntityUpdatedEvent<T> Event Publisher Interface namespace Nop.Core.Events { public partial interface IEventPublisher { Task PublishAsync<TEvent>(TEvent @event); void Publish<TEvent>(TEvent @event); } } CODE_BLOCK: EntityInsertedEvent<T> EntityUpdatedEvent<T> Event Publisher Interface namespace Nop.Core.Events { public partial interface IEventPublisher { Task PublishAsync<TEvent>(TEvent @event); void Publish<TEvent>(TEvent @event); } } CODE_BLOCK: using FluentMigrator.Builders.Create.Table; using Nop.Core.Domain.Blogs; using Nop.Core.Domain.Localization; using Nop.Data.Extensions; namespace Nop.Data.Mapping.Builders.Blogs { /// <summary> /// Represents a blog post entity builder /// </summary> public partial class BlogPostBuilder : NopEntityBuilder<BlogPost> { public override void MapEntity(CreateTableExpressionBuilder table) { table .WithColumn(nameof(BlogPost.Title)).AsString(int.MaxValue).NotNullable() .WithColumn(nameof(BlogPost.Body)).AsString(int.MaxValue).NotNullable() .WithColumn(nameof(BlogPost.MetaKeywords)).AsString(400).Nullable() .WithColumn(nameof(BlogPost.MetaTitle)).AsString(400).Nullable() .WithColumn(nameof(BlogPost.LanguageId)).AsInt32().ForeignKey<Language>(); } } } Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: using FluentMigrator.Builders.Create.Table; using Nop.Core.Domain.Blogs; using Nop.Core.Domain.Localization; using Nop.Data.Extensions; namespace Nop.Data.Mapping.Builders.Blogs { /// <summary> /// Represents a blog post entity builder /// </summary> public partial class BlogPostBuilder : NopEntityBuilder<BlogPost> { public override void MapEntity(CreateTableExpressionBuilder table) { table .WithColumn(nameof(BlogPost.Title)).AsString(int.MaxValue).NotNullable() .WithColumn(nameof(BlogPost.Body)).AsString(int.MaxValue).NotNullable() .WithColumn(nameof(BlogPost.MetaKeywords)).AsString(400).Nullable() .WithColumn(nameof(BlogPost.MetaTitle)).AsString(400).Nullable() .WithColumn(nameof(BlogPost.LanguageId)).AsInt32().ForeignKey<Language>(); } } } CODE_BLOCK: using FluentMigrator.Builders.Create.Table; using Nop.Core.Domain.Blogs; using Nop.Core.Domain.Localization; using Nop.Data.Extensions; namespace Nop.Data.Mapping.Builders.Blogs { /// <summary> /// Represents a blog post entity builder /// </summary> public partial class BlogPostBuilder : NopEntityBuilder<BlogPost> { public override void MapEntity(CreateTableExpressionBuilder table) { table .WithColumn(nameof(BlogPost.Title)).AsString(int.MaxValue).NotNullable() .WithColumn(nameof(BlogPost.Body)).AsString(int.MaxValue).NotNullable() .WithColumn(nameof(BlogPost.MetaKeywords)).AsString(400).Nullable() .WithColumn(nameof(BlogPost.MetaTitle)).AsString(400).Nullable() .WithColumn(nameof(BlogPost.LanguageId)).AsInt32().ForeignKey<Language>(); } } } COMMAND_BLOCK: public partial class BlogService : IBlogService { private readonly IRepository<BlogComment> _blogCommentRepository; private readonly IRepository<BlogPost> _blogPostRepository; public BlogService( IRepository<BlogComment> blogCommentRepository, IRepository<BlogPost> blogPostRepository) { _blogCommentRepository = blogCommentRepository; _blogPostRepository = blogPostRepository; } public virtual async Task DeleteBlogPostAsync(BlogPost blogPost) { await _blogPostRepository.DeleteAsync(blogPost); } } Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK: public partial class BlogService : IBlogService { private readonly IRepository<BlogComment> _blogCommentRepository; private readonly IRepository<BlogPost> _blogPostRepository; public BlogService( IRepository<BlogComment> blogCommentRepository, IRepository<BlogPost> blogPostRepository) { _blogCommentRepository = blogCommentRepository; _blogPostRepository = blogPostRepository; } public virtual async Task DeleteBlogPostAsync(BlogPost blogPost) { await _blogPostRepository.DeleteAsync(blogPost); } } COMMAND_BLOCK: public partial class BlogService : IBlogService { private readonly IRepository<BlogComment> _blogCommentRepository; private readonly IRepository<BlogPost> _blogPostRepository; public BlogService( IRepository<BlogComment> blogCommentRepository, IRepository<BlogPost> blogPostRepository) { _blogCommentRepository = blogCommentRepository; _blogPostRepository = blogPostRepository; } public virtual async Task DeleteBlogPostAsync(BlogPost blogPost) { await _blogPostRepository.DeleteAsync(blogPost); } } COMMAND_BLOCK: public partial interface IBlogService { Task DeleteBlogPostAsync(BlogPost blogPost); Task<BlogPost> GetBlogPostByIdAsync(int blogPostId); } public partial class BlogService : IBlogService { private readonly IRepository<BlogPost> _blogPostRepository; public BlogService(IRepository<BlogPost> blogPostRepository) { _blogPostRepository = blogPostRepository; } public async Task DeleteBlogPostAsync(BlogPost blogPost) { await _blogPostRepository.DeleteAsync(blogPost); } public async Task<BlogPost> GetBlogPostByIdAsync(int blogPostId) { return await _blogPostRepository.GetByIdAsync(blogPostId, cache => default); } } Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK: public partial interface IBlogService { Task DeleteBlogPostAsync(BlogPost blogPost); Task<BlogPost> GetBlogPostByIdAsync(int blogPostId); } public partial class BlogService : IBlogService { private readonly IRepository<BlogPost> _blogPostRepository; public BlogService(IRepository<BlogPost> blogPostRepository) { _blogPostRepository = blogPostRepository; } public async Task DeleteBlogPostAsync(BlogPost blogPost) { await _blogPostRepository.DeleteAsync(blogPost); } public async Task<BlogPost> GetBlogPostByIdAsync(int blogPostId) { return await _blogPostRepository.GetByIdAsync(blogPostId, cache => default); } } COMMAND_BLOCK: public partial interface IBlogService { Task DeleteBlogPostAsync(BlogPost blogPost); Task<BlogPost> GetBlogPostByIdAsync(int blogPostId); } public partial class BlogService : IBlogService { private readonly IRepository<BlogPost> _blogPostRepository; public BlogService(IRepository<BlogPost> blogPostRepository) { _blogPostRepository = blogPostRepository; } public async Task DeleteBlogPostAsync(BlogPost blogPost) { await _blogPostRepository.DeleteAsync(blogPost); } public async Task<BlogPost> GetBlogPostByIdAsync(int blogPostId) { return await _blogPostRepository.GetByIdAsync(blogPostId, cache => default); } } COMMAND_BLOCK: public partial class BlogService : IBlogService { private readonly IRepository<BlogPost> _blogPostRepository; public BlogService(IRepository<BlogPost> blogPostRepository) { _blogPostRepository = blogPostRepository; } } Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK: public partial class BlogService : IBlogService { private readonly IRepository<BlogPost> _blogPostRepository; public BlogService(IRepository<BlogPost> blogPostRepository) { _blogPostRepository = blogPostRepository; } } COMMAND_BLOCK: public partial class BlogService : IBlogService { private readonly IRepository<BlogPost> _blogPostRepository; public BlogService(IRepository<BlogPost> blogPostRepository) { _blogPostRepository = blogPostRepository; } } - Creational patterns – focus on object creation - Structural patterns – focus on class and object composition - Behavioral patterns – focus on communication between objects - Nop.Services - Singleton Pattern - Observer Pattern - Builder Pattern - Repository Pattern - Builder Pattern - Dependency Injection - Unit of Work - Service Layer - Service Pattern - Dependency Injection Pattern - Service Pattern - Loose coupling - Easier unit testing - Better maintainability - Cleaner architecture - Controllers - Repositories - Dependency Injection