对于大型的应用软件,特别是客户端应用软件,应用启动过程中,需要执行大量的逻辑,包括各个模块的初始化和注册等等逻辑。大型应用软件的启动过程都是非常复杂的,而客户端应用软件是对应用的启动性能有所要求的,不同于服务端的应用软件。设想,用户双击了桌面图标,然而等待几分钟,应用才启动完毕,那用户下一步会不会就是点击卸载了。为了权衡大型应用软件在启动过程,既需要执行复杂的启动逻辑,又需要关注启动性能,为此过程造一个框架是一个完全合理的事情。我所在的团队为启动过程造的库,就是本文将要和大家介绍我所在团队开源的 dotnetCampus.ApplicationStartupManager 启动流程框架的库
background
The origin of this library was a time when I listened to a sharing by the VisualStudio team. At that time, the big shots told me that in order to optimize the startup performance of VisualStudio, his team had formulated an interesting direction, which was to run the CPU, memory and disks when the application was launched. Full. Of course, this is a joke. The original meaning is that when the Visual Studio application is launched, the performance of the computer should be fully squeezed. Coincidentally, my team also has many large-scale applications with code MergeRequests exceeding 10,000. The logical complexity of these applications is very high. Originally, they could only be executed using a single thread, thereby reducing the pits caused by the dependency complexity between modules. However, in order to optimize the startup performance of the application software, considering the strategy of squeezing machine performance, multi-threading was included.
However, when opening multiple threads, you will naturally encounter many thread-related problems. The biggest problem is how to deal with the dependencies between various startup modules. If there is no better framework to handle it and only relies on the developer's personal abilities to handle it, it is completely unreliable to do this refactoring, or this thing is not far away. Maybe this version can be optimized, but what about the next version?
It is also very important to monitor the startup performance, such as analyzing the time consumption of each startup item. It is necessary to measure the performance of startup modules before optimizing the performance of startup service modules one by one. Interestingly, the startup module is very dependent on the user environment of the demon, that is, the results measured in the laboratory are very different from the results of actual user use. This also puts an important requirement on the startup process framework, which is to support convenient performance measurement monitoring of individual startup modules.
Since multiple projects expect to connect to the start-up process framework, the start-up process framework should be sufficiently abstract, and it is best not to have the function of coupling a single project
After about a year of development time, the start-up process framework was officially put into use in 2019. The logic to launch the process framework is currently running on nearly 10 million devices
当前此启动流程框架的库在 GitHub 上,基于最友好的 MIT 协议,也就是大家可以随便用的协议进行开源,开源地址: https://github.com/dotnet-campus/dotnetCampus.ApplicationStartupManager
function
我所在的团队开源的 ApplicationStartupManager 启动流程框架的库提供了如下的卖点
- Automatic build startup flow chart
- Supports high-performance asynchronous multi-threaded startup task item execution
- Support automatic UI thread scheduling logic
- Dynamic allocation of startup task resources
- Support access to pre-compilation frameworks
- Supports all. NET applications
- Time-consuming monitoring of start-up process
Startup Flow Chart
There must be explicit or implicit dependencies between various startup task items, such as relying on a certain logic or module initialization, or relying on the registration of a certain service, or depending on execution timing. After the developer sorts out the dependencies and determines the dependencies between each startup task item, a startup flow chart can be built based on this dependency
Suppose there are the following startup task items, and there are mutual dependencies among them, as shown in the figure below, and arrows are used to represent the dependencies

- Startup task item A: The startup task item that is launched first, such as the initialization startup task item of a log or container
- Start task item B: Some basic services, but you need to rely on A to start task item to complete before execution
- Start task item C: Depends on B to start the execution of task item completion
- Start task item D: Another independent module that has no connection with B C E start task item, but also relies on the completion of A start task item
- Start task item E: Also rely on B C to start task item completion
- Start task item F: Also rely on A and D to start task item completion
The above startup task items can form a directed acyclic startup flow chart, and each startup task item can have its own prefix or post-prefix. So why does it need to be ringless? If two startup task items are waiting and dependent on each other, then naturally they cannot be successfully started. As shown in the figure below, there are three startup task items that are all dependent on each other, which means that no matter which startup task item is started first, it will not meet expectations, because the predecessor of the startup task item started first is not satisfied, and there is logically a predecessor dependency that has not been executed during the startup process

In order to better build the startup flow chart, two virtual nodes are also logically added, that is, the start point and the end point. No matter which startup task item it is, it will rely on the virtual start point and follow the end point.
In addition, specific business parties will also define their own associated startup processes, that is, preset startup nodes. Key startup process points will be relied on by each startup item, so that the startup process can be artificially divided into multiple stages
For example, the start-up process can be divided into the following stages
- Launch point: A virtual node that represents application launch and is used to build a launch flow chart
- Infrastructure: Indicates that logic should be done before starting basic services, such as initialization logs, initialization containers, etc. Other start-up task items can rely on the infrastructure, so that the start-up task items executed after the infrastructure are considered ready to be completed
- Window launch: Before initializing the window of the client program, the UI preparation logic needs to be completed, such as preparation of style resources and necessary data, or injection of ViewModel. After the window is launched, you can execute logic on UI elements or register UI strong correlation logic. Or, after the window is launched, execute startup tasks that do not need to be executed before the main interface is displayed, thereby improving the display performance of the main interface
- Application startup: After completing the startup logic, the startup task items after the application is launched are logic that can be slowly executed, such as triggering automatic updates of the application, such as performing log file cleanup, etc.
- End point: Virtual node, indicating that the application startup process is completely completed, used to build the startup flow chart

As shown in the figure, each startup task item can choose to rely on a specific startup task item, or it can choose to rely on a key startup process point
Through this logic, it is possible to prepare for subsequent optimization and facilitate upper-level business developers to develop start-up tasks for the business layer. This allows upper-level business developers to clearly understand where they should place their newly written startup task items. It can also provide debugging of the dependencies of startup task items of various modules to understand whether there is a loop dependence logic
High-performance asynchronous multi-threaded startup task item execution
For better squeezing machine performance, multi-threaded start-up is necessary. After completing the construction of the startup flow chart, the startup task items can be drawn into a tree, which naturally makes it convenient for multi-thread scheduling. Task scheduling based on. NET can realize asynchronous waiting for multiple threads and solve the thread safety problem of multiple start task items in the case of multiple threads.
For example, task scheduling using a thread pool can logically divide the starting task chain of different starting task items into execution by different threads. The actual execution of threads relies on the thread pool scheduling, and even in actual execution, the thread pool only uses two actual threads to execute

During the application startup process, if you don't understand the. NET thread pool scheduling mechanism, there will be a little controversy about opening multithreading. The core controversy is whether if an application fills up CPU resources during the startup process, it will make the user's computer stuck. In fact, the above question is not easy to answer. If you have any doubts, please listen to my detailed analysis. The first point is the problem itself. Let's ask the question itself first. If we just start a thread, will it also make the user's computer stuck and unable to move? The answer is yes, it completely depends on the user's computer, including the computer configuration and the computer's demonic environment. For example, if a shoddy device is combined with several domestically produced anti-virus software, then at the moment the application is launched, there will be a lot of anti-virus work. Execution will naturally become stuck. Moreover, if the computer is stuck and cannot be moved, is it inevitable that the CPU is full? The answer is not at all. During the application startup process, there will definitely be a DLL loading process, especially the application's cold startup process. A large amount of file reading and writing will occupy the read and write of some mechanical disks, which will naturally make the computer card immobile. This process has little to do with whether multi-threading is enabled. After all, the performance between the mechanical disk and the CPU lies here. The second question is whether the blocking time is important. For example, if the application is multithreaded, it will be stuck for 500 milliseconds. If the application is launched with only a single thread, it will take 4 x 500 ms = 2 s. Is it worthwhile to open multithreaded at this time? This is a trade-off. Different application logics are naturally different. For example, productivity tools. I originally started up to use this tool. For example, the VisualStudio tool used to write code. When I opened this application, there was naturally no need for synchronous use during the process. If it jammed, it would be jammed. The last problem is that turning on multithreading in. NET does not mean that CPU resources are full. Don't forget IO asynchronous
Of course, developers who can access the application process are definitely not novices. I believe that they have some knowledge of threads and will choose the appropriate method to perform the startup task item. This also tells everyone that there is a certain threshold for library access to this startup process framework
Support automatic UI thread scheduling logic
For client applications, there is naturally a special thread that is the UI thread. During the startup process, there is a lot of logic that needs to be executed on the UI thread. Since the UI thread schedules of various application frameworks in the. NET system are different, you need to start the process framework to perform a certain amount of adaptation.
Just mark the specific startup task item that the current startup task item needs to be executed on the UI thread. The framework layer will automatically schedule the startup task item to the UI thread for execution.
By design, start task items will be scheduled to non-UI threads for execution by default
Dynamic allocation of startup task resources
There is a big difference between the time taken to start each task item on the user side and the test results in the laboratory, whether it is a development machine or a testing machine, most of the time. If you execute the startup tasks in a fixed order, there will naturally be a lot of startup time waiting for them. This startup process framework library supports automatic dynamic scheduling based on the time consuming of each startup task item during the startup process
The core method is to build a startup flow chart that supports the waiting logic of each task. Based on the Task waiting mechanism, the waiting logic can be dynamically scheduled, thereby realizing dynamic scheduling of startup task items and filling multiple threads in a tight time. Execution of the startup task. If the corresponding upper-level business developer can correctly use the Task mechanism, such as correctly using asynchronous waiting, it can achieve great concealment during the startup process.
Support access to pre-compilation frameworks
启动过程是属于性能敏感的部分,各个模块的启动任务项如何收集是一个很大的问题。启动部分属于性能敏感部分,不合适采用反射的机制。好在 dotnet campus 里面有技术储备,在 2018 年的时候就开源了 SourceFusion 预编译框架,后面在 2020 年时吸取了原有 SourceFusion 的挖坑经验,重新开源了 dotnetCampus.Telescope 预编译框架,新开源的 dotnetCampus.Telescope 也放在 SourceFusion 仓库中
在 ApplicationStartupManager 启动流程框架开发之初就考虑了对接预编译框架,通过预编译提供了无须反射即可完成启动任务项收集的能力,可以极大减少因为启动过程中反射程序集的性能损耗
The pre-compiled framework is connected, which is equivalent to the time for the logic that originally needed to be executed on the client side. When the developer compiles, the logic that originally needed to be executed on the client side is executed. This can reduce the time for the client to execute logic
Connected to the pre-compilation framework, it is possible to collect all project start-up task items when developers compile, including start-up task item types and delegate creation of start-up task items, as well as the Attribute characteristics of start-up task items
Time-consuming monitoring of start-up process
For large applications, it is very important to pay attention to the operating effect on the user side. During the start-up process, monitoring is very important. The biggest significance of monitoring is:
First, we can understand the actual execution time consuming of each startup task item on the user's device, so that we can have data support when performing performance optimization in subsequent versions. Otherwise, it will be difficult to escape the real performance bottleneck on equipment with limited development or testing sides. For example, we can not only focus on the launch distribution of 95 lines on user devices, which is the distribution of launch time on 95 percent of users, but also focus on the launch distribution of users between 95 and 99 lines. Understand the environment of some more special devices to make special optimizations
Second, version comparisons can be made and warnings can be made. For large-scale applications, there are basically gray and pre-publishing mechanisms. By monitoring the start-up time during the gray process, you can connect with the early warning mechanism and tell the developer when the time limit for a certain start-up task item increases. This can benefit the long-term development of the project
The last point is that it can tell the user how slow the startup is and how slow it is. This mechanism focuses on providing openness. For example, Visual Studio will constantly tell you which plug-in is causing the slow launch.
use method
After removing the customization requirements of each project, the library that launches the process framework only has the core logic, which means that when using it, the specific business party also needs to add the initialization logic and the specific logic that adapts the business itself. In other words, accessing the start-up process framework does not simply install a library and then call the API. Instead, it requires some docking work based on the business needs of the application. Fortunately, the startup process framework is only suitable for large-scale projects or projects that are expected to be large. Compared to other logic for large-scale applications, the amount of code required to connect the startup process framework is basically negligible. It is naturally inappropriate for small projects or projects that are not collaborative with multiple people
整个 ApplicationStartupManager 启动流程框架设计上是高性能的,减少各个部分的性能内损。但是在上启动流程框架本身就存在一定的框架性能损耗,如果对应的只是小项目或非多人协作的项目,假设可以自己编排启动任务项,那自然自己编排启动任务项如此做是能达到性能最高的
应用 ApplicationStartupManager 启动流程框架能解决的矛盾点在于项目的复杂度加上多人协作的沟通,与启动性能之间的矛盾。接入启动流程框架可以让上层业务开发者屏蔽对启动过程细节的干扰,方便上层业务开发者根据业务需求加入启动任务项,方便启动模块维护者定位和处理启动任务项的性能
By convention, the first step in using a library in. NET is to install the library through NuGet
The first step is to install the ApplicationStartupManager library using NuGet. If the project uses an SDK-style project file format, you can add the following code to the csproj project file for installation
<ItemGroup>
<PackageReference Include="dotnetCampus.ApplicationStartupManager" Version="0.0.1-alpha01" />
</ItemGroup>
为了方便让大家看到 ApplicationStartupManager 启动流程框架库的效果,我采用了放在 https://github.com/dotnet-campus/dotnetCampus.ApplicationStartupManager 里的例子代码来作为例子
Three new projects are built, as follows
- WPFDemo.Lib1: Represents the underlying component libraries, especially business components
- WPFDemo.Api: The assembly of the application's API layer, where the framework logic to start the process will be deployed
- WPFDemo.App: The top level of the application, which is the assembly where the Main function is located, is where the logic to trigger the launch is triggered
The general abstraction of the application model architecture is as follows. However, for convenience of demonstration, the Business layer and the App layer are combined, and many Lib components are combined into one Lib1 project.

The new project has been created and the NuGet package has been installed. Now it is time to build the application related launch framework logic at the API level. Why do I need an API to do additional logic after installing the NuGet package? Each application has its own unique logic, the parameters required for the launch task items of each application are different, the logging method of each application can also be different, and the launch nodes of different types of applications are also different, so these all require application-related customization
First define the preset launch node related to the application
/// <summary>
/// 包含预设的启动节点。
/// </summary>
public class StartupNodes
{
/// <summary>
/// 基础服务(日志、异常处理、容器、生命周期管理等)请在此节点之前启动,其他业务请在此之后启动。
/// </summary>
public const string Foundation = "Foundation";
/// <summary>
/// 需要在任何一个 Window 创建之前启动的任务请在此节点之前。
/// 此节点之后将开始启动 UI。
/// </summary>
public const string CoreUI = "CoreUI";
/// <summary>
/// 需要在主 <see cref="Window"/> 创建之后启动的任务请在此节点之后。
/// 此节点完成则代表主要 UI 已经初始化完毕(但不一定已显示)。
/// </summary>
public const string UI = "UI";
/// <summary>
/// 应用程序已完成启动。如果应该显示一个窗口,则此窗口已布局、渲染完毕,对用户完全可见,可开始交互。
/// 不被其他业务依赖的模块可在此节点之后启动。
/// </summary>
public const string AppReady = "AppReady";
/// <summary>
/// 任何不关心何时启动的启动任务应该设定为在此节点之前完成。
/// </summary>
public const string StartupCompleted = "StartupCompleted";
}
After the definition is complete, you can use this to divide the startup process into the following stages

Then define a log type related to the application business party. Most of the logging methods of different applications are different, and the underlying logging used are also different
/// <summary>
/// 和项目关联的日志
/// </summary>
public class StartupLogger : StartupLoggerBase
{
public void LogInfo(string message)
{
Debug.WriteLine(message);
}
public override void ReportResult(IReadOnlyList<IStartupTaskWrapper> wrappers)
{
var stringBuilder = new StringBuilder();
foreach (var keyValuePair in MilestoneDictionary)
{
stringBuilder.AppendLine($"{keyValuePair.Key} - [{keyValuePair.Value.threadName}] Start:{keyValuePair.Value.start} Elapsed:{keyValuePair.Value.elapsed}");
}
Debug.WriteLine(stringBuilder.ToString());
}
}
如例子上的日志就是记录到 Debug.WriteLine 输出,同时日志里也添加了 LogInfo 方法
继续定制应用业务相关的启动任务项的参数,如例子代码的项目就用到了 dotnetCampus.CommandLine 提供的命令行参数解析,各个启动任务项也许会用到命令行参数,因此也就需要带入到启动任务项的参数里面,作为一个属性。例子代码的项目也用到了 dotnetCampus.Configurations 高性能配置文件库 提供的应用软件配置功能,也是各个启动任务项所需要的,放入到启动任务项的参数
The parameters of the startup task item added with attributes related to the application business are defined as follows
public class StartupContext : IStartupContext
{
public StartupContext(IStartupContext startupContext, CommandLine commandLine, StartupLogger logger, FileConfigurationRepo configuration, IAppConfigurator configs)
{
_startupContext = startupContext;
Logger = logger;
Configuration = configuration;
Configs = configs;
CommandLine = commandLine;
CommandLineOptions = CommandLine.As<Options>();
}
public StartupLogger Logger { get; }
public CommandLine CommandLine { get; }
public Options CommandLineOptions { get; }
public FileConfigurationRepo Configuration { get; }
public IAppConfigurator Configs { get; }
public Task<string> ReadCacheAsync(string key, string @default = "")
{
return Configuration.TryReadAsync(key, @default);
}
private readonly IStartupContext _startupContext;
public Task WaitStartupTaskAsync(string startupKey)
{
return _startupContext.WaitStartupTaskAsync(startupKey);
}
}
为了继续承接 WaitStartupTaskAsync 的功能,于是构造函数依然带上 IStartupContext 用于获取框架里默认提供的启动任务项的参数。上面代码的 Configuration 和 Configs 两个属性都是 dotnetCampus.Configurations 高性能配置文件库 提供的功能,可以使用 COIN 格式进行配置文件的读写
After completing the definition of the parameters of the launch task item, you can customize the base type of the launch task item for a specific application. Because the base type of the startup task item must be related to the parameters of the startup task item, and the parameters of the startup task item are different for each application, the base type of the startup task item is also different. Even if the difference is only the parameter to start the task item, and the code level can be solved using generalization, it will still cause a large amount of code in the business layer, so it is better to define it again in the application.
/// <summary>
/// 表示一个和当前业务强相关的启动任务
/// </summary>
public class StartupTask : StartupTaskBase
{
protected sealed override Task RunAsync(IStartupContext context)
{
return RunAsync((StartupContext) context);
}
protected virtual Task RunAsync(StartupContext context)
{
return CompletedTask;
}
}
With the above code, all application business sides should inherit StartupTask as the base class for starting task items. After inheritance, we still rewrite the RunAsync method and execute business logic in this method
The reason why RunAsync is designed to be a virtual method rather than an abstract method here is because some application businesses need some busy startup task items. These startup task items have no actual logical functions and are just added to optimize the choreography of the startup process. Another important point is that it allows upper-level business developers to solve the problem of not knowing how to return RunAsync Tasks when writing some logic that only synchronization. It allows upper-level business developers to naturally return the results of the base.RunAsync method, thereby reducing the number of weird ways to return Tasks
After completing the customization of the startup task base type, you need to write an application business-related StartupManager type based on StartupManagerBase. The logic here needs to include the logic of how to start specific startup task items. The code is as follows
/// <summary>
/// 和项目关联的启动管理器,用来注入业务相关的逻辑
/// </summary>
public class StartupManager : StartupManagerBase
{
public StartupManager(CommandLine commandLine, FileConfigurationRepo configuration, Func<Exception, Task> fastFailAction, IMainThreadDispatcher mainThreadDispatcher) : base(new StartupLogger(), fastFailAction, mainThreadDispatcher)
{
var appConfigurator = configuration.CreateAppConfigurator();
Context = new StartupContext(StartupContext, commandLine, (StartupLogger) Logger, configuration, appConfigurator);
}
private StartupContext Context { get; }
protected override Task<string> ExecuteStartupTaskAsync(StartupTaskBase startupTask, IStartupContext context, bool uiOnly)
{
return base.ExecuteStartupTaskAsync(startupTask, Context, uiOnly);
}
}
The above code overrides the ExecuteStartupTaskAsync method to pass in business-related StartupContext parameters when calling a specific startup task item
If the application has more requirements, you can override more StartupManagerBase methods, including the ExportStartupTasks method that exports all startup items. Overriding this method allows the application to define how to export all startup task items. Overriding the AddStartupTaskMetadataCollector method allows applications to define how to add startup information in the managed assembly, etc.
After completing the above steps, there is still one more thing that needs to be completed. The newly created WPFDemo.Api project actually does not add a dependency on WPF. In the application, there are startup tasks that need to be executed by the UI thread, so the definition is completed on the WPFDemo.App with a dependency on WPF
class MainThreadDispatcher : IMainThreadDispatcher
{
public async Task InvokeAsync(Action action)
{
await Application.Current.Dispatcher.InvokeAsync(action);
}
}
After the above basics are completed, you can run the startup framework in the main function of Program.cs, enter the Program type of the WPFDemo.App project, first parse the command line in the main function, and then create the App before running the startup framework.
[STAThread]
static void Main(string[] args)
{
var commandLine = CommandLine.Parse(args);
var app = new App();
//开始启动任务
StartStartupTasks(commandLine);
app.Run();
}
Use Task.Run in the StartStartupTasks method to start the framework when the background thread runs, so that the main thread, which is the UI thread of this application, can start running. Interface related logic
private static void StartStartupTasks(CommandLine commandLine)
{
Task.Run(() =>
{
// 1. 读取应用配置
// 应用将会根据配置决定启动的行为
var configFilePath = "App.coin";
var repo = ConfigurationFactory.FromFile(configFilePath);
// 2. 对接预编译模块,获取启动任务项
var assemblyMetadataExporter = new AssemblyMetadataExporter(BuildStartupAssemblies());
// 3. 创建启动框架和跑起来
var startupManager = new StartupManager(commandLine, repo, HandleShutdownError, new MainThreadDispatcher())
// 3.1 导入预设的应用启动节点,这是必要的步骤,业务方的各个启动任务项将会根据此决定启动顺序
.UseCriticalNodes
(
StartupNodes.Foundation,
StartupNodes.CoreUI,
StartupNodes.UI,
StartupNodes.AppReady,
StartupNodes.StartupCompleted
)
// 3.2 导出程序集的启动项
.AddStartupTaskMetadataCollector(() =>
// 这是预编译模块收集的应用的所有的启动任务项
assemblyMetadataExporter.ExportStartupTasks());
startupManager.Run();
});
}
以上的例子应用里面,有业务是需要根据配置决定启动过程,因此需要先读取应用配置。应用配置选取 dotnetCampus.Configurations 高性能配置文件库 可以极大减少因为读取配置而占用太多启动时间。以上的例子里,还对接了预编译模块。预编译模块的功能是收集应用里的所有启动任务项,如此可以极大提升收集启动任务项的耗时,也不需要让上层业务开发者需要手工注册启动任务项
The above code enables you to run and start the framework after the Main function is started. However, the compilation of the above code cannot pass yet, because the logic of AssemblyMetadataExporter has not been completed, and the related logic of this pre-compiled module has not yet been completed.
This is not equivalent to the fact that this startup framework strongly relies on pre-compiled modules, but rather that pre-compiled modules can be optionally accessed. It only needs to have any logic to interface with the AddStartupTaskMetadataCollector method, and in this method you can pass in the startup task items required to obtain the application. No matter what method is used, including reflection, etc., it is okay. Connecting the pre-compilation module is only to optimize performance and reduce the time spent collecting startup task items
The next step is the access logic of the pre-compiled module. This article does not cover the principle of Telescope's pre-compiled module, but only contains methods on how to access it.
和 .NET 的其他库一样,为了接入预编译模块,就需要先安装 NuGet 库。通过 NuGet 安装 dotnetCampus.Telescope 库,如果是新 SDK 风格的项目文件,可以编辑 csproj 项目文件,添加如下代码安装
<ItemGroup>
<PackageReference Include="dotnetCampus.TelescopeSource" Version="1.0.0-alpha02" />
</ItemGroup>
不同于其他的库,由于 dotnetCampus.Telescope 预编译框架是对项目代码本身进行处理的,需要每个用到预编译都安装此库,因此需要为以上三个项目都安装,而不能靠引用依赖自动安装
After the installation is complete, create a new AssemblyInfo.cs file on the project to add features to the assembly. As agreed, you need to place the AssemblyInfo.cs file into the Properties folder. This Properties folder is a special folder. When you create it in Visual Studio, you can see that the icon of this folder is different from other folders.
Add the following code to the AssemblyInfo.cs file
[assembly: dotnetCampus.Telescope.MarkExport(typeof(WPFDemo.Api.StartupTaskFramework.StartupTask), typeof(dotnetCampus.ApplicationStartupManager.StartupTaskAttribute))]
以上就是对接预编译框架的代码,十分简单。通过给程序集加上 dotnetCampus.Telescope.MarkExportAttribute 可以标记程序集的导出预编译的类型,传入的两个参数分别是导出的类型的基类型以及所继承的特性
以上代码表示导出所有继承 WPFDemo.Api.StartupTaskFramework.StartupTask 类型,且标记了 dotnetCampus.ApplicationStartupManager.StartupTaskAttribute 特性的类型
After marking, rebuild the code, and you will find the AttributedTypesExport.g.cs generation file in the obj folder. For example, in the example project in this article, the path to the generation file is as follows
C:\lindexi\Code\ApplicationStartupManager\demo\WPFDemo\WPFDemo.Api\obj\Debug\net6.0\TelescopeSource.GeneratedCodes\AttributedTypesExport.g.cs
Suppose you have a startup task item called Foo1Startup defined as follows
[StartupTask(BeforeTasks = StartupNodes.CoreUI, AfterTasks = StartupNodes.Foundation)]
public class Foo1Startup : StartupTask
{
protected override Task RunAsync(StartupContext context)
{
context.Logger.LogInfo("Foo1 Startup");
return base.RunAsync(context);
}
}
Then the generated AttributedTypesExport.g.cs will contain the following code
using dotnetCampus.ApplicationStartupManager;
using dotnetCampus.Telescope;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using WPFDemo.Api.StartupTaskFramework;
namespace dotnetCampus.Telescope
{
public partial class __AttributedTypesExport__ : ICompileTimeAttributedTypesExporter<StartupTask, StartupTaskAttribute>
{
AttributedTypeMetadata<StartupTask, StartupTaskAttribute>[] ICompileTimeAttributedTypesExporter<StartupTask, StartupTaskAttribute>.ExportAttributeTypes()
{
return new AttributedTypeMetadata<StartupTask, StartupTaskAttribute>[]
{
new AttributedTypeMetadata<StartupTask, StartupTaskAttribute>(
typeof(WPFDemo.Api.Startup.Foo1Startup),
new StartupTaskAttribute()
{
BeforeTasks = StartupNodes.CoreUI,
AfterTasks = StartupNodes.Foundation
},
() => new WPFDemo.Api.Startup.Foo1Startup()
),
};
}
}
}
That is, the startup items in the assembly are automatically collected and the collected code is generated.
可以在启动框架模块里面,新建一个叫 AssemblyMetadataExporter 的类型来从 AttributedTypesExport.g.cs 拿到收集的类型。从 Telescope 拿到 __AttributedTypesExport__ 生成类型的方法是调用 AttributedTypes 的 FromAssembly 方法,代码如下
IEnumerable<AttributedTypeMetadata<StartupTask, StartupTaskAttribute>> collection = AttributedTypes.FromAssembly<StartupTask, StartupTaskAttribute>(_assemblies);
以上代码传入的 _assemblies 参数就是需要获取收集的启动任务项程序集列表,调用以上代码,将会从传入的各个程序集里获取预编译收集的类型
Wrap this collected return value into StartupTaskMetadata and return it to the startup framework
using System.Reflection;
using dotnetCampus.ApplicationStartupManager;
using dotnetCampus.Telescope;
namespace WPFDemo.Api.StartupTaskFramework
{
public class AssemblyMetadataExporter
{
public AssemblyMetadataExporter(Assembly[] assemblies)
{
_assemblies = assemblies;
}
public IEnumerable<StartupTaskMetadata> ExportStartupTasks()
{
var collection = Export<StartupTask, StartupTaskAttribute>();
return collection.Select(x => new StartupTaskMetadata(x.RealType.Name.Replace("Startup", ""), x.CreateInstance)
{
Scheduler = x.Attribute.Scheduler,
BeforeTasks = x.Attribute.BeforeTasks,
AfterTasks = x.Attribute.AfterTasks,
//Categories = x.Attribute.Categories,
CriticalLevel = x.Attribute.CriticalLevel,
});
}
public IEnumerable<AttributedTypeMetadata<TBaseClassOrInterface, TAttribute>> Export<TBaseClassOrInterface, TAttribute>() where TAttribute : Attribute
{
return AttributedTypes.FromAssembly<TBaseClassOrInterface, TAttribute>(_assemblies);
}
private readonly Assembly[] _assemblies;
}
}
Go back to Program.cs and create a new BuildStartupAssemblies method. In this method, specify the assembly list that needs to collect startup task items, and hand it to AssemblyMetadataExporter to obtain it.
class Program
{
private static void StartStartupTasks(CommandLine commandLine)
{
Task.Run(() =>
{
var assemblyMetadataExporter = new AssemblyMetadataExporter(BuildStartupAssemblies());
// 忽略其他逻辑
});
}
private static Assembly[] BuildStartupAssemblies()
{
// 初始化预编译收集的所有模块。
return new Assembly[]
{
// WPFDemo.App
typeof(Program).Assembly,
// WPFDemo.Lib1
typeof(Foo2Startup).Assembly,
// WPFDemo.Api
typeof(Foo1Startup).Assembly,
};
}
}
Add exported startup task items to the startup framework via StartupManager's AddStartupTaskMetadataCollector
var assemblyMetadataExporter = new AssemblyMetadataExporter(BuildStartupAssemblies());
var startupManager = new StartupManager(/*忽略代码*/)
// 导出程序集的启动项
.AddStartupTaskMetadataCollector(() => assemblyMetadataExporter.ExportStartupTasks());
startupManager.Run();
In this way, all application startup framework configuration logic can be completed, and the next step is to write the startup logic for each business module
Demonstrate how to use the startup framework by adding startup task items for each business module
Add MainWindowStartup to the WPFDemo.Appto start the main window, and the code is as follows
using System.Threading.Tasks;
using dotnetCampus.ApplicationStartupManager;
using WPFDemo.Api.StartupTaskFramework;
namespace WPFDemo.App.Startup
{
[StartupTask(BeforeTasks = StartupNodes.AppReady, AfterTasks = StartupNodes.UI, Scheduler = StartupScheduler.UIOnly)]
internal class MainWindowStartup : StartupTask
{
protected override Task RunAsync(StartupContext context)
{
var mainWindow = new MainWindow();
mainWindow.Show();
return CompletedTask;
}
}
}
The above code uses the StartupTask feature to mark that the startup task item needs to be executed before AppReady and after the UI, and requires it to be scheduled to the main thread for execution. For the main window display, it is naturally necessary to wait for the execution of other UI-related logic, such as ViewModel registration and style dictionary initialization, to be displayed. The AppReady application can only be considered completed after the main window is ready, so the launch task items can be arranged like this
Next, add a business-related start-up task item, and add BusinessStartup to implement the business. The business requires a button to be added in the main interface. Therefore, if required, BusinessStartup needs to be started after MainWindowStartup execution is completed, and the code is as follows
[StartupTask(BeforeTasks = StartupNodes.AppReady, AfterTasks = "MainWindowStartup", Scheduler = StartupScheduler.UIOnly)]
internal class BusinessStartup : StartupTask
{
protected override Task RunAsync(StartupContext context)
{
if (Application.Current.MainWindow.Content is Grid grid)
{
grid.Children.Add(new Button()
{
HorizontalAlignment = HorizontalAlignment.Center,
VerticalAlignment = VerticalAlignment.Bottom,
Margin = new Thickness(10, 10, 10, 10),
Content = "Click"
});
}
return CompletedTask;
}
}
可以看到,在 BusinessStartup 里,通过 AfterTasks 设置了 MainWindowStartup 字符串,也就表示了需要在 MainWindowStartup 执行完成之后才能执行
In addition, dependencies can span multiple projects. For example, LibStartup with the WPFDemo.Lib1 assembly in the infrastructure represents the initialization of a component. This component belongs to the infrastructure and is specified to be launched at the Foundation's default startup node through BeforeTasks
[StartupTask(BeforeTasks = StartupNodes.Foundation)]
class LibStartup : StartupTask
{
protected override Task RunAsync(StartupContext context)
{
context.Logger.LogInfo("Lib Startup");
return base.RunAsync(context);
}
}
As you can see above, in the design of this framework, RunAsync of type StartupTask is given as a virtual method to facilitate synchronization logic during business docking, and you can return the Task object by calling the base class method
The above code only marks BeforeTasks but not AfterTasks, so AfterTasks will be assigned as virtual launch points by default, that is, there is no need to wait for other launch items
In the WPFDemo.Api assembly, there is an OptionStartup that expresses the logic to execute based on the command line. This is also an infrastructure, but depends on the execution of LibStartup. The code is as follows
[StartupTask(BeforeTasks = StartupNodes.Foundation, AfterTasks = "LibStartup")]
class OptionStartup : StartupTask
{
protected override Task RunAsync(StartupContext context)
{
context.Logger.LogInfo("Command " + context.CommandLineOptions.Name);
return CompletedTask;
}
}
This allows OptionStartup to execute after LibStartup and before Foundation
The startup diagram of the above code is as follows. LibStartup and OptionStartup do not require that they must be executed in the UI thread. By default, they are scheduled to be executed in the thread pool.

In both BeforeTasks and AfterTasks, multiple different startup item lists can be passed in, separated by semi-colons. You can also use arrays using BeforeTaskList and AfterTaskList, for example, Foo1Startup with the WPFDemo.Api assembly and Foo2Startup and Foo3Startup startup task items in WPFDemo.Lib1. Foo3Startup needs to rely on the execution of Foo1Startup and Foo2Startup. You can use the following code
[StartupTask(BeforeTasks = StartupNodes.CoreUI, AfterTaskList = new[] { nameof(WPFDemo.Lib1.Startup.Foo2Startup), "Foo1Startup" })]
public class Foo3Startup : StartupTask
{
protected override Task RunAsync(StartupContext context)
{
context.Logger.LogInfo("Foo3 Startup");
return base.RunAsync(context);
}
}
以上就是应用接入 ApplicationStartupManager 启动流程框架的方法,以及业务方编写启动任务项的例子。以上的代码放在 https://github.com/dotnet-campus/dotnetCampus.ApplicationStartupManager 的例子项目