Комментарии 70
А как быть с юнит тестами? Для них отдельную реализацию писать надо?
Вообще не совсем понятно в чем экономия, в VS2019 например достаточно один раз написать вручную параметр в конструкторе MyClass(IMyService myService), а потом Ctrl+. на нем, и выбрать Create and assign field 'myService'. Это даже короче и удобнее чем прописывать вручную свойство.
Как новые разработчики в команде или пользователи библиотеки будут разбираться с этим? Просматривать тонны сорцов генераторов кода чтобы понять что вообще происходит и зачем? По мне это плохая практика.
Да, я понимаю, многие, освоившие новую «фишку» языка, хотят всем показать что умеет делать невероятные кульбиты с ней, но ведь кто-то это будет в дальнейшем это поддерживать…
Ну, по мне вот, плохая практика это писать очевидный бойлерплейт. Чтобы его не делать, мы придумываем автоматизации — вроде вот такой с Inject. И если такие автоматизации критично снижают понятность — значит нужно делать их лучше. Но вот чего точно не нужно делать — так это продолжать делать руками то, с чем отлично справляется машина
снижают понятность
Проблема в том, что термин «понятность» очень субъективен. И степень его субъективности всегда повышается с применением таких вот «фишек», которые отступают от общепринятых практик и становятся зависимыми от автора, который их создал.
делать руками то, с чем отлично справляется машина
Автоматизировать нужно до такой степени, до которой сохраняется понятность кода, а то можно придти к такому варианту, что конечный результат будет отлично читаться машиной, а вот другому человеку все останется на уровне «абрыкадыбры», которое проще выкинуть и сделать с нуля, чем попытаться внести правки.
Абсолютно согласен. Вспомним синтаксические сахары: using, lock. Разве они снижают понятность? Нет! Так вот, данный генератор — это такой же синтаксический сахар. И я искренне не могу понять в чем сложность? Мне кажется, что взглянув на такой класс — все интуитивно понятно
это такой же синтаксический сахар
Вот не согласен. using, lock — это все сахар от компилятора, я буду уверен, что возьму любой другой проект и увижу такие инструкции — они будут работать 100% однообразно. А сейчас мы ведем речь о разного рода поделках отдельных людей, без стандартов, без согласований и т.д. Которые могут решать одну и туже задачу, но в разных проектах — накладывать совершенно различные ограничения и приводить к неожиданным результатам.
Предлагаете юзать только стандартную библиотеку
Предлагаю не тащить в прод свои велосипеды, если аналоги уже есть и имеют широкое коммьюнити.
Уже какой раз сталкиваюсь, один навелосипедил и уволился, а потом остальным разгребать его «полет мысли».
аналог в студию
Аналог решения из статьи?
Обычно через проперти инжектят необязательные параметры, а constructor injection — для обязательных. Проперти не заставят всех обеспечить нужные зависимости
Коллеги, вы мою библиотеку кажется неправильно понимаете. Данная библиотека позиционируется как синтаксический сахар. После присваивания атрибута моментально генерится конструктор в compile time. Поля все так же обязательны, ведь конструктор никуда не девается, он все так же есть, просто за вас его пишет машина. Такое ощущение, что вы не разобрались и делаете выводы. Пожалуйста, ознакомьтесь со статьей ещё раз
конструктор никуда не девается
А какой в этом профит? Цель какая — просто лень писать конструктор разработчику? Ну так упомянутые выше библиотеки MEF и Autofac Тоже не требуют инициализации зависимостей в конструкторе.
Если вы определяете property injection, то это позволяет создать объект и не заполнять эти свойства.
Constructor injection гарантирует что зависимости будут заполнены.
Подход автора позволяет описать constructor injection (т.е. обеспечивания обязательных звисимостей) без написания и чтения boilerplate кода.
См фичу котлина primary constructors, см также proposal для C#
сли вы определяете property injection
Это все определяется на уровне фреймворка DI — в нем можно указать, что сервисы через проперти обязательны для резолвинга. И в случае их остутсвия генерация исключения.
Плюс в подходе через Property можно определять логику в конструкторе, в отличии от обсуждаемой библиотеки.
Они же в рантайме работают, насколько я понимаю
Но если рассматривать конкретно этот случай, то мне больше нравится вариант с написанным конструктором — это очевидно, это можно использовать всегда (не всегда все параметры идут через DI, не всегда параметры нужны в «чистом» виде, иногда нужна дополнительная инициализация в конструкторе).
Если говорить о бойлерплейте, то у меня стоит ReSharper. Он позволяет создавать классические конструкторы на основе свойств и полей в несколько команд. Не все его ставят/могут позволить поставить, но по мне, это просто потрясающая вещь.
P.s. Увидел сообщение ниже о недостатках. Но всё равно склоняюсь к мысли, что это тупиковый способ избежать написания бойлерплейта. Есть гораздо проще и надёжнее.
Если нужна дополнительная логика в конструкторе, то очевидно можно написать обычный конструктор. Никто ведь не говорит: «Если ты используешь Inject, не используй конструкторы».
«Не всегда все параметры идут через DI»
Генератор создаст точной такой конструктор, который вы бы написали ручками. Это значит, что вы не ограничиваетесь одним DI. Класс можно проинициализировать самим
Но вот всё равно не могу принять Вашу идею. Если часть конструкторов будет через [Inject], часть написана классически — то это будет не очень хорошо.
«Не всегда все параметры идут через DI»
Да, тут я не прав, подумал о чем-то другом. Единственная проблема, наверное, именно в том, что придётся всё равно писать руками, если потребуется инициализация. Для меня это важно, так как часто работаю с ICommand, которые создаются в конструкторе.
Жаль, что моё чувство прекрасного не даст мне смириться с лишним partial, так бы попробовал.
Кроме того приходит в голову проблема, что на самом деле в конструкторе слишком часто делается что-то кроме присваивания полей.
А вот засирать код компайл-тайм атрибутами — ужас.
А насчет того, конструктор делает что-то еще, то это можно решить как-то так:
- Сделать дефолтный конструктор, оперирущий зависимостями, как будто они уже проинициализированы. Для чистоты концепции сделаем его приватным.
- Вызвать конструктор в конце другого конструтора нельзя, но можно скопировать его код и добавить в конце сгенерированного.
Ещё есть StrongInject, который умеет проверять при компиляции, зарегистрирован ли в принципе тип, который можно заинжектить.
Пишем все эти же поля, курсор на имя класса, Ctrl+., создать конструктор, сортируем поля если надо(можно убрать лишние).
Готово.
Так лучше не делать.
Полный тип атрибута анализатору не нужен, достаточно имени, поэтому атрибут можно тоже сгенерировать в целевом проекте.
Можно попробовать даже ликвидировать все упоминания об инжекции, убрав атрибут из выходной сборки атрибутом [Conditional(«NEVER_BE_TRUE»)] (я не проверял, но в теории должно сработать).
Ну и атрибут разметить надо, указав на что его можно вешать.
Не совсем. Целевой проект референсит пакет как анализатор. Мы не можем ссылаться на внутренние классы анализатора, поэтому пакет анализатора подтягивает за собой ещё один пакет, в котором содержатся атрибуты. На счёт разметки атрибута, вы правы. Не подумал об этом. Спасибо, поправлю
Здесь получается что подключая анализатор надо зачем-то втаскивать ещё что-то. Зачем, если можно исключительно вспомогательный атрибут внедрить в целевой проект как internal-тип?
За всё, что больше 3х зависимостей надо бы по рукам бить — а при небольшом количестве зависимостей такой сахарок не сильно экономит ни время, ни буковки.
P.S. Если не ошибаюсь, то как раз именно до .net core этот атрибут можно было применять в том числе и к пропсам в классе.
Штука автора работает в compile time
работает в runtime
Эм, а как компиляцией определять те же скопы в ASP NET?
При использовании, обычно, нет разницы на стадии compile или runtime происходит магия
Как раз есть — называется время жизни. Я даже не представляю, как это можно решить в compile time.
Причем тут время жизни ?
Я не представляю как это настроить в compile time.
Так глядишь шарписты свой Lombok получат. Когда нибудь
Ну, PostSharp это все же не совсем то и многих полезных вещей из ломбок там нет (поправьте, если я ошибаюсь, но аналогов RequiredArgsConstructor или FieldNameConstants там нет. Да и логгирование, скорее, похоже на логгирование аспектами, чем на ломбоковую инъекцию). А про второй я никогда не слышал. Посмотрю, почитаю. Спасибо :)
Что позволяет писать меньше кода и не запоминать магических аттрибутов
Контроллеры на их основе выглядят вполне компактно
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 это:
- класс реализующий IEquatable<T>
- принимает в сгенерированном конструкторе заданные зависимости
- умеет «красиво» печатать ToString()
- переопределяет операторы [не]равенства
- определяет метод деконструкции
В общем — хороший такой класс, для контроллеров или типичных сервисов самое оно, когда нужно избежать конструктора и тела конструктора, имхо
Нельзя наследоваться от классов и классы от записей. Нельзя определить метод Clone самостоятельно.
А вообще в таких вещах, как контроллеры или сервисы-данных наследование особо сильно лучше не использовать, либо по минимуму, для определения пары базовых свойств
В отношении метода Clone — искренне надеюсь, что мне не придётся клонировать то, что я внедрял через DI )) Этот метод полезен в рамках DTO, а не управляющих элементов приложения.
С другой стороны, если понадобился свой собственный метод клонирования, то можно сделать метод Clone2'
Ну давайте простой пример: у нас все контроллеры в приложении наследуются от Microsoft.AspNetCore.Mvc.ControllerBase
который ни разу не рекорд, как предлагается это решать? Писать свою версию и поддерживать её актуальной?
Рекорды это круто, хотел бы их использовать в таком контексте, но по пока увы
ControllerBase
и использовать аттрибут [ApiController]
Если зачем-то нужен контекст, то через конструктор внедряется
IHttpContextAccessor
Интересная проблема всплыла — деконструктор публичный, а дотнет по-дефоту начинает кричать, типа «ай, ты не сказал, какой это http action». Чисто для демо определил и пометил его
[NoAction]
. но в реальной жизни не хотелось бы такой фигнёй заниматься, конечно. Гляну позже, как красиво это обходитьНу это да, но теряются все мелочи и удобства: неявный IUrlHelper, куча всяких Ok/NotFound/… А какой-нибудь TryUpdateModelAsync
вообще не вызвать: он в интернал лезет. И ещё остается ModelState который иногда валидируется с использованием сервисов (т.е. не может быть оформлен в виде атрибута)
Ничего смертельного, но не уверен что это перевешивает отсутствие конструктора
Если бы мне когда-либо пришлось писать контроллеры/DI контейнеры, я бы рекордами делал. Эта жесть с SG только ухудшает читаемость имхо.
Думаю на счёт ухудшения читаемости вы преувеличиваете. Все интуитивно понятно ( возможно только для меня )
Ну например то, что приватные поля добавляют конструктор если есть аттрибут. Аттрибут всегда был некоторым тегом, пометкой, и таким и остался. И "читателю" кода может быть неочевидно, что оказывается кто-то генерит код по этим пометкам.
Ну это конечно мое имхо).
Избавляемся от постоянного написания конструкторов для инжекта зависимостей с помощью C# Source Generators