由于依赖注入具有举足轻重的作用,所以《ASP.NET Core 6框架揭秘》的绝大部分章节都会涉及这一主题。本书第3章对.NET原生的依赖注入框架的设计和实现进行了系统的介绍,其中设计一些“鲜为人知”的细节,其中一部分就体现在本篇提供的这几个实例演示上。

[308]构造函数的选择(成功)(源代码

[309]构造函数的选择(失败)(源代码

[310]IDisposable和IAsyncDisposable接口的差异(错误编程)(源代码

[311]IDisposable和IAsyncDisposable接口的差异(错误编程)(源代码

[312]利用ActivatorUtilities提供服务实例(源代码

[313]ActivatorUtilities针对构造函数的“评分”(源代码

[314]ActivatorUtilities针对构造函数的选择(源代码

[315]ActivatorUtilities针对构造函数的选择(源代码

[316]与第三方依赖注入框架Cat的整合(源代码

[308]构造函数的选择(成功)

如果通过指定服务类型调用IServiceProvider对象的GetService方法,它总是会根据提供的服务类型从服务注册列表中找到对应的ServiceDescriptor对象,并根据它来提供所需的服务实例。ServiceDescriptor对象具有三种构建方式,分别对应服务实例三种提供方式。我们既可以提供一个Func<IServiceProvider, object>对象作为工厂来创建对应的服务实例,也可以直接提供一个创建好的服务实例。如果提供的是服务的实现类型,最终提供的服务实例将通过该类型的某个构造函数来创建,那么构造函数是通过什么策略被选择出来的?

如果IServiceProvider对象试图通过调用构造函数的方式来创建服务实例,传入构造函数的所有参数必须先被初始化,所以最终被选择的构造函数必须具备一个基本的条件,那就是IServiceProvider对象能够提供构造函数的所有参数。假设我们定义了如下四个服务接口(IFoo、IBar、IBaz和IQux)和对应的实现类型(Foo、Bar、Baz和Qux)。我们为Qux定义了三个构造函数,参数都定义成服务接口类型。为了确定最终选择哪个构造函数来创建目标服务实例,我们在构造函数执行时在控制台上输出相应的指示性文字。

public interface IFoo {}
public interface IBar {}
public interface IBaz {}
public interface IQux {} public class Foo : IFoo {}
public class Bar : IBar {}
public class Baz : IBaz {}
public class Qux : IQux
{
public Qux(IFoo foo) => Console.WriteLine("Selected constructor: Qux(IFoo)");
public Qux(IFoo foo, IBar bar) => Console.WriteLine("Selected constructor: Qux(IFoo, IBar)");
public Qux(IFoo foo, IBar bar, IBaz baz) => Console.WriteLine("Selected constructor: Qux(IFoo, IBar, IBaz)");
}

我们在如下所示的演示程序创建了一个ServiceCollection对象,并在其中添加针对IFoo、IBar及IQux接口的服务注册,但针对IBaz接口的服务注册并未添加。当利用构建的IServiceProvider来提供针对IQux接口的服务实例时,我们是否能够得到一个Qux对象呢?如果可以,它又是通过执行哪个构造函数创建的呢?

using App;
using Microsoft.Extensions.DependencyInjection; new ServiceCollection()
.AddTransient<IFoo, Foo>()
.AddTransient<IBar, Bar>()
.AddTransient<IQux, Qux>()
.BuildServiceProvider()
.GetServices<IQux>();

对于定义在Qux中的三个构造函数来说, 由于存在针对IFoo和IBar接口的服务注册,所前面两个构造函数的所有参数能够由容器提供,第三个构造函数的bar参数却不能。根据前面介绍的第一个原则(IServiceProvider对象能够提供构造函数的所有参数),Qux的前两个构造函数会成为合法的候选构造函数,那么最终会选择哪一个构造函数呢?在所有合法的候选构造函数列表中,最终被选择的构造函数具有如下特征:所有候选构造函数的参数类型都能在这个构造函数中找到。如果这样的构造函数并不存在,会直接抛出一个InvalidOperationException类型的异常。根据这个原则,Qux的第二个构造函数的参数类型包括IFoo和IBar两个接口,而第一个构造函数只具有一个类型为IFoo的参数,所以最终被选择的是Qux的第二个构造函数,运行实例程序,控制台上产生的输出结果如图1所示。


图1 构造函数的选择策略

[309]构造函数的选择(失败)

我们接下来只为Qux类型定义两个构造函数,它们都具有两个参数,参数类型分别为IFoo & IBar和IBar & IBaz,我们同时将针对IBaz/Baz的服务注册添加到创建的ServiceCollection集合中。

using App;
using Microsoft.Extensions.DependencyInjection; new ServiceCollection()
.AddTransient<IFoo, Foo>()
.AddTransient<IBar, Bar>()
.AddTransient<IBaz, Baz>()
.AddTransient<IQux, Qux>()
.BuildServiceProvider()
.GetServices<IQux>(); public class Qux : IQux
{
public Qux(IFoo foo, IBar bar) {}
public Qux(IBar bar, IBaz baz) {}
}

虽然Qux的两个构造函数的参数都可以由IServiceProvider对象来提供,但是并没有某个构造函数拥有所有候选构造函数的参数类型,所以选择一个最佳的构造函数。运行该程序后会抛出图2所示的InvalidOperationException类型的异常,并提示无法从两个候选的构造函数中选择一个最优的来创建服务实例。


图2 构造函数的选择策略

[310]IDisposable和IAsyncDisposable接口的差异(错误编程)

IServiceProvider对象除了提供所需的服务实例,它还需要负责在其生命周期终结的时候释放它们(如果需要的话)。这里所说的回收释放与 .NET的垃圾回收机制无关,仅仅针对自身类型实现了IDisposable或者IAsyncDisposable接口的服务实例(下面称为Disposable服务实例),具体的释放操作体现为调用它们的Dispose或者DisposeAsync方法。是当IServiceScope对象的Dispose方法被执行的时候,如果待释放服务实例对应的类型仅仅实现了IAsyncDisposable接口,而没有实现IDisposable接口,此时会抛出一个InvalidOperationException异常。

using Microsoft.Extensions.DependencyInjection;

using var scope = new ServiceCollection()
.AddScoped<Fooar>()
.BuildServiceProvider()
.CreateScope();
scope.ServiceProvider.GetRequiredService<Fooar>(); public class Fooar : IAsyncDisposable
{
public ValueTask DisposeAsync() => default;
}

如上面的代码片段所示,以Scoped模式注册的Foobar类型实现了IAsyncDisposable接口。我们在一个创建的服务范围内创建该服务实例之后,如图3所示的InvalidOperationException异常会在服务范围被释放的时候抛出来。

图3 IAsyncDisposable实例按照同步方式释放时抛出的异常

[311]IDisposable和IAsyncDisposable接口的差异(错误编程)

不论采用怎样的生命周期模式,服务实例的释放总是在容器被释放时完成的。容器的释放具有同步和异步两种形式,并由对应的服务范围来决定。以异步方式释放容器可以采用同步的方式释放服务实例,反之则不成立。如果服务类型只实现了IAsyncDisposable接口,意味着我们只能采用异步的方式释放容器,这正是图3-11所示的异常消息试图表达的意思。在这种情况下,我们应该按照如下的方式创建代表异步服务范围的AsyncServiceScope对象,并调用DisposeAsync方法(await using)以异步的方式释放容器。

using Microsoft.Extensions.DependencyInjection;

await using var scope = new ServiceCollection()
.AddScoped<Fooar>()
.BuildServiceProvider()
.CreateAsyncScope();
scope.ServiceProvider.GetRequiredService<Fooar>();

[312]利用ActivatorUtilities提供服务实例

IServiceProvider对象能够提供指定类型服务实例的前提存在对应的服务注册,但是有的时候我们需要利用容器创建一个对应类型不曾注册的实例。一个最为典型的例子是MVC应用针对目标Controller实例的创建,因为Controller类型并未作为依赖服务进行注册。这种情况我们就会使用到ActivatorUtilities这个静态的工具类型。当我们调用定义在ActivatorUtilities类型中的如下这些静态方法根据指定的IServiceProvider对象创建指定服务实例时,虽然不要求针对目标服务被预先注册,但是要求指定的IServiceProvider对象能够提供构造函数中必要的参数。

public static class ActivatorUtilities
{
public static object CreateInstance(IServiceProvider provider, Type instanceType, params object[] parameters);
public static T CreateInstance<T>(IServiceProvider provider, params object[] parameters); public static object GetServiceOrCreateInstance(IServiceProvider provider, Type type);
public static T GetServiceOrCreateInstance<T>(IServiceProvider provider);
}

如下的程序演示了ActivatorUtilities的典型用法。如代码片段所示,Foobar类型的构造函数除了注入Foo和Bar这两个可以由容器提供的对象之外,还包含一个用来初始化Name属性的字符串类型的参数。我们将IServiceProvider对象作为参数调用ActivatorUtilities的CreateInstance<T>方法创建一个Foobar对象,此时构造函数的第一个name参数必须显式指定。

using Microsoft.Extensions.DependencyInjection;
using System.Diagnostics; var serviceProviderr = new ServiceCollection()
.AddSingleton<Foo>()
.AddSingleton<Bar>()
.BuildServiceProvider();
var foobar = ActivatorUtilities.CreateInstance<Foobar>(serviceProviderr, "foobar");
Debug.Assert(foobar.Name == "foobar"); public class Foo { }
public class Bar { }
public class Foobar
{
public string Name { get; }
public Foo Foo { get; }
public Bar Bar { get; } public Foobar(string name, Foo foo, Bar bar)
{
Name = name;
Foo = foo;
Bar = bar;
}
}

[313]ActivatorUtilities针对构造函数的“评分”

当我们调用ActivatorUtilities类型的CreateInstance方法创建指定类型的实例时,它总是会选择选择一个“适合”的构造函数。前面我们详细讨论过依赖注入容器对构造函数的选择策略,那么这里的构造函数又是如何被选择出来的呢?如果目标类型定义了多个候选的公共构造函数,最终哪一个被选择取决于两个因素:显式指定的参数列表和构造函数被定义顺序。具体来说,它会遍历每一个候选的公共构造函数,并针对它们创建具有如下定义的ConstructorMatcher对象,然后将我们显式指定的参数列表作为参数调用其Match方法,该方法返回的数字表示当前构造函数与指定的参数列表的匹配度。值越大,意味着匹配度越高,-1表示完全不匹配。

public static class ActivatorUtilities
{
private struct ConstructorMatcher
{
public ConstructorMatcher(ConstructorInfo constructor);
public int Match(object[] givenParameters);
}
}

ActivatorUtilities最终会选择匹配度不小于零且值最高的那个构造函数。如果多个构造函数同时拥有最高匹配度,遍历的第一个构造函数会被选择。我个人其实不太认可这样的设计,既然匹配度相同,对应的构造函数就应该是平等的,为了避免错误的构造函数被选择,抛出异常可能是更好的选择。

对于根据构造函数创建的ConstructorMatcher对象来说,它的Match方法相当于为候选的构造函数针对当前调用场景打了一个匹配度分值,那么这个得分是如何计算的呢?具体的计算流程基本上体现在图4中。假设构造函数参数类型依次为Foo、Bar和Baz,如果显式指定的参数列表的某一个与这三个类型都不匹配,比如指定了一个Qux对象,并且Qux类型没有继承这三个类型中的任何一个,此时的匹配度得分就是-1。

图4 构造函数针对参数数组的匹配度

如果指定的N个参数都与构造函数的前N个参数匹配得上,那么最终的匹配度得分就是N-1。假设foo、bar和baz分别为代码类型为Foo、Bar和Baz的对象,那么只有三种匹配场景,即提供的参数分别为[foo]、[foo, bar]和[foo,bar, baz],最终的匹配度得分分别为0、1和2。如果指定的参数数组不能满足上述的严格匹配规则,最终的得分就是0。为了验证构造函数匹配规则,我们来做一个简单的示例演示。如下面的代码片段所示,我们定义了一个Foobarbaz类型,它的构造函数的参数类型依次为Foo、Bar和Baz。我们采用了反射的方式创建了针对这个构造函数的ConstructorMatcher对象。对于给出的几种参数序列,我们调用ConstructorMatcher对象的Match方法计算该构造函数与它们的匹配度。

using Microsoft.Extensions.DependencyInjection;
using System.Reflection; var constructor = typeof(Foobarbaz).GetConstructors().Single();
var matcherType = typeof(ActivatorUtilities).GetNestedType("ConstructorMatcher", BindingFlags.NonPublic) ?? throw new InvalidOperationException("It fails to resove ConstructorMatcher type");
var matchMethod = matcherType.GetMethod("Match"); var foo = new Foo();
var bar = new Bar();
var baz = new Baz();
var qux = new Qux(); Console.WriteLine($"[Qux] = {Match(qux)}"); Console.WriteLine($"[Foo] = {Match(foo)}");
Console.WriteLine($"[Foo, Bar] = {Match(foo, bar)}");
Console.WriteLine($"[Foo, Bar, Baz] = {Match(foo, bar, baz)}"); Console.WriteLine($"[Bar, Baz] = {Match(bar, baz)}");
Console.WriteLine($"[Foo, Baz] = {Match(foo, baz)}"); int? Match(params object[] args)
{
var matcher = Activator.CreateInstance(matcherType, constructor);
return (int?)matchMethod?.Invoke(matcher, new object[] { args });
}
public class Foo {}
public class Bar {}
public class Baz {}
public class Qux {} public class Foobarbaz
{
public Foobarbaz(Foo foo, Bar bar, Baz baz) { }
}

演示程序执行之后会在控制台上输出如图5所示的结果。对于第一个测试结果,由于我们指定了一个Qux对象,它与构造函数的任一个参数都不兼容,所以匹配度为-1。接下来的三个参数组合完全符合上述的匹配规则,所以得到的匹配度得分为N-1(0、1和2)。至于其他两个,[Bar, Baz]虽然与构造函数的后两个参数兼容(包括顺序),由于Match方法从第一个参数进行匹配,得分依然是0。最后一个组合[Foo, Baz]由于漏掉一个,同样得零分。


图5 测试同一构造函数针对不同参数组合的匹配度

[314]ActivatorUtilities针对构造函数的选择

我不确定构造函数选择策略在今后的版本中会不会修改,就目前的设计来说,我是不认同的。我觉得这样的选择策略是不严谨的,就上面的演示实例验证的构造函数来说,对于参数组合[Foo, Bar]和[Bar, Foo],以及[Foo, Bar]和[Bar, Baz],我不觉得它们在匹配程度上有什么不同。这样的策略还会带来另一个问题,那就是最终被选择的构造函数不仅仅依赖于指定的参数组合,还决定于候选构造函数在所在类型中被定义的顺序。

using Microsoft.Extensions.DependencyInjection;

var serviceProvider = new ServiceCollection()
.AddSingleton<Foo>()
.AddSingleton<Bar>()
.AddSingleton<Baz>()
.BuildServiceProvider(); ActivatorUtilities.CreateInstance<Foobar>(serviceProvider);
ActivatorUtilities.CreateInstance<BarBaz>(serviceProvider); public class Foo {}
public class Bar {}
public class Baz {} public class Foobar
{
public Foobar(Foo foo) => Console.WriteLine("Foobar(Foo foo)");
public Foobar(Foo foo, Bar bar) => Console.WriteLine("Foobar(Foo foo, Bar bar)");
}
public class BarBaz
{
public BarBaz(Bar bar, Baz baz) => Console.WriteLine("BarBaz(Bar bar, Baz baz)");
public BarBaz(Bar bar) => Console.WriteLine("BarBaz(Bar bar)");
}

以如上的演示程序为例,Foobar和Barbaz都具有两个构造函数,参数数量分别为1和2,不同的是Foobar中包含一个参数的构造函数被放在前面,而Barbaz则将其置于后面。当我们调用ActivatorUtilities的CreateInstance<T>构造函数分别创建Foobar和Barbaz对象的时候,总是第一个构造函数被执行(如图6所示)。这意味着当我们无意中改变了构造函数的定义顺序就会改变应用程序执行的行为,这在我看来是不能接受的。


图6 选择的构造函数与定义顺序有关

[315]ActivatorUtilities针对构造函数的选择

默认的构造函数选择策略过于模糊且不严谨,如果希望ActivatorUtilities选择某个构造函数,我们可以通过在目标构造函数上标注ActivatorUtilitiesConstructorAttribute特性的方式来解决这个问题。就上面这个实例来说,如果我们希望ActivatorUtilities选择FooBar具有两个参数的构造函数,可以按照如下的方式在该构造函数上面标注ActivatorUtilitiesConstructorAttribute特性。

public class Foobar
{
public Foobar(Foo foo) => Console.WriteLine("Foobar(Foo foo)"); [ActivatorUtilitiesConstructor]
public Foobar(Foo foo, Bar bar) => Console.WriteLine("Foobarbaz(Foo foo, Bar bar)");
}

[316]与第三方依赖注入框架Cat的整合

我们在第2章“依赖注入(上)”中创建了一个名为Cat的依赖注入框架,我们接下来就通过上述的方式将它引入到应用中。我们首选创建一个名为CatBuilder的类型作为对应的ContainerBuilder。由于需要涉及针对服务范围的创建,我们在CatBuilder类中定义了如下两个内嵌的私有类型。表示服务范围的ServiceScope对象实际上就是对一个IServiceProvider对象的封装,而ServiceScopeFactory类型为创建它的工厂,它是对一个Cat对象的封装。

public class CatBuilder
{
private class ServiceScope : IServiceScope
{
public ServiceScope(IServiceProvider serviceProvider) => ServiceProvider = serviceProvider;
public IServiceProvider ServiceProvider { get; }
public void Dispose()=> (ServiceProvider as IDisposable)?.Dispose();
} private class ServiceScopeFactory : IServiceScopeFactory
{
private readonly Cat _cat;
public ServiceScopeFactory(Cat cat) => _cat = cat;
public IServiceScope CreateScope() => new ServiceScope(_cat);
}
}

一个CatBuilder对象是对一个Cat对象的封装,它的BuildServiceProvider方法会直接返回这个Cat对象,并将它作为最终构建的依赖注入容器。CatBuilder对象在初始化过程中添加了针对IServiceScopeFactory/ServiceScopeFactory的服务注册。为了实现程序集范围内的批量服务注册,我们为CatBuilder类型定义一个Register方法。

public class CatBuilder
{
private readonly Cat _cat;
public CatBuilder(Cat cat)
{
_cat = cat;
_cat.Register<IServiceScopeFactory>(c => new ServiceScopeFactory(c.CreateChild()), Lifetime.Transient);
}
public IServiceProvider BuildServiceProvider() => _cat;
public CatBuilder Register(Assembly assembly)
{
_cat.Register(assembly);
return this;
}
...
}

如下面的代码片段所示,CatServiceProviderFactory类型实现了IServiceProviderFactory<CatBuilder>接口。在实现的CreateBuilder方法中,我们创建了一个Cat对象,并将IServiceCollection集合包含的服务注册(ServiceDescriptor对象)转换成Cat的服务注册形式(ServiceRegistry对象)。在将转换后的服务注册应用到Cat对象上之后,我们最终利用这个Cat对象创建出返回的CatBuilder对象。在实现的CreateServiceProvider方法中,我们直接返回调用CatBuilder对象的CreateServiceProvider方法得到的IServiceProvider对象。

public class CatServiceProviderFactory : IServiceProviderFactory<CatBuilder>
{
public CatBuilder CreateBuilder(IServiceCollection services)
{
var cat = new Cat();
foreach (var service in services)
{
if (service.ImplementationFactory != null)
{
cat.Register(service.ServiceType, provider => service.ImplementationFactory(provider), service.Lifetime.AsCatLifetime());
}
else if (service.ImplementationInstance != null)
{
cat.Register(service.ServiceType, service.ImplementationInstance);
}
else
{
cat.Register(service.ServiceType, service.ImplementationType, service.Lifetime.AsCatLifetime());
}
}
return new CatBuilder(cat);
}
public IServiceProvider CreateServiceProvider(CatBuilder containerBuilder) => containerBuilder.BuildServiceProvider();
}

对于服务实例的生命周期模式,Cat与 .NET依赖注入框架具有一致的表达,所以在将服务注册从ServiceDescriptor类型转化成ServiceRegistry类型时,我们可以简单的完成两者的转换。具体的转换实现如下所示的AsCatLifetime扩展方法中。

internal static class Extensions
{
public static Lifetime AsCatLifetime(this ServiceLifetime lifetime)
{
return lifetime switch
{
ServiceLifetime.Scoped => Lifetime.Self,
ServiceLifetime.Singleton => Lifetime.Root,
_ => Lifetime.Transient,
};
}
}

我们接下来演示如何利用CatServiceProviderFactory创建作为依赖注入容器的IServiceProvider对象。我们定义了Foo、Bar、Baz和Qux四个类型和它们实现的IFoo、IBar、IBaz与IQux接口。Qux类型上标注了一个MapToAttribute特性,并注册了与对应接口IQux之间的映射。这些类型派生的基类Base实现了IDisposable接口,我们在其构造函数和实现的Dispose方法中输出相应的文本,以确定实例被创建和释放的时机。

public interface IFoo {}
public interface IBar {}
public interface IBaz {}
public interface IQux {}
public interface IFoobar<T1, T2> {}
public class Base : IDisposable
{
public Base() => Console.WriteLine($"Instance of {GetType().Name} is created.");
public void Dispose() => Console.WriteLine($"Instance of {GetType().Name} is disposed.");
} public class Foo : Base, IFoo{ }
public class Bar : Base, IBar{ }
public class Baz : Base, IBaz{ }
[MapTo(typeof(IQux), Lifetime.Root)]
public class Qux : Base, IQux { }
public class Foobar<T1, T2>: IFoobar<T1,T2>
{
public IFoo Foo { get; }
public IBar Bar { get; }
public Foobar(IFoo foo, IBar bar)
{
Foo = foo;
Bar = bar;
}
}

在如下所示的演示程序中,我们先创建了一个ServiceCollection集合,并采用三种不同的生命周期模式分别添加了针对IFoo、IBar和IBaz接口的服务注册。我们接下来根据ServiceCollection集合创建了一个CatServiceProviderFactory工厂,并调用其CreateBuilder方法创建出对应的CatBuilder对象。我们最后调用CatBuilder对象的Register方法完成了针对当前入口程序集的批量服务注册,其目的在于添加针对IQux/Qux的服务注册。

using App;
using Microsoft.Extensions.DependencyInjection; var services = new ServiceCollection()
.AddTransient<IFoo, Foo>()
.AddScoped<IBar>(_ => new Bar())
.AddSingleton<IBaz>(new Baz()); var factory = new CatServiceProviderFactory();
var builder = factory.CreateBuilder(services).Register(typeof(Foo).Assembly);
var container = factory.CreateServiceProvider(builder); GetServices();
GetServices();
Console.WriteLine("\nRoot container is disposed.");
(container as IDisposable)?.Dispose(); void GetServices()
{
using var scope = container.CreateScope();
Console.WriteLine("\nService scope is created.");
var child = scope.ServiceProvider; child.GetService<IFoo>();
child.GetService<IBar>();
child.GetService<IBaz>();
child.GetService<IQux>(); child.GetService<IFoo>();
child.GetService<IBar>();
child.GetService<IBaz>();
child.GetService<IQux>();
Console.WriteLine("\nService scope is disposed.");
}

在调用CatServiceProviderFactory工厂的CreateServiceProvider方法来创建出作为依赖注入容器的IServiceProvider对象之后,我们先后两次调用了本地方法GetServices,后者会利用这个IServiceProvider对象来创建一个服务范围,并利用此服务范围内的IServiceProvider提供两组服务实例。利用CatServiceProviderFactory创建的IServiceProvider对象最终通过调用其Dispose方法进行释放。该程序运行之后在控制台上输出的结果如图7所示,输出结果体现的服务生命周期与演示程序体现的生命周期是完全一致的。

图7 利用CatServiceProviderFactory创建IServiceProvider对象

ASP.NET Core 6框架揭秘实例演示[06]:依赖注入框架设计细节的更多相关文章

  1. ASP.NET Core 6框架揭秘实例演示[07]:文件系统

    ASP.NET Core应用具有很多读取文件的场景,如读取配置文件.静态Web资源文件(如CSS.JavaScript和图片文件等).MVC应用的视图文件,以及直接编译到程序集中的内嵌资源文件.这些文 ...

  2. ASP.NET Core 6框架揭秘实例演示[08]:配置的基本编程模式

    .NET的配置支持多样化的数据源,我们可以采用内存的变量.环境变量.命令行参数.以及各种格式的配置文件作为配置的数据来源.在对配置系统进行系统介绍之前,我们通过几个简单的实例演示一下如何将具有不同来源 ...

  3. ASP.NET Core 6框架揭秘实例演示[09]:配置绑定

    我们倾向于将IConfiguration对象转换成一个具体的对象,以面向对象的方式来使用配置,我们将这个转换过程称为配置绑定.除了将配置树叶子节点配置节的绑定为某种标量对象外,我们还可以直接将一个配置 ...

  4. ASP.NET Core 6框架揭秘实例演示[10]:Options基本编程模式

    依赖注入使我们可以将依赖的功能定义成服务,最终以一种松耦合的形式注入消费该功能的组件或者服务中.除了可以采用依赖注入的形式消费承载某种功能的服务,还可以采用相同的方式消费承载配置数据的Options对 ...

  5. ASP.NET Core 6框架揭秘实例演示[11]:诊断跟踪的几种基本编程方式

    在整个软件开发维护生命周期内,最难的不是如何将软件系统开发出来,而是在系统上线之后及时解决遇到的问题.一个好的程序员能够在系统出现问题之后马上定位错误的根源并找到正确的解决方案,一个更好的程序员能够根 ...

  6. ASP.NET Core 6框架揭秘实例演示[12]:诊断跟踪的进阶用法

    一个好的程序员能够在系统出现问题之后马上定位错误的根源并找到正确的解决方案,一个更好的程序员能够根据当前的运行状态预知未来可能发生的问题,并将问题扼杀在摇篮中.诊断跟踪能够帮助我们有效地纠错和排错&l ...

  7. ASP.NET Core 6框架揭秘实例演示[13]:日志的基本编程模式[上篇]

    <诊断跟踪的几种基本编程方式>介绍了四种常用的诊断日志框架.其实除了微软提供的这些日志框架,还有很多第三方日志框架可供我们选择,比如Log4Net.NLog和Serilog 等.虽然这些框 ...

  8. ASP.NET Core 6框架揭秘实例演示[14]:日志的进阶用法

    为了对各种日志框架进行整合,微软创建了一个用来提供统一的日志编程模式的日志框架.<日志的基本编程模式>以实例演示的方式介绍了日志的基本编程模式,现在我们来补充几种"进阶" ...

  9. ASP.NET Core 6框架揭秘实例演示[15]:针对控制台的日志输出

    针对控制台的ILogger实现类型为ConsoleLogger,对应的ILoggerProvider实现类型为ConsoleLoggerProvider,这两个类型都定义在 NuGet包"M ...

随机推荐

  1. 学习javaScript必知必会(3)~数组(数组创建,for...in遍历,辅助函数,高级函数filter、map、reduce)

    一.数组: 1.js是弱语言,js中的数组定义时:不用指定数据类型.不用功指定数组长度:数组可以存储任何数据类型的数据 2.数组定义的[ ] 的实质: [] = new Array(); {} = n ...

  2. rocketmq实现延迟队列精确到秒级实现方案1-代理实现

    简单的来说,就是rocketmq发送消息到broker的时候,判断是否定时消息, 如果是定时消息,将消息发送到代理服务(这个是一个独立的服务,需要自己开发,定时地把消息发送出去), 当然了消息用什么来 ...

  3. 【Android】安卓四大组件之Activity(一)

    [Android]安卓四大组件之Activity(一) 前言 Activity是Android学习中的一个重要组件,想要对其进行系统的了解可以分为几块内容,这一大章节的内容是有关于activity之间 ...

  4. C#winform控件序列化,反序列化

    using System; using System.Collections.Generic; using System.Drawing; using System.IO; using System. ...

  5. Windows和Linux(Centos7)下安装Nginx

    安装Nginx 这篇记录只不过做了一个简单总结,如果对这块没什么概念的话可以看一下知乎的这篇文章 https://zhuanlan.zhihu.com/p/83890573 window下安装 win ...

  6. Tomcat-给Tomcat添加第三方jar包

    给动态web工程添加额外jar包 1,打开项目结构菜单操作界面,添加一个自己的类库 2,添加你当前类库需要的jar包 3,选择你添加的类库,给哪个模块使用 4,选择Artifacts选项,将类库添加到 ...

  7. 微前端框架 之 single-spa 从入门到精通

    前序 目的 会使用single-spa开发项目,然后打包部署上线 刨析single-spa的源码原理 手写一个自己的single-spa框架 过程 编写示例项目 打包部署 框架源码解读 手写框架 关于 ...

  8. 005 Linux 命令三剑客之-sed

    grep:数据查找定位 awk:数据切片,数据格式化,功能最复杂 sed:数据修改 01 Linux 命令三剑客? 三剑客各有所长,和锅锅一一搞起就是了! sed:擅长数据修改. grep:擅长数据查 ...

  9. python 小兵(8)闭包和装饰器

    闭包"是什么,以及,更重要的是,写"闭包"有什么用处. (个人理解) 1."闭包"是什么 首先给出闭包函数的必要条件: 闭包函数必须返回一个函数对象 ...

  10. sql 同步远程数据库(表)到本地

    一)在同一个数据库服务器上面进行数据表间的数据导入导出: 1. 如果表tb1和tb2的结构是完全一样的,则使用以下的命令就可以将表tb1中的数据导入到表tb2中: insert into db2.tb ...