EFCore 컨 텍스트 의 사용 부터 DI 의 생명 주 기 를 깊이 분석 한 다음 에 자동 속성 주입 을 실현 합 니 다.

이야기 의 배경
최근 에 자신의 오래된 프로젝트 를 Framework 에서 Net Core 3.0 으로 옮 기 고 있 습 니 다.데 이 터 는 이 선택 에 접근 한 것 은 EFCore+Mysql 입 니 다.EF 를 사용 하면 DbContext 와 접촉 할 수 밖 에 없습니다.Core 에서 일반적인 용법 은 다음 과 같 습 니 다.DbContext 클래스 를 DbContext 에서 계승 하여 DbContextOptions 파 라 메 터 를 가 진 구조 기 를 만 들 고 시작 클래스 StartUp 의 Configure Services 방법 에서 IServiceCollection 의 확장 방법 AddDbContext 를 호출 하여 컨 텍스트 를 DI 용기 에 주입 합 니 다.그리고 사용 하 는 곳 에서 구조 함수 의 매개 변 수 를 통 해 인 스 턴 스 를 가 져 옵 니 다.OK,아무 문제 없어 요.공식 예시 도 다 이렇게 써 요.그러나 구조 함수 라 는 방식 으로 문맥 인 스 턴 스 를 얻 는 것 은 매우 불편 하 다.예 를 들 어 Attribute 나 정적 클래스 에서 또는 시스템 이 시 작 될 때 데 이 터 를 초기 화 하 는 것 은 다음 과 같은 장면 이다.

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) { }
  }
위의 코드 를 통 해 알 수 있 듯 이 BaseController 를 계승 하려 는 모든 종 류 는'불필요 한'구조 함 수 를 써 야 합 니 다.만약 에 매개 변수 가 몇 개 더 많 으 면 참 을 수 없습니다(하나의 매개 변수 만 있어 도 참 을 수 없습니다).그러면 어떻게 해야만 데이터베이스 컨 텍스트 인 스 턴 스 를 더욱 우아 하 게 얻 을 수 있 습 니까?저 는 다음 과 같은 몇 가지 방법 을 생각 합 니 다.
DbContext 어디서 왔어요?
1、바로 도망 가기 new
원본 으로 돌아 갑 니 다.인 스 턴 스 를 만 들 려 면 new 보다 더 좋 은 방법 이 없습니다.Framework 에 DI 가 없 을 때 도 그렇게 할 수 있 습 니 다.그러나 EFCore 에서 다른 것 은 DbContext 는 더 이상 무 참 구조 함 수 를 제공 하지 않 습 니 다.대신 DbContextOptions 형식의 매개 변 수 를 입력 해 야 합 니 다.이 매개 변 수 는 보통 컨 텍스트 옵션 설정 을 합 니 다.예 를 들 어 어떤 유형의 데이터 베 이 스 를 사용 하 는 연결 문자열 이 얼마 인지 등 입 니 다.

 public BloggingContext(DbContextOptions<BloggingContext> options) : base(options)
    {
    }
기본 적 인 상황 에서 저 희 는 StartUp 에 컨 텍스트 를 등록 할 때 설정 을 했 습 니 다.DI 용 기 는 자동 으로 options 를 들 려 줍 니 다.new 컨 텍스트 를 수 동 으로 사용 하려 면 매번 스스로 전달 해 야 하 는 것 이 아 닙 니까?안 돼,이 건 너무 아파.그럼 이 인 자 를 전달 하지 않 을 방법 이 있 습 니까?분명히 있 을 거 야.우 리 는 구조 함수 가 있 는 것 을 제거 하고 DbContext 의 OnConfiguring 방법 을 다시 쓸 수 있 습 니 다.이 방법 에서 데이터 베 이 스 를 설정 할 수 있 습 니 다.

  protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
      optionsBuilder.UseSqlite("Filename=./efcoredemo.db");
    }
그럼 에 도 불구 하고 우아 하지 않 은 점 이 있 습 니 다.연결 문자열 이 코드 에 하 드 인 코딩 되 어 설정 파일 에서 읽 을 수 없습니다.어쨌든 나 는 참 을 수 없어 서 다른 방안 을 찾 을 수 밖 에 없다.
2、DI 용기 에서 수 동 으로 가 져 오기
앞에서 시작 클래스 에 컨 텍스트 를 등 록 했 으 니 DI 용기 에서 인 스 턴 스 를 가 져 오 는 것 은 문제 가 없 을 것 입 니 다.그래서 나 는 추측 을 검증 하기 위해 테스트 코드 를 썼 다.

 var context = app.ApplicationServices.GetService<BloggingContext>();
안 타 깝 게 도 이상 을 던 졌 습 니 다.

오류 메 시 지 는 루트 provider 에서 이 서 비 스 를 받 을 수 없다 는 것 이 명확 하 다.저 는 G 역 에서 DI 프레임 워 크 의 소스 코드(주 소 는https://github.com/aspnet/Extensions/tree/master/src/DependencyInjection를 다운 받 았 습 니 다.잘못된 정 보 를 가지 고 역방향 트 레이스 를 했 는데 CallSiteValidator 류 의 Validate Resolution 방법 에서 이상 한 것 을 발 견 했 습 니 다.

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()));
      }
    }
계속 위로 올 라 가면 GetService 방법의 실현 을 볼 수 있 습 니 다.

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);
    }
보 입 니 다.콜백 은 비어 있 는 상태 에서 검증 을 하지 않 습 니 다.그래서 매개 변수 가 설정 할 수 있 을 것 이 라 고 추측 합 니 다.트 레이스 대상 을 로 바꾸다callback 을 계속 위로 넘 기 면 DI 프레임 워 크 의 핵심 클래스 인 ServiceProvider 에서 다음 과 같은 방법 을 찾 을 수 있 습 니 다.

internal ServiceProvider(IEnumerable<ServiceDescriptor> serviceDescriptors, ServiceProviderOptions options)
    {
      IServiceProviderEngineCallback callback = null;
      if (options.ValidateScopes)
      {
        callback = this;
        _callSiteValidator = new CallSiteValidator();
      }
      //  ....
    }
내 추측 이 맞다 는 것 을 말 해 준다.검증 은 Validate Scopes 에 의 해 제 어 된 것 이다.이렇게 보면 ValidateScopes 를 False 로 설정 하면 해결 할 수 있 습 니 다.이것 도 인터넷 에서 보편적 인 해결 방안 입 니 다.

 .UseDefaultServiceProvider(options =>
    {
       options.ValidateScopes = false;
    })
그러나 이렇게 하 는 것 은 매우 위험 하 다.
왜 위험 해?도대체 루트 provider 가 무엇 입 니까?그럼 네 이 티 브 DI 라 이 프 사이클 부터 얘 기해 야 지.우 리 는 DI 용기 가 IServiceProvider 대상 으로 봉 인 된 것 을 알 고 있 으 며,서 비 스 는 모두 여기에서 얻 을 수 있다.그러나 이것 은 단일 한 대상 이 아니 라 등급 구 조 를 가 진 것 이다.가장 꼭대기 층 인 앞에서 언급 한 루트 provider 는 시스템 차원 에 만 속 하 는 DI 제어 센터 로 이해 할 수 있다.Asp.Net Core 에 내 장 된 DI 는 3 가지 서비스 모델 이 있 는데 그것 이 바로 Singleton,Transient,Scoped 이 고 Singleton 서비스 인 스 턴 스 는 루트 provider 에 저장 되 어 있 기 때문에 전체적인 사례 를 만 들 수 있 습 니 다.이에 대응 하 는 Scoped 는 특정한 provider 에 저 장 된 것 입 니 다.이 provider 에 서 는 하나의 예 를 보장 할 수 있 고 Transient 서 비 스 는 수시로 만들어 야 하 며 다 쓰 면 버 립 니 다.이 를 통 해 알 수 있 듯 이 루트 provider 에서 단일 서 비 스 를 받 지 않 으 면 서비스 범위(Scope)를 지정 해 야 합 니 다.이 인증 은 ServiceProvider Options 의 ValidateScopes 를 통 해 제 어 됩 니 다.기본적으로 Asp.Net Core 프레임 워 크 는 HostBuilder 를 만 들 때 현재 개발 환경 여 부 를 판단 하고 개발 환경 에서 이 인증 을 시작 합 니 다.

그래서 앞의 인증 을 닫 는 방식 이 잘못 되 었 습 니 다.이 는 루트 provider 가 하나 밖 에 없 기 때 문 입 니 다.마침 어떤 singleton 서비스 가 scope 서 비 스 를 인용 하면 이 scope 서비스 도 singleton 이 될 수 있 습 니 다.DbContext 를 등록 하 는 확장 방법 을 자세히 살 펴 보면 실제 적 으로 scope 서 비 스 를 제공 합 니 다.

만약 이런 상황 이 발생 한다 면 데이터베이스 연결 이 계속 방출 되 지 않 을 것 이 고 어떤 결과 가 있 는 지 모두 가 알 아야 한다.
그래서 앞의 테스트 코드 는 이렇게 써 야 합 니 다.

  using (var serviceScope = app.ApplicationServices.CreateScope())
   {
     var context = serviceScope.ServiceProvider.GetService<BloggingContext>();
   }
이와 관련 된 ValidateOnBuild 속성 도 있 습 니 다.즉,IServiceProvider 를 구축 할 때 검증 을 하고 소스 코드 에서 도 나타 납 니 다.

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());
        }
      }
그 렇 기 때문에 Asp.Net Core 는 디자인 할 때 모든 요청 에 독립 된 Scope 를 만 들 었 습 니 다.이 Scope 의 provider 는 HttpContext.RequestServices 에 봉인 되 어 있 습 니 다.
[에피소드]
코드 알림 을 통 해 알 수 있 듯 이 IServiceProvider 는 2 가지 서 비 스 를 가 져 오 는 방식 을 제공 합 니 다.

이 두 개 는 어떤 차이 가 있 나 요?각각 방법 요약 을 보면 GetService 를 통 해 등록 되 지 않 은 서 비 스 를 받 을 때 null 로 돌아 가 고 GetRequired Service 는 Invalid Operation Exception 을 던 집 니 다.이것 뿐 입 니 다.

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

    //     :
    //   A service object of type T.
    //
    //   :
    //  T:System.InvalidOperationException:
    //   There is no service of type T.
    public static T GetRequiredService<T>(this IServiceProvider provider);
궁 극적인 방법
지금까지 합 리 적 으로 보 이 는 방안 을 찾 았 음 에 도 불구 하고 우아 하지 못 하 다.다른 제3자 DI 프레임 워 크 를 사용 한 친 구 는 속성 주입 의 쾌감 이 비교 할 수 없다 는 것 을 알 아야 한다.그럼 원생 DI 는 이 기능 을 실 현 했 습 니까?저 는 G 역 에서 Issue 를 찾 는 것 을 기 쁘 게 생각 합 니 다.이런 대답 을 보 았 습 니 다https://github.com/aspnet/Extensions/issues/2406.

정 부 는 개발 속성 주입 계획 이 없고 어 쩔 수 없 이 자신 에 게 의지 할 수 밖 에 없다 고 명 확 히 밝 혔 다.
내 생각 은 아마도:사용자 정의 탭(Attribute)을 만 들 고 주입 해 야 할 속성 에 탭 을 한 다음 에 서비스 활성화 류 를 써 서 주어진 인 스 턴 스 가 주입 해 야 할 속성 을 분석 하고 값 을 부여 하 는 것 입 니 다.특정한 유형 이 인 스 턴 스 를 만 들 때 구조 함수 에서 이 활성화 방법 을 사용 하여 속성 주입 을 실현 하 는 것 입 니 다.여기 서 핵심 적 인 점 은 DI 용기 에서 인 스 턴 스 를 가 져 올 때 현재 요청 과 같은 Scope 라 는 것 을 보증 해 야 한 다 는 것 입 니 다.즉,현재 HttpContext 에서 이 IServiceProvider 를 가 져 와 야 한 다 는 것 입 니 다.
사용자 정의 탭 만 들 기:

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

  }
속성 분석 방법:

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)
        {
          // DI      
          var innerService = provider.GetService(property.PropertyType);
          if (innerService != null)
          {
            //          
            PropertyActivate(innerService, provider);
            //    
            property.SetValue(service, innerService);
          }
        }
      }
    }
그리고 컨트롤 러 에서 속성 활성화:

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

    public LoginController(IHttpContextAccessor httpContextAccessor)
    {
      var pro = new AutowiredServiceProvider();
      pro.PropertyActivate(this, httpContextAccessor.HttpContext.RequestServices);
    }
이렇게 되면 기능 이 실현 되 었 지만 그 안에 몇 가지 문제 가 존재 한다.첫 번 째 는 컨트롤 러 의 구조 함수 에서 Controller Base 의 HttpContext 속성 을 직접 사용 할 수 없 기 때문에 IHttpContextAccessor 대상 을 주입 하여 가 져 와 야 합 니 다.문제 가 다시 원점 으로 돌아 가 는 것 같 습 니 다.두 번 째 는 모든 구조 함수 에 이런 코드 를 써 야 하기 때문에 참 을 수 없다.그래서 컨트롤 러 가 활성화 되 었 을 때 조작 을 할 방법 이 없 을 까?AOP 프레임 워 크 도입 을 고려 하지 않 았 는데,이 를 위해 AOP 를 도입 하 는 것 은 다소 무 거 운 느낌 이 든다.인터넷 검색 을 통 해 Asp.Net 코어 프레임 워 크 활성화 컨트롤 러 는 IControllerActivator 인 터 페 이 스 를 통 해 이 루어 진 것 으로 기본 구현 은 Default Controller Activator(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());
    }
이렇게 되면 저 는 컨트롤 러 활성 기 를 실현 하면 컨트롤 러 를 연결 하여 활성화 할 수 있 지 않 습 니까?그래서 다음 과 같은 종류 가 있 습 니 다.

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)
        {
          // DI      
          var innerService = provider.GetService(property.PropertyType);
          if (innerService != null)
          {
            //          
            PropertyActivate(innerService, provider);
            //    
            property.SetValue(service, innerService);
          }
        }
      }
    }
  }
주의해 야 할 것 은 Default Controller Activator 의 컨트롤 러 인 스 턴 스 는 TypeActivator Cache 에서 가 져 왔 고 자신의 활성 기 는 DI 에서 가 져 왔 기 때문에 시스템 의 모든 컨트롤 러 를 DI 에 추가 로 등록 하고 다음 과 같은 확장 방법 으로 밀봉 해 야 합 니 다.

/// <summary>
    ///         ,          
    /// </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);
      });
    }
Configure Services 에서 호출:

services.AddHosControllers(this);
여기까지,큰 성 과 를 거 두 었 습 니 다!즐겁게 크 루 드 를 계속 할 수 있 게 되 었 습 니 다.
엔 딩
시중 에 쓰기 좋 은 DI 프레임 이 무더기로 쌓 여 있 는데,코어 에 집적 하 는 것 도 간단 한데,왜 이렇게 소란 을 피 우 느 냐?어 쩔 수 없어,이게 바퀴 만 드 는 즐거움 이 잖 아.위 에 있 는 이 물건 들 은 처음부터 끝까지 많은 시간 을 들 였 고 속성 주입 도 그곳 에 최적화 된 공간 이 있 으 니 연 구 를 환영 합 니 다.
이상 이 바로 본 고의 모든 내용 입 니 다.여러분 의 학습 에 도움 이 되 고 저 희 를 많이 응원 해 주 셨 으 면 좋 겠 습 니 다.

좋은 웹페이지 즐겨찾기