Каким образом он находится в рамках скоупа, если это корневой сервис провайдер?
Вот, теперь я, хотя бы, понимаю о чем весь спор.
Нет, сервис провайдер передается в фабрику через замыкание и он используется на всей рекурсии. А так как фабрика зарегана на уровне жизни меньшем, чем синглтон (в моем случае трансиент), провайдер берется из скоупа. В противном случае, через него не удалось бы создать ни один скоупд сервис (будет экзепшн при попытке создать скоупд сервис из рута). А они создаются. Код рабочий и уже в проде.
Предложенное мной решение выглядит элементарным, на первый взгляд. Но уверяю вас, там все устроено сложнее. Посмотрите внимательно на методы расширения, которые регистрируют сервис с его декораторами.
Возможно, мне следовало снабдить код подробными комментариями.
Происходит вот что.
Вы пытаетесь заинжектить сервис.
Создается фабрика, которая резольвит верхний декоратор. В эту фабрику передается сервис провайдер, созданный коркой из скоупа. Далее этот сервис провайдер передается везде по замыканию. Эта фабрика так же создает фабрику, которая зарезольвит следующий уровень декоратора (либо сам сервис), положит эту фабрику в NextDelegate и вернет этот декоратор. Каждая последующая фабрика по рекурсии сделает то же самое.
Предположим, у нас такая расстановка. Декоратор синглтон, декоратор скопд, сервис трансиент.
Вы инжектите в своем коде сервис. Отрабатывает фабрика и возвразщает вам синглтон декоратор, которому назначает фабрику на следующий уровень.
После этого вы выполняете фабрику. Она (тк сервис провайдер находится в рамках скоупа) создает новый экземпляр скопд декоратора и назначает ему фабрику для следующего уровня (это уже трансиент сервис).
В скопд декораторе вы выполняете делегат и получаете трансиент сервис.
Далее, вы в рамках того же скоупа снова инжектите сервис.
Вновь создается фабрика (она ведь трансиент) и резольвит верхний декоратор. Так как он синглтон, то снова не создается (лайфтайм саксесс :)).
Далее эта фабрика назначает ему фабрику следующего уровня. Хоть она уже и была назначена ранее, это ни на что не повлияет.
Вы выполняете делегат. Делегат, используя сервис провайдер, резольвит следующий уровень. Но он ведь скоупд и в рамках данного скоупа (запроса) уже создан. Возвращается тот же самый экземпляр, что был в предыдущей итерации. Но мы этого и хотели добиться, зарегистрировав его как скоупд (лайфтайм саксесс :)).
В этом декораторе мы фабрикой уже резольвим сам сервис, но он трансиент, он создается по новой (лайфтайм саксесс :)).
Далее, в следующем запросе, а значит в новом скоупе, мы получим новый экземпляр скопд декоратора.
Если ваш сервис — синглтон, то IServiceProvider в этой фабрике не будет привязан ни к одному скоупу.
Почему это? Сам синглтон будет создан той же самой фабрикой, что и скопд.
А значит, ваш Scoped-сервис также будет иметь время жизни как синглтон.
Нет, я же специально для этого сделал делегат-фабрику для сервиса вместо просто сервиса. Более того, сама эта фабрика Transient. В описанном мной коде это видно.
Что вы вкладываете в понятие «он внутри него выполняется»? Скоуп — это объект, делегат не может выполняться внутри объекта.
Делегат тоже объект, но это объект, который выполняется. Бредово звучит, согласен. :)
У нас ООП, поэтому у нас все является объектами. Транзакция тоже объект, например, который описывает операцию, в рамках которой что-то выполняется. Скоуп, кстати — «рамки» в переводе с инглиша. Я в этом смысле.
Если у делегата есть ссылка на объект-скоуп — то он может создать заресолвить сервис в этой скоупе, это я и называю «знанием про скоуп». Если у делегата нет ссылки на скоуп, то всё что он может сделать — это создать новый скоуп, но в этом случае возникает проблема освобождения скоупа.
Тут все немного сложнее. Фабрика для резольва сервиса выглядит как Func<IServiceProvider, TService>. Вопрос, откуда берется IServiceProvider, который корка передает в эту фабрику. Ответ — из скоупа. А откуда берется скоуп? Корка создает его на основе рута, перед резольвом контейнера.
Отсюда ответ на следующий вопрос.
Так всё-таки, у вашего делегата ссылка на скоуп есть или её нет? Если есть — то откуда? Если нет — то кто вызывает Dispose?
Она мне не нужна.
А контейнер откуда знает, когда его диспозить?
Так я же время жизни задаю. Все что Scoped и ниже, диспозится вместе со скопом, синглтоны диспозятся вместе с контейнером.
Но, если я сохраню результат NextDelegate() в переменную в синглтоне и буду использовать его, то вот тут будут проблемы. Поэтому я и добавил базовый класс декоратора, в котором NextDelegate выполняется каждый раз при попытке получить следующий сервис. Остальное на совести разработчика. Но даже если ему не получится объяснить почему, для начала можно просто предложить запомнить.
Мы как будто на разных языках говорим :)
Делегат не знает про скоуп, он внутри него выполняется. И внутри него вызывает GetService()
Скоуп создается перед вызовом метода контроллера и диспозится после его окончания. Все что тянется из контейнера, зареганное как скоуп, тянется из этого скоупа. И вот этот скоуп как раз ничего не знает про то место, откуда он используется. Из синглтона или еще откуда.
// Не работает :-(
Так ясное дело оно не работает, вы диспозите объект, который хотите вернуть. А зачем? В мой делегат он попадает из контейнера. Контейнер его создает, он же и диспозит, по окончании времени жизни.
Вот его-то, созданный коркой во время запроса скоуп, вы и теряете в синглтоне.
Не. Там же делегат, который является фабрикой и возвращает следующий экземпляр. И он все еще выполняется в скоупе. Вы воспроизвели то, о чем сказали? Если да, стоит подумать над решением. Если нет, я завтра могу попробовать либо найти использование, которое уже работает. Но навскидку проблем не вижу.
Тот, кто создал IDisposable, не знает в какой момент он перестанет быть вам нужен. У потребителя должен быть способ уведомить об этом поставщика, и ваш NextDelegate этого способа не предоставляет.
С тех пор как они управляют деньгами. Я либо в ограниченном бюджете и сам не полезу в глубокий рефактор, если нет на это средств, или я знаю, что есть возможность расширить бюджет и иду к мэнеджеру с предложением, описываю проблему, предлагаю решения, менеджер выбирает.
Если речь идет о скоупе запроса — то ваше решение содержит ошибку, поскольку в ASP.NET Core нет способа «восстановить» потеряный скоуп.
Не совсем понял. Я не пытаюсь искать никакой скоуп. Корка создает его во время запроса.
Если же декоратор сам волен создавать скоуп — то ваш вызов NextDelegate() всё равно содержит ошибку, потому что кто вызывать Dispose() скоупу будет?
И тут не понял. Вызывает диспоз в 99% случаев тот, кто создает IDisposable(). Если кто-то в декораторе решит поиграться со скопами на свой страх и риск для каких-то нетривиальных задач, то он же его и задиспозит. В общем я не понимаю описанную проблему.
Если вам понадобилось декорировать Scoped синглтоном — у вас что-то сильно не так с архитектурой. И лучше бы сначала это «что-то» исправить, а уже потом обходные пути искать.
Тут я не согласен, это больше похоже на предположени основанное на техническом ограничении инжекта сервиса с меньшим временем жизни в большее. Но это именно техническое ограничение, а не логическое. Да и что лучше сделать сначала решать менеджерам. Скорее всего, они выберут «че побыстрее».
Ну, например, сервис использует DbContext, который по умолчанию Scoped. Его можно зарегать и с другим временем жизни, но по ряду причин это делать не следует. Получается, ваш сервис уже не может быть синглтоном. А зачем вы хотели синглтон? Ну, скажем, ваш сервис долго инициализируется (какой нибудь вспомогательный функционал, который сложно вынести наружу). И вот, у вас появились декораторы. Вы выносите этот вспомогательный функционал в декоратор, и вам контекст базы ни к чему. Так почему бы не зарегать его (декоратор) синглтоном? У вас запрос ускорится с 2 секунд до 100 мс (цифры с потолка, есессно). Вы бы этого хотели?
пока я не увидел кейсов, где стандартными средствами (пускай и с некоторым бойлерплейтом) нельзя решить ту или иную проблему
Ну например, у меня ASP NET Core приложение и я использую другую IoC реализацию, потому что она умеет то, что не умеют другие. И если я перейду на автофак ради декораторов, потеряю другие плюшки. Использовать 2 различные реализации, ну такое себе, к тому же их еще надо подружить. Мое же решение использует базовые абстракции, на которых построен DI в ASP NET Core и оно не зависит от реализации под капотом. Как вам кейс?
ну моки всяких там логгеров можно просто заранее определить. Хоть в статике. И использовать их. Я же говорю про случаи, когда ваш конструктор принимает 10 аргументов, но в тестируемом методе используется только один. Иди еще разберись, нужен ли именно этот сервис в тестируемом методе. Это тот случай, когда сервис нужно декомпозировать. Но как декомпозировать сервис, который помимо полезной нагрузки должен еще чего то куда то записывать, открывать закрывать транзакции, отправлять СМС и майнить криптовалюту? Ответ тут только один. Он и не должен. Но вот разгрузить его бывает проблематично.
Круто. Сразу умеет декорировать на стороне, но я все еще не понял, как задать время жизни декораторов. Похоже, мы с авторами предложенных выше библиотек преследовали разные цели.
К моему великому стыду, я не знал, что автофак умеет декораторы. Но исполнение мне не очень нравится.
Во-первых, мне не нравится, что в подходе с декораторами приходится плодить столько классов. Возникают вопросы, как лучше организовать структуру проекта, чтобы не утонуть в этом. А тут еще и прокси добавляются.
Во-вторых, инъекция через конструктор не позволяет управлять временем жизни. Предложенным прокси вы оборачиваете синглтон в трансиент, но не сможете обернуть трансиент в синглтон. В моем же инструменте это возможно. Более того, он и разрабатывался так, чтобы обойти это ограничение. Именно поэтому у меня NextDelegate вместо Next.
Бывает, сроки поджимают. Бывает, что-то закрываем быстрее. Тогда появляется время закрывать техдолг. Это как кредит у банка. В конечном счете переплачиваешь, но если все продумать, профит должен перевесить.
К сожалению в данном проекте мы уже ее не используем, так как стек утвержден. Но будем иметь вас ввиду. Даже от ручного построения SQL для работы с jsonb отказались в пользу возможностей EF
Вот, теперь я, хотя бы, понимаю о чем весь спор.
Нет, сервис провайдер передается в фабрику через замыкание и он используется на всей рекурсии. А так как фабрика зарегана на уровне жизни меньшем, чем синглтон (в моем случае трансиент), провайдер берется из скоупа. В противном случае, через него не удалось бы создать ни один скоупд сервис (будет экзепшн при попытке создать скоупд сервис из рута). А они создаются. Код рабочий и уже в проде.
Возможно, мне следовало снабдить код подробными комментариями.
Происходит вот что.
Вы пытаетесь заинжектить сервис.
Создается фабрика, которая резольвит верхний декоратор. В эту фабрику передается сервис провайдер, созданный коркой из скоупа. Далее этот сервис провайдер передается везде по замыканию. Эта фабрика так же создает фабрику, которая зарезольвит следующий уровень декоратора (либо сам сервис), положит эту фабрику в NextDelegate и вернет этот декоратор. Каждая последующая фабрика по рекурсии сделает то же самое.
Предположим, у нас такая расстановка. Декоратор синглтон, декоратор скопд, сервис трансиент.
Вы инжектите в своем коде сервис. Отрабатывает фабрика и возвразщает вам синглтон декоратор, которому назначает фабрику на следующий уровень.
После этого вы выполняете фабрику. Она (тк сервис провайдер находится в рамках скоупа) создает новый экземпляр скопд декоратора и назначает ему фабрику для следующего уровня (это уже трансиент сервис).
В скопд декораторе вы выполняете делегат и получаете трансиент сервис.
Далее, вы в рамках того же скоупа снова инжектите сервис.
Вновь создается фабрика (она ведь трансиент) и резольвит верхний декоратор. Так как он синглтон, то снова не создается (лайфтайм саксесс :)).
Далее эта фабрика назначает ему фабрику следующего уровня. Хоть она уже и была назначена ранее, это ни на что не повлияет.
Вы выполняете делегат. Делегат, используя сервис провайдер, резольвит следующий уровень. Но он ведь скоупд и в рамках данного скоупа (запроса) уже создан. Возвращается тот же самый экземпляр, что был в предыдущей итерации. Но мы этого и хотели добиться, зарегистрировав его как скоупд (лайфтайм саксесс :)).
В этом декораторе мы фабрикой уже резольвим сам сервис, но он трансиент, он создается по новой (лайфтайм саксесс :)).
Далее, в следующем запросе, а значит в новом скоупе, мы получим новый экземпляр скопд декоратора.
Нет, я же специально для этого сделал делегат-фабрику для сервиса вместо просто сервиса. Более того, сама эта фабрика Transient. В описанном мной коде это видно.
Вы явно не поняли задумку.
У нас ООП, поэтому у нас все является объектами. Транзакция тоже объект, например, который описывает операцию, в рамках которой что-то выполняется. Скоуп, кстати — «рамки» в переводе с инглиша. Я в этом смысле.
Тут все немного сложнее. Фабрика для резольва сервиса выглядит как Func<IServiceProvider, TService>. Вопрос, откуда берется IServiceProvider, который корка передает в эту фабрику. Ответ — из скоупа. А откуда берется скоуп? Корка создает его на основе рута, перед резольвом контейнера.
Отсюда ответ на следующий вопрос.
Она мне не нужна.
Так я же время жизни задаю. Все что Scoped и ниже, диспозится вместе со скопом, синглтоны диспозятся вместе с контейнером.
Но, если я сохраню результат NextDelegate() в переменную в синглтоне и буду использовать его, то вот тут будут проблемы. Поэтому я и добавил базовый класс декоратора, в котором NextDelegate выполняется каждый раз при попытке получить следующий сервис. Остальное на совести разработчика. Но даже если ему не получится объяснить почему, для начала можно просто предложить запомнить.
Мы как будто на разных языках говорим :)
Делегат не знает про скоуп, он внутри него выполняется. И внутри него вызывает GetService()
Скоуп создается перед вызовом метода контроллера и диспозится после его окончания. Все что тянется из контейнера, зареганное как скоуп, тянется из этого скоупа. И вот этот скоуп как раз ничего не знает про то место, откуда он используется. Из синглтона или еще откуда.
Так ясное дело оно не работает, вы диспозите объект, который хотите вернуть. А зачем? В мой делегат он попадает из контейнера. Контейнер его создает, он же и диспозит, по окончании времени жизни.
Я опять не понял.
С тех пор как они управляют деньгами. Я либо в ограниченном бюджете и сам не полезу в глубокий рефактор, если нет на это средств, или я знаю, что есть возможность расширить бюджет и иду к мэнеджеру с предложением, описываю проблему, предлагаю решения, менеджер выбирает.
И тут не понял. Вызывает диспоз в 99% случаев тот, кто создает IDisposable(). Если кто-то в декораторе решит поиграться со скопами на свой страх и риск для каких-то нетривиальных задач, то он же его и задиспозит. В общем я не понимаю описанную проблему.
Тут я не согласен, это больше похоже на предположени основанное на техническом ограничении инжекта сервиса с меньшим временем жизни в большее. Но это именно техническое ограничение, а не логическое. Да и что лучше сделать сначала решать менеджерам. Скорее всего, они выберут «че побыстрее».
Еще пример, ваш декоратор стейтфул.
Ну например, у меня ASP NET Core приложение и я использую другую IoC реализацию, потому что она умеет то, что не умеют другие. И если я перейду на автофак ради декораторов, потеряю другие плюшки. Использовать 2 различные реализации, ну такое себе, к тому же их еще надо подружить. Мое же решение использует базовые абстракции, на которых построен DI в ASP NET Core и оно не зависит от реализации под капотом. Как вам кейс?
Во-первых, мне не нравится, что в подходе с декораторами приходится плодить столько классов. Возникают вопросы, как лучше организовать структуру проекта, чтобы не утонуть в этом. А тут еще и прокси добавляются.
Во-вторых, инъекция через конструктор не позволяет управлять временем жизни. Предложенным прокси вы оборачиваете синглтон в трансиент, но не сможете обернуть трансиент в синглтон. В моем же инструменте это возможно. Более того, он и разрабатывался так, чтобы обойти это ограничение. Именно поэтому у меня NextDelegate вместо Next.