Pull to refresh

Comments 70

Я пока что не опробовал, но выглядит очень многообещающе! Уже немного задолбало писать эти инжекты по несколько раз =) Спасибо, попробую применить в ближайшее же время.

А как быть с юнит тестами? Для них отдельную реализацию писать надо?
Вообще не совсем понятно в чем экономия, в VS2019 например достаточно один раз написать вручную параметр в конструкторе MyClass(IMyService myService), а потом Ctrl+. на нем, и выбрать Create and assign field 'myService'. Это даже короче и удобнее чем прописывать вручную свойство.

А что с юнит тестами не так? Генератор создаст точно такой же конструктор, который вы бы написали ручками. Это просто синтаксический сахар. Я лишь предложил альтернативу. А то, что удобнее для вас, выбираете вы сами!

Ужас, привет макрос аду из C/C++. Я люблю C# за понятность.
Как новые разработчики в команде или пользователи библиотеки будут разбираться с этим? Просматривать тонны сорцов генераторов кода чтобы понять что вообще происходит и зачем? По мне это плохая практика.
Да, я понимаю, многие, освоившие новую «фишку» языка, хотят всем показать что умеет делать невероятные кульбиты с ней, но ведь кто-то это будет в дальнейшем это поддерживать…

Ну, по мне вот, плохая практика это писать очевидный бойлерплейт. Чтобы его не делать, мы придумываем автоматизации — вроде вот такой с Inject. И если такие автоматизации критично снижают понятность — значит нужно делать их лучше. Но вот чего точно не нужно делать — так это продолжать делать руками то, с чем отлично справляется машина

А как быть если нужна кастомная логика валидации зависимостей в конструкторе?

В таком случае — руками, очевидно же! Хотя можно доработать генератор и придумать своих атрибутов, которые будут задавать логику.
Но тут стоит поднять вопрос — а нормальный ли код мы пишем, что нам потребовалось делать генератор для конструктора?

снижают понятность

Проблема в том, что термин «понятность» очень субъективен. И степень его субъективности всегда повышается с применением таких вот «фишек», которые отступают от общепринятых практик и становятся зависимыми от автора, который их создал.
делать руками то, с чем отлично справляется машина

Автоматизировать нужно до такой степени, до которой сохраняется понятность кода, а то можно придти к такому варианту, что конечный результат будет отлично читаться машиной, а вот другому человеку все останется на уровне «абрыкадыбры», которое проще выкинуть и сделать с нуля, чем попытаться внести правки.

Абсолютно согласен. Вспомним синтаксические сахары: using, lock. Разве они снижают понятность? Нет! Так вот, данный генератор — это такой же синтаксический сахар. И я искренне не могу понять в чем сложность? Мне кажется, что взглянув на такой класс — все интуитивно понятно

это такой же синтаксический сахар

Вот не согласен. using, lock — это все сахар от компилятора, я буду уверен, что возьму любой другой проект и увижу такие инструкции — они будут работать 100% однообразно. А сейчас мы ведем речь о разного рода поделках отдельных людей, без стандартов, без согласований и т.д. Которые могут решать одну и туже задачу, но в разных проектах — накладывать совершенно различные ограничения и приводить к неожиданным результатам.
Предлагаете юзать только стандартную библиотеку без сторонних зависимостей? :) В противном случае, описанные вами проблемы не избежать. И это касается не только данной библиотеки
Предлагаете юзать только стандартную библиотеку

Предлагаю не тащить в прод свои велосипеды, если аналоги уже есть и имеют широкое коммьюнити.
Уже какой раз сталкиваюсь, один навелосипедил и уволился, а потом остальным разгребать его «полет мысли».
Я сказал — MEF или Autofac они не требуют конструктора, инжектят через проперти.

Обычно через проперти инжектят необязательные параметры, а constructor injection — для обязательных. Проперти не заставят всех обеспечить нужные зависимости

Да, именно поэтому такое не пользуется популярностью.

Коллеги, вы мою библиотеку кажется неправильно понимаете. Данная библиотека позиционируется как синтаксический сахар. После присваивания атрибута моментально генерится конструктор в compile time. Поля все так же обязательны, ведь конструктор никуда не девается, он все так же есть, просто за вас его пишет машина. Такое ощущение, что вы не разобрались и делаете выводы. Пожалуйста, ознакомьтесь со статьей ещё раз

конструктор никуда не девается

А какой в этом профит? Цель какая — просто лень писать конструктор разработчику? Ну так упомянутые выше библиотеки MEF и Autofac Тоже не требуют инициализации зависимостей в конструкторе.
Да, достало то, что приходится писать классический конструктор. Я снова не понимаю к чему вы в пример приводите MEF или Autofac. Тут подход разный.
В большинстве приложений с которыми я сталкивался — используются разные DI фреймворки и они позволяют определять зависимости без конструкторов. Зачем тащить и изобретать еще что-то дополнительное, когда это все есть.

Если вы определяете property injection, то это позволяет создать объект и не заполнять эти свойства.


Constructor injection гарантирует что зависимости будут заполнены.


Подход автора позволяет описать constructor injection (т.е. обеспечивания обязательных звисимостей) без написания и чтения boilerplate кода.


См фичу котлина primary constructors, см также proposal для C#

сли вы определяете property injection

Это все определяется на уровне фреймворка DI — в нем можно указать, что сервисы через проперти обязательны для резолвинга. И в случае их остутсвия генерация исключения.
Плюс в подходе через Property можно определять логику в конструкторе, в отличии от обсуждаемой библиотеки.

Они же в рантайме работают, насколько я понимаю

Я обеими руками за то, чтобы писать меньше бойлерплейта. Например, я не перестаю восхищаться атрибутом [Reactive] для ReactiveUI.
Но если рассматривать конкретно этот случай, то мне больше нравится вариант с написанным конструктором — это очевидно, это можно использовать всегда (не всегда все параметры идут через DI, не всегда параметры нужны в «чистом» виде, иногда нужна дополнительная инициализация в конструкторе).
Если говорить о бойлерплейте, то у меня стоит ReSharper. Он позволяет создавать классические конструкторы на основе свойств и полей в несколько команд. Не все его ставят/могут позволить поставить, но по мне, это просто потрясающая вещь.

P.s. Увидел сообщение ниже о недостатках. Но всё равно склоняюсь к мысли, что это тупиковый способ избежать написания бойлерплейта. Есть гораздо проще и надёжнее.

Если нужна дополнительная логика в конструкторе, то очевидно можно написать обычный конструктор. Никто ведь не говорит: «Если ты используешь Inject, не используй конструкторы».


«Не всегда все параметры идут через DI»
Генератор создаст точной такой конструктор, который вы бы написали ручками. Это значит, что вы не ограничиваетесь одним DI. Класс можно проинициализировать самим

То, что Вы сделали классно в качестве примера, Вы молодец, правда :)
Но вот всё равно не могу принять Вашу идею. Если часть конструкторов будет через [Inject], часть написана классически — то это будет не очень хорошо.

«Не всегда все параметры идут через DI»
Да, тут я не прав, подумал о чем-то другом. Единственная проблема, наверное, именно в том, что придётся всё равно писать руками, если потребуется инициализация. Для меня это важно, так как часто работаю с ICommand, которые создаются в конструкторе.
Скажите это джавистам с их Lombock :)

Жаль, что моё чувство прекрасного не даст мне смириться с лишним partial, так бы попробовал.


Кроме того приходит в голову проблема, что на самом деле в конструкторе слишком часто делается что-то кроме присваивания полей.

Более того сгенерировать конструктор может какой-нибудь райдер по нажатии клавиши.
А вот засирать код компайл-тайм атрибутами — ужас.
UFO just landed and posted this here
«partial» — пакость, но с ним ничего сделать нельзя. Ограничения технологии. Но и привыкнуть можно, ничего.

А насчет того, конструктор делает что-то еще, то это можно решить как-то так:
  1. Сделать дефолтный конструктор, оперирущий зависимостями, как будто они уже проинициализированы. Для чистоты концепции сделаем его приватным.
  2. Вызвать конструктор в конце другого конструтора нельзя, но можно скопировать его код и добавить в конце сгенерированного.

Ещё есть StrongInject, который умеет проверять при компиляции, зарегистрирован ли в принципе тип, который можно заинжектить.

Пишем все эти же поля, курсор на имя класса, Ctrl+., создать конструктор, сортируем поля если надо(можно убрать лишние).
Готово.

Целевой проект должен референсить ваш пакет и как анализатор (для генерации) и как библиотеку (чтобы атрибуты были доступны).
Так лучше не делать.
Полный тип атрибута анализатору не нужен, достаточно имени, поэтому атрибут можно тоже сгенерировать в целевом проекте.
Можно попробовать даже ликвидировать все упоминания об инжекции, убрав атрибут из выходной сборки атрибутом [Conditional(«NEVER_BE_TRUE»)] (я не проверял, но в теории должно сработать).
Ну и атрибут разметить надо, указав на что его можно вешать.

Не совсем. Целевой проект референсит пакет как анализатор. Мы не можем ссылаться на внутренние классы анализатора, поэтому пакет анализатора подтягивает за собой ещё один пакет, в котором содержатся атрибуты. На счёт разметки атрибута, вы правы. Не подумал об этом. Спасибо, поправлю

Если есть возможность не втаскивать лишнее с собой, лучше не втаскивать.
Здесь получается что подключая анализатор надо зачем-то втаскивать ещё что-то. Зачем, если можно исключительно вспомогательный атрибут внедрить в целевой проект как internal-тип?
Проверил. Условный атрибут действительно работает.
На самом деле не обязательно добавлять пакет как библиотеку. В дальнейшем планируется дать возможность добавлять свои типы через метод Initialize, а пока можно пользоваться своего рода костылём. Подробнее можете глянуть в видео с DotNext. На ютубе уже доступно. Не знаю можно ли оставлять ссылки, поэтому скажу название и время. «Андрей Дятлов — Source Generators в действии» смотреть примерно с 57 минуты.
Да, смотрел, и именно после этого доклада решил поиграться с генераторами.
Выглядит несколько сомнительно.
За всё, что больше 3х зависимостей надо бы по рукам бить — а при небольшом количестве зависимостей такой сахарок не сильно экономит ни время, ни буковки.
На первый взгляд выглядит как заново изобретенный атрибут FromServices.

P.S. Если не ошибаюсь, то как раз именно до .net core этот атрибут можно было применять в том числе и к пропсам в классе.
Ну как раз прошло 10 лет с выхода MEF, почему бы не переизобрести его.

[Import]
public ICalculator Calculator { get; set; }

[ImportMany]
IEnumerable<Lazy<IOperation, IOperationData>> operations;


Если я правильно помню, mef работает в runtime, примерно также, как и di-контейнер.
Штука автора работает в compile time
работает в runtime

Эм, а как компиляцией определять те же скопы в ASP NET?
Я имел в виду саму концепцию, с точки зрения пользователя. В MEF места инъекций и то что экспортим для инъекций помечаются аттрибутами. При использовании, обычно, нет разницы на стадии compile или runtime происходит магия. Скорость в данной задаче не важна, инъекции обычно происходят разово при инициализации, даже рефлекшн не даст узких мест.
При использовании, обычно, нет разницы на стадии compile или runtime происходит магия

Как раз есть — называется время жизни. Я даже не представляю, как это можно решить в compile time.
Одной из главных фишек контейнеров является гибкая настройка времени жизни объектов которые они инжектят.
Я не представляю как это настроить в compile time.
Коллеги, вы мою библиотеку кажется неправильно понимаете. Данная библиотека позиционируется как синтаксический сахар. После присваивания атрибута моментально генерится конструктор в compile time

Так глядишь шарписты свой Lombok получат. Когда нибудь

Так уже есть)
PostSharp и Fody.
Ну и вот ещё сурс генераторы пришли.

Ну, PostSharp это все же не совсем то и многих полезных вещей из ломбок там нет (поправьте, если я ошибаюсь, но аналогов RequiredArgsConstructor или FieldNameConstants там нет. Да и логгирование, скорее, похоже на логгирование аспектами, чем на ломбоковую инъекцию). А про второй я никогда не слышал. Посмотрю, почитаю. Спасибо :)

С приходом C# 9 появляются records с дефолтными конструкторами
Что позволяет писать меньше кода и не запоминать магических аттрибутов
Контроллеры на их основе выглядят вполне компактно

Пример реализации record-контроллера с внедрением зависимости


На всякий случай код, для попробовать
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.AspNetCore.Mvc;
// ReSharper disable All

namespace WebApplicationTestRecords.Controllers
{
    public interface IMyService {
        string[] GetSummaries();
    }

    public class MyService: IMyService {
        public string[] GetSummaries() => new[] {
            "1111", "22222", "2222", "Cool", "Mild",
            "Warm", "Balmy", "333333", "Sweltering", "Scorching"
        };
    }
    
    [ApiController]
    [Route("[controller]")]
    public record WeatherForecastController(IMyService myService)
    {
        [HttpGet]
        public IEnumerable<WeatherForecast> Get()
        {
            var summaries = myService.GetSummaries();
            
            var rng = new Random();
            return Enumerable.Range(1, 5).Select(index =>
                new WeatherForecast
                {
                    Date = DateTime.Now.AddDays(index),
                    TemperatureC = rng.Next(-20, 55),
                    Summary = summaries[rng.Next(summaries.Length)]
                })
                .ToArray();
        }
    }
}


И не забудь в файле Startup.cs зарегать новый сервис:
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
       services.AddTransient<IMyService, MyService>();
            
      services.AddControllers();

PS: разнобой со скобками на скрине только для компактности

Насколько я помню, рекорды это не только синтаксис для конструктора, но и еще куча возможностей и ограничений. Соответственно, не в каждом случае подойдет

Можно про ограничения подробнее?

А то из того что вижу (линк тут) в результате компиляции record это:
  1. класс реализующий IEquatable<T>
  2. принимает в сгенерированном конструкторе заданные зависимости
  3. умеет «красиво» печатать ToString()
  4. переопределяет операторы [не]равенства
  5. определяет метод деконструкции

В общем — хороший такой класс, для контроллеров или типичных сервисов самое оно, когда нужно избежать конструктора и тела конструктора, имхо

Нельзя наследоваться от классов и классы от записей. Нельзя определить метод Clone самостоятельно.

Спасибо, но т.к. рекорды это просто классы, то проще всего пользоваться ими при наследовании.
А вообще в таких вещах, как контроллеры или сервисы-данных наследование особо сильно лучше не использовать, либо по минимуму, для определения пары базовых свойств

+ они могут быть абстрактными и с виртуальными методами

В отношении метода Clone — искренне надеюсь, что мне не придётся клонировать то, что я внедрял через DI )) Этот метод полезен в рамках DTO, а не управляющих элементов приложения.

С другой стороны, если понадобился свой собственный метод клонирования, то можно сделать метод Clone2'

Ну давайте простой пример: у нас все контроллеры в приложении наследуются от Microsoft.AspNetCore.Mvc.ControllerBase который ни разу не рекорд, как предлагается это решать? Писать свою версию и поддерживать её актуальной?


Рекорды это круто, хотел бы их использовать в таком контексте, но по пока увы

Как решать: не наследоваться от абстрактного класса ControllerBase и использовать аттрибут [ApiController]

Если зачем-то нужен контекст, то через конструктор внедряется IHttpContextAccessor

Интересная проблема всплыла — деконструктор публичный, а дотнет по-дефоту начинает кричать, типа «ай, ты не сказал, какой это http action». Чисто для демо определил и пометил его [NoAction]. но в реальной жизни не хотелось бы такой фигнёй заниматься, конечно. Гляну позже, как красиво это обходить

Заголовок спойлера


Ну это да, но теряются все мелочи и удобства: неявный IUrlHelper, куча всяких Ok/NotFound/… А какой-нибудь TryUpdateModelAsync вообще не вызвать: он в интернал лезет. И ещё остается ModelState который иногда валидируется с использованием сервисов (т.е. не может быть оформлен в виде атрибута)


Ничего смертельного, но не уверен что это перевешивает отсутствие конструктора

Согласен с вами. А учитывая деконструктор быть может никаких удобств и нет

В рекорде публичные свойства, а в статье приватные поля.

Публичные только на чтение. Принципиально это никакого значения не имеет, кмк


Интересная попытка использования генераторов кода. Как по мне наличие необходимости постоянно менять зависимости или поддерживать длинный список из этих самых зависимостей говорит о том что есть потенциальные проблемы с дизайном.

Если бы мне когда-либо пришлось писать контроллеры/DI контейнеры, я бы рекордами делал. Эта жесть с SG только ухудшает читаемость имхо.

Думаю на счёт ухудшения читаемости вы преувеличиваете. Все интуитивно понятно ( возможно только для меня )

Ну например то, что приватные поля добавляют конструктор если есть аттрибут. Аттрибут всегда был некоторым тегом, пометкой, и таким и остался. И "читателю" кода может быть неочевидно, что оказывается кто-то генерит код по этим пометкам.


Ну это конечно мое имхо).

Аттрибуты это какое-то тайное знание, о котором нужно помнить (
Хоть я и сам для автофака делал аттрибут [DI], но я же о нём и забывал
Sign up to leave a comment.

Articles