From the use of efcore context to the in-depth analysis of the life cycle of Di, automatic attribute injection is finally implemented

Time:2020-3-18

Story background

Recently, we are migrating an old project from framework to. Net core 3.0. Efcore + MySQL is selected for data access. If EF is used, it is inevitable to deal with dbcontext. The general usage in core is to create a xxxcontext class that inherits from dbcontext, implement a constructor with dbcontextoptions parameter, call the extension method adddbcontext of iservicecollection in the configureservices method of startup class startup, inject context into di container, and then use the Place gets the instance through the parameter of the constructor. OK, no problem. The official examples are all used in this way. However, it’s not convenient to get the context instance through the constructor, such as initializing some data in attribute or static class, or when the system is started. It’s more like the following scenario:


public class BaseController : Controller
  {
    public BloggingContext _dbContext;
    public BaseController(BloggingContext dbContext)
    {
      _dbContext = dbContext;
    }

    public bool BlogExist(int id)
    {
      return _dbContext.Blogs.Any(x => x.BlogId == id);
    }
  }

  public class BlogsController : BaseController
  {
    public BlogsController(BloggingContext dbContext) : base(dbContext) { }
  }

As can be seen from the above code, any class that wants to inherit basecontroller must write a “redundant” constructor. If there are more parameters, it will be unbearable (even if there is only one parameter, I can’t stand it). So how can I get the database context instance more elegantly? I think of the following ways.

Where does dbcontext come from

1. Go straight NEW

Back to the original, since you want to create an instance, there is no better way than direct new. When there is no Di in the framework, it is almost the same. But the difference in efcore is that dbcontext no longer provides a parameterless constructor, instead, it must pass in a parameter of type dbcontextoptions, which is usually used to configure some context options, such as what type of database connection string to use.


 public BloggingContext(DbContextOptions<BloggingContext> options) : base(options)
    {
    }

By default, we have configured it when registering context in startup, and di container will automatically help us pass in options. If you want to manually new a context, isn’t it necessary to pass it by yourself every time? No, it’s too painful. Is there any way not to pass this parameter? There must be. We can remove the parameter constructor and override the onconfiguring method in dbcontext to configure the database in this method:


  protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
      optionsBuilder.UseSqlite("Filename=./efcoredemo.db");
    }

Even so, there is still a lack of elegance, that is, the connection string is hard coded in the code and cannot be read from the configuration file. Anyway, I can’t stand it. I have to find another solution.

2. Get manually from di container

Now that the context has been registered in the startup class, there is no problem getting the instance from the di container. So I wrote a test code to verify the conjecture:


 var context = app.ApplicationServices.GetService<BloggingContext>();

Unfortunately, an exception was thrown:

The error message is very clear, and the service cannot be obtained from the root provider. I downloaded the source code of Di framework from station g (the address is https://github.com/aspnet/extensions/tree/master/src/dependencyinjection), took the error message for reverse tracking, and found that the exception came from the validateresolution method of callsitevalidator class:


public void ValidateResolution(Type serviceType, IServiceScope scope, IServiceScope rootScope)
    {
      if (ReferenceEquals(scope, rootScope)
        && _scopedServices.TryGetValue(serviceType, out var scopedService))
      {
        if (serviceType == scopedService)
        {
          throw new InvalidOperationException(
            Resources.FormatDirectScopedResolvedFromRootException(serviceType,
              nameof(ServiceLifetime.Scoped).ToLowerInvariant()));
        }

        throw new InvalidOperationException(
          Resources.FormatScopedResolvedFromRootException(
            serviceType,
            scopedService,
            nameof(ServiceLifetime.Scoped).ToLowerInvariant()));
      }
    }

Continue up and see the implementation of getservice method:


internal object GetService(Type serviceType, ServiceProviderEngineScope serviceProviderEngineScope)
    {
      if (_disposed)
      {
        ThrowHelper.ThrowObjectDisposedException();
      }

      var realizedService = RealizedServices.GetOrAdd(serviceType, _createServiceAccessor);
      _callback?.OnResolve(serviceType, serviceProviderEngineScope);
      DependencyInjectionEventSource.Log.ServiceResolved(serviceType);
      return realizedService.Invoke(serviceProviderEngineScope);
    }

It can be seen that “call back” will not be verified when it is empty, so it is assumed that there are parameters to configure it. Replace the trace object with “call back” and continue to scroll up. Find the following method in the core class serviceprovider of Di framework:

internal ServiceProvider(IEnumerable<ServiceDescriptor> serviceDescriptors, ServiceProviderOptions options)
    {
      IServiceProviderEngineCallback callback = null;
      if (options.ValidateScopes)
      {
        callback = this;
        _callSiteValidator = new CallSiteValidator();
      }
      // omission.
    }

It shows that my guess is right. Validation is controlled by validatescopes. In this way, it can be solved by setting validatescopes to false, which is also a common solution on the Internet:


 .UseDefaultServiceProvider(options =>
    {
       options.ValidateScopes = false;
    })

But it is extremely dangerous to do so.

Why is it dangerous? What is root provider? That starts with the life cycle of the native di. We know that the di container is encapsulated as an iserviceprovider object, and services are obtained from here. However, this is not a single object. It has a hierarchical structure. The top layer, the root provider mentioned earlier, can be understood as a di control center only at the system level. In ASP. Net core, the built-in Di has three service modes: singleton, transient and scoped. The singleton service instance is saved in root provider, so it can be a global singleton. The corresponding scoped is saved in a provider, which can ensure that it is a single instance in the provider, while the transient service is created at any time and discarded when it is used up. Therefore, unless a singleton service is obtained in the root provider, a service scope must be specified. This validation is controlled by validatescopes of serviceprovideroptions. By default, the ASP. Net core framework will determine whether the current development environment is available when creating the hostbuilder. This verification will be enabled in the development environment:

So the previous way to turn off validation is wrong. This is because there is only one root provider. If a singleton service references a scope service, it will cause the scope service to become a singleton. Take a closer look at the extension method of registering dbcontext, which actually provides the scope service:

If this happens, the database connection will not be released all the time. As for the consequences, we should all know.

So the previous test code should read as follows:


  using (var serviceScope = app.ApplicationServices.CreateScope())
   {
     var context = serviceScope.ServiceProvider.GetService<BloggingContext>();
   }

There is also a validateonbuild property related to it, that is to say, it will be verified when building iserviceprovider, which can also be reflected in the source code:


if (options.ValidateOnBuild)
      {
        List<Exception> exceptions = null;
        foreach (var serviceDescriptor in serviceDescriptors)
        {
          try
          {
            _engine.ValidateService(serviceDescriptor);
          }
          catch (Exception e)
          {
            exceptions = exceptions ?? new List<Exception>();
            exceptions.Add(e);
          }
        }

        if (exceptions != null)
        {
          throw new AggregateException("Some services are not able to be constructed", exceptions.ToArray());
        }
      }

Because of this, the ASP. Net core creates an independent scope for each request at design time, and the provider of the scope is encapsulated in httpcontext. Requestservices.

[episode]

As can be seen from the code prompt, iserviceprovider provides two ways to obtain services:

What’s the difference between the two? Looking at the respective method summaries, we can see that getting a service without registration through getservice will return null, while getrequiredservice will throw an invalidoperationexception, that’s all.

//Return result:
    //   A service object of type T or null if there is no such service.
    public static T GetService<T>(this IServiceProvider provider);

    //Return result:
    //   A service object of type T.
    //
    // exception:
    //  T:System.InvalidOperationException:
    //   There is no service of type T.
    public static T GetRequiredService<T>(this IServiceProvider provider);

Ultimate trick

So far, although a reasonable solution has been found, it is still not elegant enough. Friends who have used other third-party Di frameworks should know that the pleasure of attribute injection is incomparable. Do native Di realize this function? I’m glad to go to G station to search for issue and see such a reply (https://github.com/aspnet/extensions/issues/2406):

The official made it clear that there was no plan to develop attribute injection, there was no way but to rely on themselves.

My idea is probably to create a custom tag (Attribute) to label the attributes that need to be injected, and then write a service activation class to parse the attributes that need to be injected into a given instance and assign values. When a certain type is created, it is also called the activation method in the construction function to achieve attribute injection. Here is a core point to note that when getting an instance from the di container, you must ensure that it is the same scope as the current request, that is to say, you must get the iserviceprovider from the current httpcontext.

Create a custom label first:


  [AttributeUsage(AttributeTargets.Property)]
  public class AutowiredAttribute : Attribute
  {

  }

Methods to resolve attributes:

public void PropertyActivate(object service, IServiceProvider provider)
    {
      var serviceType = service.GetType();
      var properties = serviceType.GetProperties().AsEnumerable().Where(x => x.Name.StartsWith("_"));
      foreach (PropertyInfo property in properties)
      {
        var autowiredAttr = property.GetCustomAttribute<AutowiredAttribute>();
        if (autowiredAttr != null)
        {
          //Get instance from di container
          var innerService = provider.GetService(property.PropertyType);
          if (innerService != null)
          {
            //Recursively solve the problem of service nesting
            PropertyActivate(innerService, provider);
            //Property assignment
            property.SetValue(service, innerService);
          }
        }
      }
    }

Then activate the properties in the controller:


[Autowired]
    public IAccountService _accountService { get; set; }

    public LoginController(IHttpContextAccessor httpContextAccessor)
    {
      var pro = new AutowiredServiceProvider();
      pro.PropertyActivate(this, httpContextAccessor.HttpContext.RequestServices);
    }

In this way, although the function is realized, there are several problems. The first is that the httpcontext property of controllerbase cannot be directly used in the constructor of the controller, so it must be obtained by injecting the ihttpcontextaccessor object. It seems that the problem returns to the origin. The second is that every constructor has to write such a pile of code, which can’t be tolerated. So I wonder if there is any way to do something when the controller is activated? I didn’t consider introducing AOP framework. I feel it’s a bit heavy to introduce AOP for this function. Through online search, it is found that the ASP. Net core framework activation controller is implemented through the icontroleractivator interface, and its default implementation is defaultcontrolleractivator (https://github.com/aspnet/aspnetcore/blob/master/src/mvc/mvc.core/src/controllers/defaultcontrolleractivator. CS):


/// <inheritdoc />
    public object Create(ControllerContext controllerContext)
    {
      if (controllerContext == null)
      {
        throw new ArgumentNullException(nameof(controllerContext));
      }

      if (controllerContext.ActionDescriptor == null)
      {
        throw new ArgumentException(Resources.FormatPropertyOfTypeCannotBeNull(
          nameof(ControllerContext.ActionDescriptor),
          nameof(ControllerContext)));
      }

      var controllerTypeInfo = controllerContext.ActionDescriptor.ControllerTypeInfo;

      if (controllerTypeInfo == null)
      {
        throw new ArgumentException(Resources.FormatPropertyOfTypeCannotBeNull(
          nameof(controllerContext.ActionDescriptor.ControllerTypeInfo),
          nameof(ControllerContext.ActionDescriptor)));
      }

      var serviceProvider = controllerContext.HttpContext.RequestServices;
      return _typeActivatorCache.CreateInstance<object>(serviceProvider, controllerTypeInfo.AsType());
    }

In this way, if I implement a controller activator, I can’t take over the controller activation, so I have the following class:

public class HosControllerActivator : IControllerActivator
  {
    public object Create(ControllerContext actionContext)
    {
      var controllerType = actionContext.ActionDescriptor.ControllerTypeInfo.AsType();
      var instance = actionContext.HttpContext.RequestServices.GetRequiredService(controllerType);
      PropertyActivate(instance, actionContext.HttpContext.RequestServices);
      return instance;
    }

    public virtual void Release(ControllerContext context, object controller)
    {
      if (context == null)
      {
        throw new ArgumentNullException(nameof(context));
      }
      if (controller == null)
      {
        throw new ArgumentNullException(nameof(controller));
      }
      if (controller is IDisposable disposable)
      {
        disposable.Dispose();
      }
    }

    private void PropertyActivate(object service, IServiceProvider provider)
    {
      var serviceType = service.GetType();
      var properties = serviceType.GetProperties().AsEnumerable().Where(x => x.Name.StartsWith("_"));
      foreach (PropertyInfo property in properties)
      {
        var autowiredAttr = property.GetCustomAttribute<AutowiredAttribute>();
        if (autowiredAttr != null)
        {
          //Get instance from di container
          var innerService = provider.GetService(property.PropertyType);
          if (innerService != null)
          {
            //Recursively solve the problem of service nesting
            PropertyActivate(innerService, provider);
            //Property assignment
            property.SetValue(service, innerService);
          }
        }
      }
    }
  }

It should be noted that the controller instance in defaultcontrolleractivator is obtained from typeactivatorcache, while its own activator is obtained from di. Therefore, all the controllers of the system must be registered in di additionally and encapsulated into the following extension methods:

/// <summary>
    ///Custom controller activation and manual registration of all controllers
    /// </summary>
    /// <param name="services"></param>
    /// <param name="obj"></param>
    public static void AddHosControllers(this IServiceCollection services, object obj)
    {
      services.Replace(ServiceDescriptor.Transient<IControllerActivator, HosControllerActivator>());
      var assembly = obj.GetType().GetTypeInfo().Assembly;
      var manager = new ApplicationPartManager();
      manager.ApplicationParts.Add(new AssemblyPart(assembly));
      manager.FeatureProviders.Add(new ControllerFeatureProvider());
      var feature = new ControllerFeature();
      manager.PopulateFeature(feature);
      feature.Controllers.Select(ti => ti.AsType()).ToList().ForEach(t =>
      {
        services.AddTransient(t);
      });
    }

Call in ConfigureServices:


services.AddHosControllers(this);

Here it is, and it is done! It’s good to continue crud.

Ending

The di frameworks that are easy to use on the market are piled up, and it’s easy to integrate into the core. Why bother? No way. That’s the fun of making wheels. The above things have also made a lot of time. There is still room for optimization in attribute injection. Welcome to discuss.

The above is the whole content of this article. I hope it will help you in your study, and I hope you can support developepaer more.