Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / programming / architecture

Building a Plugin Architecture with Managed Extensibility Framework

1.44/5 (2 votes)
16 Sep 2024CPOL8 min read 5.3K   80  
How to build a plugin architecture in C# ?
Starting today, we embark on a journey exploring a sequence of articles delving into the creation of a plugin architecture within the .NET Framework. Initially, we will delve into the foundational concepts before elucidating them through the utilization of the Managed Extensibility Framework, an integral component native to the .NET Framework.

Introduction

One of the most advantageous qualities a software can possess is indeed extensibility. This essentially means that the design of our programs should facilitate the seamless integration of new capabilities or features without necessitating extensive refactoring or introducing fragility. As engineers, it is crucial for us to identify these points of extensibility within the codebase, or, if necessary, swiftly adapt certain areas of the program to imbue it with this quality.

The outline of this article is as follows: we will first explore how this capability can be achieved through well-crafted design patterns (such as a strategy or a template method) and uncover certain limitations inherent in these patterns. Consequently, we will transition to a more sophisticated yet widely adopted approach centered around plugins. Through this journey, we will delve into the concept of a plugin architecture and its implications.

The subsequent textbook proves useful for concluding this series: Software Architecture by Example: Using C# and .NET (Michaels)

This article was originally published here: Building a plugin architecture with Managed Extensibility Framework

When should extensibility be introduced in software ?

Incorporating extensibility features into a software system itself may not be inherently challenging. What proves more intricate is the discernment, assessment, or anticipation of when it is opportune and beneficial to introduce such extensibility points. Frequently, this need is implicitly conveyed by clients, even without their explicit awareness of it.

Image 1

What Methods can we Employ to Seamlessly Integrate Extensibility Points into a Software System?

In this scenario, leveraging an object-oriented programming language such as C# or Java (which is not overly restrictive), we conventionally establish an interface from which multiple classes can inherit. Subsequently, it falls upon the client to invoke the appropriate class during runtime.

Information
In the realm of OOP, the objective is to encapsulate elements that exhibit variability.

We contemplate the straightforward scenario outlined previously, wherein we must define multiple payment methods for a virtual ecommerce website.

Image 2

Thus, the initial step involves defining a contract, typically an interface, to showcase the available methods.

C#
public interface IPaymentMethod
{
    string Name { get; }
    
    bool TryProcessPayment(double amount);
}

Information

The interface presented here is minimalist. In a real-world scenario, there would likely be additional methods and more intricate data structures (here, we are utilizing a simple double to denote the amount).

Once this contract is established, deriving the relevant classes to accommodate the specific characteristics of each payment method becomes straightforward.

C#
public class PayPalPaymentMethod : IPaymentMethod
{
    public string Name => "PAYPAL";
    
    public bool TryProcessPayment(double amount)
    {
        // Process payment with PayPal
        return true;
    }
}

public class VisaPaymentMethod : IPaymentMethod
{
    public string Name => "VISA";
    
    public bool TryProcessPayment(double amount)
    {
        // Process payment with Visa
        return true;
    }
}

And so forth for each payment method...

It is then incumbent upon the client to invoke the appropriate class, which can be achieved through various methods. We propose the following approach here.

C#
public class PaymentMethodFactory
{
    private static PaymentMethodFactory _instance;

    private PaymentMethodFactory() { }
    
    public static PaymentMethodFactory Instance
    {
        get
        {
            if (_instance == null) _instance = new PaymentMethodFactory();
            return _instance;
        }
    }
    
    public IPaymentMethod Get(string name)
    {
        IPaymentMethod method = name switch
        {
            "PAYPAL" => new PayPalPaymentMethod(),
            "VISA" => new VisaPaymentMethod(),
            _ => throw new NotImplementedException()
        };
        
        return method;
    }    
}

Information

Please note that this class is implemented as a singleton. It is unnecessary to instantiate it each time a payment method is required.

The main program can then assemble these components together.

C#
internal class Program
{
    static void Main(string[] args)
    {        
        Console.WriteLine("Enter method:");
        var s = Console.ReadLine();
        var method = PaymentMethodFactory.Instance.Get(s);

        // Process payment
        var res = method.TryProcessPayment(200.0);
    }
}

Information

This approach to processing is founded on the Strategy design pattern.

What is the Process for Integrating a New Payment Method using this Approach?

Suppose we need to introduce a new payment method. How should we proceed in this scenario?

Image 3

Indeed, it's a straightforward process: we simply create a new class that inherits from IPaymentMethod and adjust the PaymentMethodFactory class accordingly.

C#
public class MasterCardPaymentMethod : IPaymentMethod
{
    public string Name => "MASTERCARD";
    
    public bool TryProcessPayment(double amount)
    {
        // Process payment with MasterCard
        return true;
    }
}

public class PaymentMethodFactory
{
    // ...
    
    public IPaymentMethod Get(string name)
    {
        IPaymentMethod method = name switch
        {
            "PAYPAL" => new PayPalPaymentMethod(),
            "VISA" => new VisaPaymentMethod(),
            "MASTERCARD" => new MasterCardPaymentMethod(),
            _ => throw new NotImplementedException()
        };
        
        return method;
    }    
}

What are the Limitations of this Architecture?

  • There is nothing flawed in the preceding approach. It is a highly effective and widely utilized method for extending software. Moreover, it would indeed be the preferable approach if we only needed to add a few payment methods.

  • This approach is also ideally suited if we do not require third parties to independently develop the extensions. On the contrary, indeed, we would need to expose our contracts to make them publicly available. In such a scenario, an architecture based on plugins would be more advantageous, and we will delve into this in the next article.

  • Nevertheless, as illustrated by the PaymentMethodFactory, there exists a requirement to manually register each method whenever a new class is introduced. This process could potentially become cumbersome and prone to errors, particularly in scenarios involving numerous modules.

How can we incorporate these considerations effectively? Go on reading.

What is a Plugin Architecture?

A plugin architecture is a design pattern or framework that allows an application or system to be extended or enhanced with additional functionality through plugins or modules.

Image 4

In a plugin architecture:

  • An application or system provides a core set of features and functionality (core system).

  • Plugins are separate pieces of code that can be added to the core system to extend its capabilities. Plugins are usually designed to perform specific tasks or add specific features (DarkThemeManager, LightThemeManager).

  • The core system is designed to dynamically load and integrate plugins at runtime, without requiring modification to the core codebase. This allows for easy installation, removal, and updating of plugins without disrupting the operation of the main application.

  • Plugins interact with the core system through well-defined interfaces or APIs (IPaymentMethod, IThemeManager). This ensures that plugins can integrate seamlessly with the core system and communicate effectively.

  • A plugin architecture provides users with the ability to customize the functionality of the application according to their specific needs or preferences. Users can choose which plugins to install based on the features they require, allowing for a highly flexible and customizable user experience.

Plugin architectures are commonly used in a wide range of software applications, including content management systems, web browsers, multimedia players, and integrated development environments (IDEs), among others. They provide a powerful mechanism for extending the functionality of software systems while maintaining flexibility, modularity, and ease of maintenance.

We can discern the presence of numerous extensibility points within the system. Contrary to our prior approach, which relied on implementing a Strategy design pattern, the current system exhibits greater openness. This means that third-party developers have the opportunity to create their own plugins. Furthermore, the software is intentionally crafted to facilitate this, a departure from the previous paradigm where only the manufacturer or proprietor possessed the ability to code extensibility points.

What are the Various Components Comprising a Plugin Architecture?

What is a Plugin?

A plugin is a modular component or extension that can be added to a software application to enhance its functionality or introduce new features. Plugins are designed to integrate seamlessly with the core system and are typically developed independently from the main application. They are often created to perform specific tasks or provide specialized capabilities, such as adding new tools to an editor, integrating with external services, or extending the functionality of a web browser.

Defining the Host

The term "host" in our context refers to the core system of the software. Essentially, it represents the main program responsible for overseeing overarching concerns such as security and bootstrapping. Moreover, the host serves as the provider of the plugin API, which delineates the various extensibility points accessible within the system.

Information

The host application bears the responsibility of loading and overseeing plugins, guaranteeing their proper execution, and furnishing them with requisite resources or data. This entails the dynamic loading of plugins into the system, managing their lifecycle, and orchestrating their interactions with the host environment. Furthermore, the host is tasked with ensuring that plugins operate seamlessly within the application's ecosystem, thereby contributing to the overall functionality and performance of the software.

Defining the Interfaces

A plugin architecture necessitates the provision of interfaces to facilitate the seamless integration of plugins within the host system. These interfaces essentially serve as contracts, delineating the methods and properties that plugins must adhere to. Consequently, they establish a set of guidelines that plugins must follow to ensure proper incorporation into the host environment.

In essence, these interfaces act as a standardized framework that governs the interaction between plugins and the host system, fostering compatibility and coherence within the overall architecture.

Defining the Plugins

Plugins, also referred to as modules or extensions, are discrete components that empower the expansion of a host application without necessitating recompilation. These self-contained units augment the functionality of the host system by introducing new features or capabilities, all while maintaining independence from the core codebase. This enables developers to seamlessly integrate additional functionality into the host environment, enhancing its versatility and adaptability without the need for extensive modifications or rebuilding of the entire application.

What Constraints does a Plugin Architecture Entail?

As is often the case, every advancement in this world comes with its own set of trade-offs. While a plugin architecture facilitates the seamless integration of features into software, it also imposes certain constraints.

Information

This section draws upon our specific experiences and may not necessarily apply universally or comprehensively to other scenarios.

While a plugin architecture can certainly serve internal purposes, its primary application lies in facilitating usage by third-party entities. Within this framework, security emerges as a paramount concern, necessitating continuous vigilance to thwart potential attempts by malicious actors to compromise the host system.

Addressing these security considerations is by no means a straightforward task; it often demands considerable time and effort to implement effectively.

Plugins frequently require access to internal data, such as orders for payment methods, and may need to interact with various types of information. Ensuring that plugins receive precisely the necessary data—neither more nor less—is more akin to an art than a science.

Given the complexities associated with modifying an interface post-release, it's imperative to anticipate and address this concern with careful foresight.

But enough with theory; let's delve into practical implementation! We'll now explore how a plugin architecture can be swiftly implemented in the .NET Framework using the Managed Extensibility Framework (MEF). But to avoid overloading this article, readers interested in this implementation can find the continuation here.

History

  • 22nd March, 2024: Initial version

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)