Start from scratch ASP.NET Plug in development of core MVC - Summary of recent problems and solutions to some problems

Keywords: github FileProvider Javascript less

Title: implement from scratch ASP.NET Plug in development of core MVC (7) - Summary of recent problems and solutions to some problems
By Lamond Lu
Address: https://www.cnblogs.com/lwqlun/p/12930713.html
Source code: https://github.com/lamondlu/Mystique


Prospect review

brief introduction

In the previous article, I explained to you the loading problem of plug-in reference assembly. When loading plug-ins, we not only need to load the plug-in assembly, but also the plug-in reference assembly. After finishing the last article, a lot of small partners contacted me and raised various questions. Thank you for your support. You are my driving force. In this article, I will summarize and answer some of the main questions.

How to start a project in debug mode in Visual Studio?

Among all the problems, the most mentioned one is how to start the project using debug mode in Visual Studio. By default, the current project can start debugging mode in Visual Studio, but when you try to access the installed plug-in route, all plug-in views cannot be opened.

The temporary solution given here is to use the command line dotnet in the bin\Debug\netcoreapp3.1 directory Mystique.dll To start the project.

Reasons and solutions for view not found

The main reason for this problem is that when the main site is started in debugging mode in Visual Studio, the default Working directory is the root directory of the current project, not the bin\Debug\netcoreapp3.1 directory, so when the main program looks for the plug-in view, according to all the built-in rules, the specified view file cannot be found, So the error message of The view 'xx' was not found is given.

Therefore, what we need to do here is to modify the Working directory of the current primary site. Here, we need to set the Working directory to the bin\Debug\netcoreapp3.1 directory under the current primary site.

PS: during the development process, I upgraded the. NET Core to version 3.1. If you are still using. NET Core 2.2 or. NET Core 3.0, please configure the Working directory as the corresponding directory

This way, when you start the project in debug mode again in Visual Studio, you can access the plug-in view.

The consequent loss of style

After reading the previous solution, are you already eager to try it?

But when you start the project, you will find that the whole site's style and Javascript script file references are lost.

The reason is that the default static resource files of the primary site are all placed in the wwwroot subdirectory of the project root directory, but now we have changed the Working directory to bin\Debug\netcoreapp3.1. There is no wwwroot subdirectory in bin\Debug\netcoreapp3.1, so after modifying the Working directory, the static resource files cannot be loaded normally.

In order to fix this problem, we need to make two changes to the code.

First of all, we need to know when we use app.UseStaticFiles() add a static resource file directory to the When the project is started in debugging mode in studio, the default directory for project lookup is the wwwroot directory in the root directory of the current project, so we need to change this location to the physical fileprovider implementation mode, and specify that the directory where the current static resource file is located is the wwwroot directory under the project directory.

Secondly, because the current configuration is only for Visual Studio debugging, we need to use the precompiled instructions ා if DEBUG and ා if! Debug to configure different static resource file directories for different scenarios.

So the final modification result of Configure() method is as follows:

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }
    else
    {
        app.UseExceptionHandler("/Home/Error");
    }

#if DEBUG
    app.UseStaticFiles(new StaticFileOptions
    {
         FileProvider = new PhysicalFileProvider(@"G:\D1\Mystique\Mystique\wwwroot")
    });
#endif

#if !DEBUG
    app.UseStaticFiles();
#endif

    app.MystiqueRoute();
}

After the modification, recompile the project and start it in debug mode, you will find that the familiar interface is back.

How to realize message passing between plug-ins?

This problem was discussed with brother Yi Mingzhi at the end of last year when he discussed the dynamic plug-in development. The function proposed by brother Yi is easy to realize. However, in the process of practice, I stumbled over the AssemblyLoadContext.

Basic ideas

The simplest way to achieve message communication between two different plug-ins is to register and subscribe with messages.

PS: using the third-party message queue is also an implementation method, but in this practice, it is just for the sake of simplicity. Instead of using the additional message registration subscription component, it directly uses the in-process message registration subscription

Basic ideas:

  • Define the inotification handler interface to process messages
  • In each independent component, we expose the messages and handlers subscribed by the current component to the main program through the inotification provider interface
  • In the main site, we implement a message registration and subscription container through the inotification register interface. When the site is started, the system can implement it through the inotification provider interface of each component, and register the subscribed message and handler into the message publishing and subscription container of the main site.
  • In each plug-in, use the Publish method of the inotification register interface to Publish messages

According to the above ideas, we first define a message processing interface INotification

    public interface INotificationHandler
    {
        void Handle(string data);
    }

I didn't use strong type to standardize the message format here. The main reason is that if you use strong type to define messages, different plug-ins must refer to an assembly where the strong type and strong type message definitions are stored, which will increase the coupling between plug-ins and make each plug-in less independent.

PS: the above design is only personal preference. If you like to use strong type, there is no problem at all.

Next, let's define the message publish / subscribe interface and message handler interface

    public interface INotificationProvider
    {
        Dictionary<string, List<INotificationHandler>> GetNotifications();
    }
    public interface INotificationRegister
    {
        void Subscribe(string eventName, INotificationHandler handler);

        void Publish(string eventName, string data);
    }

The code here is very simple. The inotification provider interface provides a collection of message processors. The inotification register interface defines the methods of message subscription and publishing.

Now we are Mystique.Core.Mvc The interface implementation of inotification register is completed in the project.

    public class NotificationRegister : INotificationRegister
    {
        private static Dictionary<string, List<INotificationHandler>>
            _containers = new Dictionary<string, List<INotificationHandler>>();

        public void Publish(string eventName, string data)
        {
            if (_containers.ContainsKey(eventName))
            {
                foreach (var item in _containers[eventName])
                {
                    item.Handle(data);
                }
            }
        }

        public void Subscribe(string eventName, INotificationHandler handler)
        {
            if (_containers.ContainsKey(eventName))
            {
                _containers[eventName].Add(handler);
            }
            else
            {
                _containers[eventName] = new List<INotificationHandler>() { handler };
            }
        }
    }

Finally, we need to configure the discovery and binding of message subscribers in the project startup method MystiqueSetup.

    public static void MystiqueSetup(this IServiceCollection services, 
    	IConfiguration configuration)
    {

        ...
        using (IServiceScope scope = provider.CreateScope())
        {
            ...

            foreach (ViewModels.PluginListItemViewModel plugin in allEnabledPlugins)
            {
                ...
                using (FileStream fs = new FileStream(filePath, FileMode.Open))
                {
                    ...
                    var providers = assembly.GetExportedTypes()
                    .Where(p => p.GetInterfaces()
                        .Any(x => x.Name == "INotificationProvider"));

                    if (providers != null && providers.Count() > 0)
                    {
                        var register = scope.ServiceProvider
                            .GetService<INotificationRegister>();

                        foreach (var p in providers)
                        {
                            var obj = (INotificationProvider)assembly
                                .CreateInstance(p.FullName);
                            var result = obj.GetNotifications();

                            foreach (var item in result)
                            {
                                foreach (var i in item.Value)
                                {
                                    register.Subscribe(item.Key, i);
                                }
                            }
                        }
                    }
                }
            }
        }

        ...
    }

After completing the above basic settings, we can try to publish subscription messages in the plug-in.

First, we create the message LoadHelloWorldEvent in DemoPlugin2 and the corresponding message processor LoadHelloWorldEventHandler

    public class NotificationProvider : INotificationProvider
    {
        public Dictionary<string, List<INotificationHandler>> GetNotifications()
        {
            var handlers = new List<INotificationHandler> { new LoadHelloWorldEventHandler() };
            var result = new Dictionary<string, List<INotificationHandler>>();

            result.Add("LoadHelloWorldEvent", handlers);

            return result;
        }
    }

    public class LoadHelloWorldEventHandler : INotificationHandler
    {
        public void Handle(string data)
        {
            Console.WriteLine("Plugin2 handled hello world events." + data);
        }
    }

    public class LoadHelloWorldEvent
    {
        public string Str { get; set; }
    }

Then we modify the HelloWorld method of DemoPlugin1 and publish a message of LoadHelloWorldEvent before returning to the view.

    [Area("DemoPlugin1")]
    public class Plugin1Controller : Controller
    {
        private INotificationRegister _notificationRegister;

        public Plugin1Controller(INotificationRegister notificationRegister)
        {
            _notificationRegister = notificationRegister;
        }

        [Page("Plugin One")]
        [HttpGet]
        public IActionResult HelloWorld()
        {
            string content = new Demo().SayHello();
            ViewBag.Content = content + "; Plugin2 triggered";

            _notificationRegister.Publish("LoadHelloWorldEvent", JsonConvert.SerializeObject(new LoadHelloWorldEvent() { Str = "Hello World" }));

            return View();
        }
    }

    public class LoadHelloWorldEvent
    {
        public string Str { get; set; }
    }

The flexibility problem of assembly loadcontext

The above code looks nice, but in practice, you will encounter a smart problem, that is, the system cannot convert the NotificationProvider in DemoPlugin2 to an object of the interface type inotification provider.

This problem has been bothering me for a long time, and I can't imagine the possible problem at all, but I have a vague feeling that it is caused by an AssemblyLoadContext.

In the previous article, we looked up the assembly load design documentation for the. NET Core.

In the design document of. NET Core, there is such a description for assembly loading

If the assembly was already present in A1's context, either because we had successfully loaded it earlier, or because we failed to load it for some reason, we return the corresponding status (and assembly reference for the success case).

However, if C1 was not found in A1's context, the Load method override in A1's context is invoked.

  • For Custom LoadContext, this override is an opportunity to load an assembly before the fallback (see below) to Default LoadContext is attempted to resolve the load.
  • For Default LoadContext, this override always returns null since Default Context cannot override itself.

In short, this means that when loading an assembly in a custom LoadContext, if the assembly cannot be found, the program will automatically go to the default LoadContext to find it. If none of the default LoadContext can be found, it will return null.

I suddenly wonder if it's because DemoPlugin1, DemoPlugin2 and the assembly LoadContext of the primary site are all loaded Mystique.Core.dll Because of the assembly, although they load the same assembly, the system considers it to be 2 assemblies because of the different LoadContext.

PS: the AssemblyLoadContext of the primary site is the default LoadContext

In fact, for DemoPlugin1 and DemoPlugin2, they do not have to be loaded at all Mystique.Core.dll Assembly, because the default LoadContext of the primary site has already loaded this assembly, when DemoPlugin1 and DemoPlugin2 use Mystique.Core.dll When the inotification provider defined in the assembly is loaded in the default LoadContext, the assembly they load will be All of them are in the default LoadContext, so there is no difference.

According to this idea, I modified the code of the loading part of the plug-in assembly Mystique.Core . * assemblies are excluded from the load list.

After restarting the project, the project runs normally and the message publishing subscription runs normally.

Features to be added in subsequent attempts of the project

Due to space issues, other remaining issues and functions will be completed in the next article. Here are the features that will be added step by step later in the project

  • After adding / removing plug-ins, the main site navigation bar automatically loads the plug-in entry page (completed, described in the next article)
  • In the primary site, add the page management module
  • Try to load multiple plug-ins on one page. The current plug-in can only implement one plug-in and one page.

But if you have any other ideas, you can also leave a message for me or Issue on Github. Your suggestions are the driving force for my progress.

summary

This article explains and answers the problems in the previous Github Issue and document reviews. It mainly explains how to debug plug-ins in Visual Studio and how to achieve message transmission between plug-ins. I will continue to add new content according to the feedback later. Please look forward to it.

Posted by bubblegum.anarchy on Fri, 22 May 2020 00:20:18 -0700