
Durante lo sviluppo del nostro CMS WebRight, è nata l’esigenza di permettere a persone esterne al team di sviluppare componenti aggiuntive. Ho pensato potesse essere interessante raccontarvi come abbiamo affrontato il problema, mostrandovi come sviluppare un sistema a plug-in per le vostre applicazioni .NET.
Il primo problema da risolvere è stato riconoscere i plugin tra gli assembly caricati. Nel nostro caso, abbiamo utilizzato un’interfaccia che permette, a chi la implementa, di definire quello che per noi è un plugin. Dato che WebRight è basato su un nostro framework chiamato Raptor, l’interfaccia si chiama IRaptorPlugin:
public interface IRaptorPlugin
{
string Name { get; }
string Version { get; }
void OnLoad();
}
Se, ad esempio, vogliamo creare un plugin per la gestione di un menu, creiamo una libreria contenente una classe che implementa l’interfaccia:
public class MenuPlugin : IRaptorPlugin
{
public string Name => "Menu";
public string Version => "1.0.0"
public void OnLoad()
{
}
}
A questo punto, siamo già in grado di caricare il plugin nel CMS, riconoscendo questa classe grazie alla Reflection. Abbiamo quindi creato un plugin manager che fa il lavoro sporco per noi, di cui vi riporto una versione semplificata per evidenziare i punti salienti:
public class RaptorPluginManager : IRaptorPluginManager
{
public ICollection<Assembly> Plugins { get; private set; }
public RaptorPluginManager(string path, IServiceCollection services)
{
this.Plugins = new List<Assembly>();
string[] dlls = Directory.GetFiles(path, "*.dll", SearchOption.AllDirectories);
foreach (var dll in dlls)
{
var plugin = Assembly.LoadFrom(dll);
var pluginClass = plugin.GetTypes().Where(x => typeof(IRaptorPlugin).IsAssignableFrom(x)).SingleOrDefault();
if (pluginClass != null)
{
var pluginInstance = Activator.CreateInstance(pluginClass);
var loadMethod = pluginClass.GetMethod("OnLoad");
loadMethod.Invoke(pluginInstance, null);
this.Plugins.Add(plugin);
}
}
}
}
Questo codice non fa altro che cercare tutte le DLL in una cartella specificata e, per ogni elemento trovato, verificare se è presente un’ implementazione di IRaptorPlugin. Per ciascuna di tali implementazioni, viene eseguito il metodo OnLoad, che può essere utilizzato per popolare qualche configurazione o per effettuare per un seed di dati, e viene poi collezionata in una lista di plugin.
Una volta che abbiamo la lista, possiamo caricare funzionalità specifiche del framework. Nel nostro caso, parlando di un CMS, abbiamo definito i concetti di Page e Widget, il primo per visualizzare una pagina di contenuti, il secondo per mostrare una particolare sezione di contenuto all’interno di una pagina. Nell’implementazione di WebRight in ASP.NET Core, una Page è una View di MVC, a cui possiamo arrivare direttamente tramite una specifica rotta, mentre una widget è un ViewComponent (trovate la definizione nel mio articolo).
Completiamo quindi il nostro plugin col codice per inserire un widget Menu.Ricalcando lo stesso meccanismo usato per il concetto di plugin, una widget è una classe che implementa una specifica interfaccia, che nel nostro caso si chiama IWebRightWidget:
public interface IWebRightWidget
{
IWebRightBaseViewModel GetViewModel(string language, object[] parameters);
}
Questa interfaccia non solo marca una classe come widget di WebRight, ma permette anche di definire come vengono recuperati i dati necessari al suo funzionamento, che verranno poi riversati in un classico ViewModel. Se ad esempio i dati per la widget di un menu vengono definiti all’interno di una classe WebRightMenuViewModel, che estenderà una classe base del framework o implementerà direttamente IWebRightBaseViewModel, l’implementazione della widget somiglierà a qualcosa del genere:
public class WebRightMenuWidget : IWebRightWidget
{
public IRaptorBaseViewModel GetViewModel(string language, object[] parameters)
{
WebRightMenuViewModel vm = new WebRightMenuViewModel();
… view model fill logic ...
return vm;
}
}
Come fa il framework a sapere che nel plugin è presente una widget? WebRight definisce una WidgetFactory che contiene un metodo che, dato il nome della widget, ne verifica l’esistenza e la restituisce al chiamante che provvederà a visualizzarla:
public IWebRightWidget GetWidgetByName(string name)
{
if (cache.Exists(name))
{
return cache.Get<IWebRightWidget>(name);
}
else
{
var widgetType = pluginManager.Plugins.GetTypes()
.Where(y => typeof(IWebRightWidget).IsAssignableFrom(y) && y.Name == name)
.FirstOrDefault();
if (widgetType != null)
{
var widget = (IWebRightWidget)this.serviceProvider.GetService(widgetType);
cache.Add<IWebRightWidget>(widget, name);
return widget;
}
foreach (var plugin in this.pluginManager.Plugins)
{
widgetType = plugin.GetTypes()
.Where(y => typeof(IWebRightWidget).IsAssignableFrom(y) && y.Name == name)
.FirstOrDefault();
if (widgetType != null)
{
var widget = (IWebRightWidget)this.serviceProvider.GetService(widgetType);
cache.Add<IWebRightWidget>(widget, name);
return widget;
}
}
throw new WebRightWidgetNotFoundException(name);
}
}
}
Il metodo va a verificare se tra i plugin esposti dal PluginManager è presente la widget richiesta. Se la trova, la aggiunge in una cache per velocizzare richieste successive e la restituisce; in caso contrario viene sollevata una eccezione.
Come potete vedere, si tratta semplicemente di stabilire delle convenzioni e usare la Reflection di .NET, ottimizzando le performance facendo uso della cache. Il codice che abbiamo in produzione è ovviamente un po’ più complesso di così, ma l’idea di base è quella esposta. Se siete curiosi di sapere come poi il widget viene visualizzato, potete leggere un mio precedente articolo.
Alla prossima!