<?xml version="1.0" encoding="utf-8"?>
<rss xmlns:a10="http://www.w3.org/2005/Atom" version="2.0">
  <channel xmlns:media="http://search.yahoo.com/mrss/" xmlns:content="http://purl.org/rss/1.0/modules/content/">
    <title>ABP.IO Stories</title>
    <link>https://abp.io/community/articles</link>
    <description>A hub for ABP Framework, .NET, and software development. Access articles, tutorials, news, and contribute to the ABP community.</description>
    <lastBuildDate>Sun, 11 Oct 2026 00:16:29 Z</lastBuildDate>
    <generator>Community - ABP.IO</generator>
    <image>
      <url>https://abp.io/assets/favicon.ico/favicon-32x32.png</url>
      <title>ABP.IO Stories</title>
      <link>https://abp.io/community/articles</link>
    </image>
    <a10:link rel="self" type="application/rss+xml" title="self" href="https://abp.io/community/rss?member=qais.alkhateeb%40devnas-jo.com" />
    <item>
      <guid isPermaLink="true">https://abp.io/community/posts/unlocking-modularity-in-abp.io-a-closer-look-at-the-contributor-pattern-ixf6wgbw</guid>
      <link>https://abp.io/community/posts/unlocking-modularity-in-abp.io-a-closer-look-at-the-contributor-pattern-ixf6wgbw</link>
      <a10:author>
        <a10:name>qais.alkhateeb@devnas-jo.com</a10:name>
        <a10:uri>https://abp.io/community/members/qais.alkhateeb@devnas-jo.com</a10:uri>
      </a10:author>
      <category>modularity</category>
      <title>Unlocking Modularity in ABP.io: A Closer Look at the Contributor Pattern</title>
      <description>Deep dive into the ABP framework's established `Contributor pattern`. This post walks you through the ins and outs of leveraging this existing pattern for enhancing modularity in your software, discover through examples and code snippets how to implement this pattern to enhance your projects,</description>
      <pubDate>Sun, 21 Apr 2024 18:49:42 Z</pubDate>
      <a10:updated>2026-10-10T17:12:31Z</a10:updated>
      <content:encoded><![CDATA[<h1>Introduction</h1>
<p>The Contributor pattern has been utilized in various modules and packages within the ABP framework, including Toolbar Contributor, Tenant Resolver, Settings Page Contributor, and Image Resizer Contributor.</p>
<p>This pattern enables external modules to contribute to the execution or rendering of components within existing modules. A common example is the <code>IMenuContributor</code> for the <a href="https://docs.abp.io/en/abp/latest/UI/AspNetCore/Navigation-Menu">Navigation Menu</a>.</p>
<p>The <code>AbpUiNavigationModule</code> allows external modules to add their navigation menus, centralizing UI navigation in the application while being open for extension and closed for modification.</p>
<pre><code class="language-cs">using System.Threading.Tasks;
using MyProject.Localization;
using Volo.Abp.UI.Navigation;

namespace MyProject.Web.Menus
{
    public class MyProjectMenuContributor : IMenuContributor
    {
        public async Task ConfigureMenuAsync(MenuConfigurationContext context)
        {
            if (context.Menu.Name == StandardMenus.Main)
            {
                await ConfigureMainMenuAsync(context);
            }
        }

        private async Task ConfigureMainMenuAsync(MenuConfigurationContext context)
        {
            var l = context.GetLocalizer&lt;MyProjectResource&gt;();

            context.Menu.AddItem(
                new ApplicationMenuItem(&quot;MyProject.Crm&quot;, l[&quot;Menu:CRM&quot;])
                    .AddItem(new ApplicationMenuItem(
                        name: &quot;MyProject.Crm.Customers&quot;, 
                        displayName: l[&quot;Menu:Customers&quot;], 
                        url: &quot;/crm/customers&quot;)
                    ).AddItem(new ApplicationMenuItem(
                        name: &quot;MyProject.Crm.Orders&quot;, 
                        displayName: l[&quot;Menu:Orders&quot;],
                        url: &quot;/crm/orders&quot;)
                     )
            );
        }
    }
}
</code></pre>
<h1>Deep dive in Abp.io Contributer pattern:</h1>
<h2>Definition</h2>
<p>The contributor pattern is used to allow various parts of an application to contribute or inject additional functionality into a core component without directly modifying it. in simple words it's an implementation for OC princeple.</p>
<pre><code>Software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification.
</code></pre>
<h2>Analysis</h2>
<p>let's start by analysing two of the most common usage of <code>Contributer Pattern</code></p>
<h4>1) Menu conitrubutor:</h4>
<p><img src="https://raw.githubusercontent.com/QaisAlkhateeb/QKhateeb.AbpIO.Posts/master/menu_code.png" alt="MenuClassDiagram" /></p>
<p>As shown in the diagram, menu <em>contributors</em> that implement <code>IMenuContributo</code> can manage and configgure thier menu items by accessing <code>MenuConfigurationContext</code>, <code>MenuConfigurationContext</code> shared between all contributors and represents the <em>data model</em>.</p>
<p>the implementation of <code>IMenuManager</code> (<em>manager</em>) creates <code>MenuConfigurationContext</code>, load all contibutors and execute <code>ConfigureMenuAsync</code>.</p>
<h4>2) Settings Page Contributor:</h4>
<p><img src="https://raw.githubusercontent.com/QaisAlkhateeb/QKhateeb.AbpIO.Posts/master/settings_code.png" alt="SettingsClassDiagram" /></p>
<p>I think it's now more clear and you can start capture the pattern, in settings page again we have the <code>SettingPageCreationContext</code> which shared between all contributors and contains the settings groups (<em>data model</em>).</p>
<p>the <code>SettingPageContributorManager</code> creates the <code>SettingPageCreationContext</code>, loads the contributors by the help of DI <code>SettingManagementPageOptions</code>, and execute the <code>contributor.ConfigureAsync(context);</code></p>
<p>A slight different between both MenuContributor strucutre and SettingsPageContributor structor, that <code>SettingPageCreationContext</code> exposed to consumers while in Menu management you can access the data model only by using <code>IMenuManager</code></p>
<h2>Structure of Contributor Pattern:</h2>
<ul>
<li><p><em>ComponentContext</em>: a shared context with all contributors contains initial values and data models related to contributor component.</p>
</li>
<li><p><em>Contributor</em>: Implements <code>Configure</code> method that acccess <code>ComponentContext</code> to add it's contribution to a component.</p>
</li>
<li><p><em>ContributorManager</em>: Create and initialize <code>ComponentContext</code>, load and configure <code>Contributor</code>s</p>
</li>
<li><p><em>ContributorOptions</em>: a useage of <code>IOptions&lt;&gt;</code> pattern to use DI to load contributors, used by <code>ContributorManager</code></p>
</li>
</ul>
<p><img src="https://raw.githubusercontent.com/QaisAlkhateeb/QKhateeb.AbpIO.Posts/master/code.png" alt="ContributorPatternClassDiagram" /></p>
<h1>Example:</h1>
<p>Let's apply this pattern in new example. when you create new Abp application you will find <code>HostDashboard</code> Page:</p>
<pre><code class="language-html">&lt;!-- HostDashboard.cshtml --&gt;

&lt;div id=&quot;HostDashboardWidgetsArea&quot; data-widget-filter=&quot;#DashboardFilterForm&quot;&gt;
    &lt;abp-row&gt;
        
        @if (await WidgetManager.IsGrantedAsync(typeof(AuditLoggingErrorRateWidgetViewComponent)))
        {
            &lt;abp-column size-md=&quot;_12&quot; size-lg=&quot;_6&quot;&gt;
                @await Component.InvokeAsync(typeof(AuditLoggingErrorRateWidgetViewComponent))
            &lt;/abp-column&gt;
        }
        @if (await WidgetManager.IsGrantedAsync(typeof(AuditLoggingAverageExecutionDurationPerDayWidgetViewComponent)))
        {
            &lt;abp-column size-md=&quot;_12&quot; size-lg=&quot;_6&quot;&gt;
                @await Component.InvokeAsync(typeof(AuditLoggingAverageExecutionDurationPerDayWidgetViewComponent))
            &lt;/abp-column&gt;
        }
        @if (await WidgetManager.IsGrantedAsync(typeof(SaasEditionPercentageWidgetViewComponent)))
        {
            &lt;abp-column size-md=&quot;_12&quot; size-lg=&quot;_6&quot;&gt;
                @await Component.InvokeAsync(typeof(SaasEditionPercentageWidgetViewComponent))
            &lt;/abp-column&gt;
        }
        @if (await WidgetManager.IsGrantedAsync(typeof(SaasLatestTenantsWidgetViewComponent)))
        {
            &lt;abp-column size-md=&quot;_12&quot; size-lg=&quot;_6&quot;&gt;
                @await Component.InvokeAsync(typeof(SaasLatestTenantsWidgetViewComponent))
            &lt;/abp-column&gt;
        }
    &lt;/abp-row&gt;
&lt;/div&gt;
</code></pre>
<p>Instead of having specific widgets let's build a widgets contributor for that Page, after we complete the implementatoin we should be able to load the widgets on the dashboard from contributrs:</p>
<pre><code class="language-cs">&lt;div id=&quot;HostDashboardWidgetsArea&quot; data-widget-filter=&quot;#DashboardFilterForm&quot;&gt;
    &lt;abp-row&gt;
        @foreach (var widget in (await hostDashboardManager.GetWidgetsAsync()))
        {
            if (await WidgetManager.IsGrantedAsync(widget.WidgetType))
            {
                &lt;abp-column size-md=&quot;_12&quot; size-lg=&quot;_6&quot;&gt;
                    @await Component.InvokeAsync(widget.WidgetType)
                &lt;/abp-column&gt;
            }
        }
    &lt;/abp-row&gt;
&lt;/div&gt;
</code></pre>
<p>we will start with the <em>ComponentContext</em> and <em>Component Model</em>:</p>
<pre><code class="language-cs">// DashboardWidget.cs

using System;

namespace Abp.ContributorPatternExample.Web.HostDashboardManagement
{
    public class DashboardWidget
    {
        public Type WidgetType { get; set; }
    }
}
</code></pre>
<pre><code class="language-cs">// HostDashboardPageContext.cs

using System;
using System.Collections.Generic;
using Volo.Abp.DependencyInjection;

namespace Abp.ContributorPatternExample.Web.HostDashboardManagement
{
    public class HostDashboardPageContext : IServiceProviderAccessor
    {
        public List&lt;DashboardWidget&gt; Widgets { get; }

        public IServiceProvider ServiceProvider { get; private set; }

        public HostDashboardPageContext(IServiceProvider serviceProvider)
        {
            Widgets = new List&lt;DashboardWidget&gt;();
            ServiceProvider = serviceProvider;

        }

        public void AddWidget(Type dashboardWidget)
        {
            Widgets.Add(new DashboardWidget
            {
                WidgetType = dashboardWidget,
            });
        }
    }
}
</code></pre>
<p>Now let's defin the interface of the contributor:</p>
<pre><code class="language-cs">// IHostDashboardPageContributor.cs

using System.Threading.Tasks;

namespace Abp.ContributorPatternExample.Web.HostDashboardManagement
{
    public interface IHostDashboardPageContributor
    {
        Task ConfigureAsync(HostDashboardPageContext context);
    }
}
</code></pre>
<p>We will create the <em>Contributor Manager</em> and <em>Options</em> to load and configure the contributors:</p>
<pre><code class="language-cs">//HostDashboardPageOptions.cs

using System.Collections.Generic;

namespace Abp.ContributorPatternExample.Web.HostDashboardManagement
{
    public class HostDashboardPageOptions
    {
        public List&lt;IHostDashboardPageContributor&gt; Contributors { get; }

        public HostDashboardPageOptions() 
        {
            Contributors = new List&lt;IHostDashboardPageContributor&gt;();
        }
    }
}
</code></pre>
<pre><code class="language-cs">// HostDashboardPageManager.cs

using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Volo.Abp.DependencyInjection;

namespace Abp.ContributorPatternExample.Web.HostDashboardManagement
{
    public class HostDashboardPageManager : IScopedDependency
    {
        private readonly IServiceProvider _serviceProvider;

        public HostDashboardPageManager(IServiceProvider serviceProvider)
        {
            _serviceProvider = serviceProvider;
        }

        protected virtual async Task&lt;HostDashboardPageContext&gt; ConfigureAsync() 
        {
            var context = new HostDashboardPageContext(_serviceProvider);
            var contributors = _serviceProvider.GetRequiredService&lt;IOptions&lt;HostDashboardPageOptions&gt;&gt;().Value.Contributors;

            foreach (var contributor in contributors)
            {
                await contributor.ConfigureAsync(context);
            }

            return context;
        }

        public virtual async Task&lt;IReadOnlyList&lt;DashboardWidget&gt;&gt; GetWidgetsAsync()
        {
            var context = await ConfigureAsync();

            return context.Widgets.AsReadOnly();
        }
    }
}
</code></pre>
<p>By this steps we created the structor of Contributor Pattern, now let' use this pattern, we will create a default contributor and configure <code>HostDashboardPageOptions</code> in the module:</p>
<pre><code class="language-cs">// DefaultHostDashboardPageContributor.cs

using System.Threading.Tasks;
using Volo.Abp.AuditLogging.Web.Pages.Shared.Components.AverageExecutionDurationPerDayWidget;
using Volo.Abp.AuditLogging.Web.Pages.Shared.Components.ErrorRateWidget;
using Volo.Saas.Host.Pages.Shared.Components.SaasEditionPercentageWidget;
using Volo.Saas.Host.Pages.Shared.Components.SaasLatestTenantsWidget;

namespace Abp.ContributorPatternExample.Web.HostDashboardManagement
{
    public class DefaultHostDashboardPageContributor : IHostDashboardPageContributor
    {
        public async Task ConfigureAsync(HostDashboardPageContext context)
        {
            context.AddWidget(typeof(AuditLoggingErrorRateWidgetViewComponent));
            context.AddWidget(typeof(AuditLoggingAverageExecutionDurationPerDayWidgetViewComponent));
            context.AddWidget(typeof(SaasEditionPercentageWidgetViewComponent));
            context.AddWidget(typeof(SaasLatestTenantsWidgetViewComponent));
        }
    }
}
</code></pre>
<pre><code class="language-cs">// ContributorPatternExampleWebModule.cs

Configure&lt;HostDashboardPageOptions&gt;(options =&gt;
{
    options.Contributors.Add(new DefaultHostDashboardPageContributor());
});
</code></pre>
<p>Now we ready to use this on the <code>HostDashboard</code> page:</p>
<pre><code class="language-html">
&lt;!-- Inject Contributor Manager --&gt;

@inject HostDashboardPageManager hostDashboardManager 

&lt;div id=&quot;HostDashboardWidgetsArea&quot; data-widget-filter=&quot;#DashboardFilterForm&quot;&gt;
    &lt;abp-row&gt;
        &lt;!-- render contributors widgets --&gt;
        @foreach (var widget in (await hostDashboardManager.GetWidgetsAsync()))
        {
            if (await WidgetManager.IsGrantedAsync(widget.WidgetType))
            {
                &lt;abp-column size-md=&quot;_12&quot; size-lg=&quot;_6&quot;&gt;
                    @await Component.InvokeAsync(widget.WidgetType)
                &lt;/abp-column&gt;
            }
        }
    &lt;/abp-row&gt;
&lt;/div&gt;
</code></pre>
<h1>Side Note (Personal Preference)</h1>
<p>Instead of directly consuming the <em>Component Context</em>, such as in the <code>SettingPageCreationContext</code>, or consuming the <em>Component Model</em> by injecting the <em>Contributor Manager</em> (e.g., <code>MenuManager</code> or <code>HostDashboardPageManager</code>), I prefer to create a consumer intefrace. this approach hide the creation context details required for contribution away from the consumers and ensures the model remains immutable from the consumer side.</p>
<pre><code class="language-cs">using System.Collections.Generic;
using System.Threading.Tasks;
using Volo.Abp.DependencyInjection;

namespace Abp.ContributorPatternExample.Web.HostDashboardManagement
{
    public class HostDashboardPageContributorClient : IScopedDependency
    {
        private readonly HostDashboardPageManager _manager;

        public HostDashboardPageContributorClient(HostDashboardPageManager hostDashboardPageManager)
        {
            _manager = hostDashboardPageManager;
        }

        public virtual async Task&lt;IReadOnlyList&lt;DashboardWidget&gt;&gt; GetWidgetsAsync()
        {
            var context = await _manager.ConfigureAsync();

            return context.Widgets.AsReadOnly();
        }
    }
}
</code></pre>
<h1>Summerize</h1>
<p>The Contributor pattern is a powerful tool for implementing extensibility in your modules. By applying the Contributor Pattern, you can easily build rich, modular components that benefit from contributions from other modules.</p>
<p>The link of the example source code <a href="https://github.com/QaisAlkhateeb/QKhateeb.AbpIO.Posts/tree/master/Example">Source Code</a></p>
]]></content:encoded>
      <media:thumbnail url="https://abp.io/api/posts/cover-picture-source/3a12142a-f78c-2c17-b036-dc89b3c2efa9" />
      <media:content url="https://abp.io/api/posts/cover-picture-source/3a12142a-f78c-2c17-b036-dc89b3c2efa9" medium="image" />
    </item>
  </channel>
</rss>