Функциональный C#

    C# — язык мультипарадигмальный. В последнее время крен наметился в сторону функциональщины. Можно пойти дальше и добавить еще немного методов-расширений, позволяющих писать меньше кода, не «залезая» при этом на территорию F#.

    PipeTo


    Пока Pipe Operator не собираются включать в следующий релиз. Что-ж, можно обойтись и методом.

    public static TResult PipeTo<TSource, TResult>(
        this TSource source, Func<TSource, TResult> func)
        => func(source);

    Императивный вариант

    public IActionResult Get()
    {
        var someData = query
            .Where(x => x.IsActive)
            .OrderBy(x => x.Id)
            .ToArray();
        return Ok(someData);
    }
    

    С PipeTo

    public IActionResult Get() =>  query
        .Where(x => x.IsActive)
        .OrderBy(x => x.Id)
        .ToArray()
        .PipeTo(Ok);

    Заметили? В первом варианте мне нужно было вернуть взгляд к объявлению переменной и потом перейти к Ok. С PipeTo execution-flow строго слева-направо, сверху-вниз.

    Either


    В реальном мире алгоритмы чаще содержат ветвления, чем бывают линейными:

    public IActionResult Get(int id) =>  query
        .Where(x => x.Id == id)
        .SingleOrDefault()
        .PipeTo(x => x != null ? Ok(x) : new NotFoundResult(“Not Found”));

    Выглядит уже не так хорошо. Исправим это с помощью метода Either:

    public static TOutput Either<TInput, TOutput>(this TInput o, Func<TInput, bool> condition,
        Func<TInput, TOutput> ifTrue, Func<TInput, TOutput> ifFalse)
        => condition(o) ? ifTrue(o) : ifFalse(o);
    
    public IActionResult Get(int id) =>  query
        .Where(x => x.Id == id)
        .SingleOrDefault()
        .Either(x => x != null, Ok, _ => (IActionResult)new NotFoundResult("Not Found"));

    Добавим перегрузку с проверкой на null:

    public static TOutput Either<TInput, TOutput>(this TInput o, Func<TInput, TOutput> ifTrue,
        Func<TInput, TOutput> ifFalse)
        => o.Either(x => x != null, ifTrue, ifFalse);
    
    public IActionResult Get(int id) =>  query
        .Where(x => x.Id == id)
        .SingleOrDefault()
        .Either(Ok, _ => (IActionResult)new NotFoundResult("Not Found"));

    К сожалению вывод типов в C# еще не идеален, поэтому пришлось добавить явный каст к IActionResult.

    Do


    Get-методы контроллеров не должны создавать побочных эффектов, но иногда «очень надо».

    public static T Do<T>(this T obj, Action<T> action)
    {
        if (obj != null)
        {
            action(obj);
        }
    
        return obj;
    }
    
    public IActionResult Get(int id) =>  query
        .Where(x => x.Id == id)
        .Do(x => ViewBag.Title = x.Name)
        .SingleOrDefault()
        .Either(Ok, _ => (IActionResult)new NotFoundResult("Not Found"));

    При такой организации кода побочный эффект с Do обязательно бросится в глаза во время code review. Хотя в целом использование Do — очень спорная идея.

    ById


    Не находите, что повторять постоянно q.Where(x => x.Id == id).SingleOrDefault() муторно?

    public static TEntity ById<TKey, TEntity>(this IQueryable<TEntity> queryable, TKey id)
        where TEntity : class, IHasId<TKey> where TKey : IComparable, IComparable<TKey>, IEquatable<TKey>
        => queryable.SingleOrDefault(x => x.Id.Equals(id));
    
    public IActionResult Get(int id) =>  query
        .ById(id)
        .Do(x => ViewBag.Title = x.Name)
        .Either(Ok, _ => (IActionResult)new NotFoundResult("Not Found"));

    А если, я не хочу получать сущность целиком и мне нужна проекция:

    public static TProjection ById<TKey, TEntity, TProjection>(this IQueryable<TEntity> queryable, TKey id,
    Expression<Func<TEntity, TProjection>> projectionExpression)
        where TKey : IComparable, IComparable<TKey>, IEquatable<TKey>
        where TEntity : class, IHasId<TKey>
        where TProjection : class, IHasId<TKey>
        => queryable.Select(projectionExpression).SingleOrDefault(x => x.Id.Equals(id));   
    
    public IActionResult Get(int id) =>  query
        .ById(id, x => new {Id = x.Id, Name = x.Name, Data = x.Data})
        .Do(x => ViewBag.Title = x.Name)
        .Either(Ok, _ => (IActionResult)new NotFoundResult("Not Found"));

    Я думаю, что к текущему моменту (IActionResult)new NotFoundResult("Not Found")) уже тоже примелькалось и вы сами без труда напишете метод OkOrNotFound

    Paginate


    Пожалуй, не бывает приложений, работающих с данными без постраничного вывода.

    Вместо:

    .Skip((paging.Page - 1) * paging.Take)
    .Take(paging.Take);

    Можно сделать так:

    public interface IPagedEnumerable<out T> : IEnumerable<T>
    {
        long TotalCount { get; }
    }
    
    public static IQueryable<T> Paginate<T>(this IOrderedQueryable<T> queryable, IPaging  paging) 
    => queryable
        .Skip((paging.Page - 1) * paging.Take)
        .Take(paging.Take);
    
    public static IPagedEnumerable<T> ToPagedEnumerable<T>(this IOrderedQueryable<T> queryable,
        IPaging paging)
        where T : class
        => From(queryable.Paginate(paging).ToArray(), queryable.Count());
    
    public static IPagedEnumerable<T> From<T>(IEnumerable<T> inner, int totalCount)
        =>  new PagedEnumerable<T>(inner, totalCount);
    
    public IActionResult Get(IPaging paging) =>  query
        .Where(x => x.IsActive)
        .OrderBy(x => x.Id)
        .ToPagedEnumerable(paging)
        .PipeTo(Ok);

    IQueryableSpecification IQueryableFilter


    Если вы дочитали до этого места, возможно, Вам понравится идея по другому компоновать Where и OrderBy в LINQ выражениях:

    public class MyNiceSpec : AutoSpec<MyNiceEntity>
    {
        public int? Id { get; set; }
    
        public string Name { get; set; }
    
        public string Code { get; set; }
    
        public string Description { get; set; }
    }
    
    public IActionResult Get(MyNiceSpec spec) =>  query
        .Where(spec)
        .OrderBy(spec)
        .ToPagedEnumerable(paging)
        .PipeTo(Ok);
    

    При этом иногда имеет смысл применять Where до вызова Select, а иногда — после. Добавим метод MaybeWhere, который сможет работать как с IQueryableSpecification, так и с Expression<Func<T, bool>>

    public static IQueryable<T> MaybeWhere<T>(this IQueryable<T> source, object spec)
        where T : class
    {
        var specification = spec as IQueryableSpecification<T>;
        if (specification != null)
        {
            source = specification.Apply(source);
        }
    
        var expr = spec as Expression<Func<T, bool>>;
        if (expr != null)
        {
            source = source.Where(expr);
        }
    
        return source;
    }

    И теперь можно написать метод, учитывающий разные варианты:

    public static IPagedEnumerable<TDest> Paged<TEntity, TDest>(
    this IQueryableProvider queryableProvider, IPaging spec ,
    Expression<Func<TEntity, TDest>> projectionExpression)
        where TEntity : class, IHasId
        where TDest : class, IHasId
        => queryableProvider
            .Query<TEntity>()
            .MaybeWhere(spec)
            .Select(projectionExpression)
            .MaybeWhere(spec)
            .MaybeOrderBy(spec)
            .OrderByIdIfNotOrdered()
            .ToPagedEnumerable(spec);

    Или с применением Queryable Extensions AutoMapper:

    public static IPagedEnumerable<TDest> Paged<TEntity, TDest>(this IQueryableProvider queryableProvider,
        IPaging spec)
        where TEntity : class, IHasId
        where TDest : class, IHasId => queryableProvider
            .Query<TEntity>()
            .MaybeWhere(spec)
            .ProjectTo<TDest>()
            .MaybeWhere(spec)
            .MaybeOrderBy(spec)
            .OrderByIdIfNotOrdered()
            .ToPagedEnumerable(spec);

    Если вы считаете, что лепить IPaging, IQueryableSpecififcation и IQueryableOrderBy на один объект богомерзко, то ваш вариант такой:

    public static IPagedEnumerable<TDest> Paged<TEntity, TDest>(this IQueryableProvider queryableProvider,
        IPaging paging, IQueryableOrderBy<TDest> queryableOrderBy,
    IQueryableSpecification<TEntity> entitySpec = null, IQueryableSpecification<TDest> destSpec = null)
        where TEntity : class, IHasId where TDest : class
        => queryableProvider
            .Query<TEntity>()
            .EitherOrSelf(entitySpec, x => x.Where(entitySpec))
            .ProjectTo<TDest>()
            .EitherOrSelf(destSpec, x => x.Where(destSpec))
            .OrderBy(queryableOrderBy)
            .ToPagedEnumerable(paging);

    В итоге получаем три строчки кода для метода, который фильтрует, сортирует и обеспечивает постраничный вывод для любых источников данных с поддержкой LINQ.

    public IActionResult Get(MyNiceSpec spec) =>  query
        .Paged<int, MyNiceEntity, MyNiceDto>(spec)
        .PipeTo(Ok);

    К сожалению сигнатуры методов в C# выглядят монструозно из-за обилия generic'ов. К счастью, в прикладном коде параметры методов можно опустить. Сигнатуры extension'ов LINQ выглядят примерно также. Как часто вы указываете возвращаемый из Select тип? Хвала var, который избавил нас от этого мучения.
    Поделиться публикацией
    Похожие публикации
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама
    Комментарии 59
    • +2
      В мире С++ люди еще помимо fluent interface’ов делают так: дают некоторым результатам цепочных вызовов классы-обертки, т.е. например вызов Maybe() возвращает не T или nullptr, а обертку Maybe<T>. А для некоторых таких оберток генерят перегрузки операторов вроде || чтобы композиция не выглядела так адово, как огромная цепочка вызовов.
      • 0
        В C# тоже можно так делать. А для Maybe еще и реализовать метод SelectMany и писать совсем странные конструкции вроде:
           var maybe=
                        from x in 1.Maybe()
                        from y in SomeOtherMaybe
                        select x + y
        

        Правда, практического применения этой конструкции мне найти не удалось :)
        • +2
          В C++ вообще можно изменить поведение так, что вообще непонятно что творит код. В своей жизни пришлось клепать аналог Expression Tree, ORM, с лютым синтаксисом и т.д… А уж если посмотреть на что-нить типа boost::spirit, boost::phoenix, то становится дурно.

          Увы, C# в большинстве случаев не даст такое сделать, ибо generic != template, да и даже отсутствие typdef уже делает невозможным подобные вещи.

          С другой стороны, скорость компиляции и требования к памяти оставляют желать лучшего в случае таких лютых извращений на C++.
          • +1
            Да, шаблоны в плюсах — штука эпичная. Особенно ошибки компилятора в них:)
            Увы, C# в большинстве случаев не даст такое сделать, ибо generic != template, да и даже отсутствие typdef уже делает невозможным подобные вещи.

            Думаю, что не «увы», а «хорошо что» :). У C# же достаточно неплохо с интероперабельностью с плюсами. Захотелось чего-нибудь эдакого — знай подключай плюсовые dll. А для типовых задач, решаемых на C# такая мощь — избыточна.
            • +1
              Особенно ошибки компилятора в них:)

              Со временем можно научиться в этом разбираться. И если получится разбираться в каше логов GCC, то с остальными компиляторами проблем не возникает.

              Думаю, что не «увы», а «хорошо что» :).

              Ну да, примерно это и имелось ввиду :) Пока народ не проникся Roslyn, C# будет держаться.

              А в плане силы C#, у него есть рефлексия, деревья выражений и кодогенерация, и тут можно очень много хорошего наворотить.
        • 0

          -----удалено, ошибся----

          • +3
            Приятного дебага!
            • –2
              Вообще код упадет на цепочке вызовов. Поставив брейпоинт на начало и нажав несколько раз  F11 вы быстро найдете ошибку. В целом же, я вообще не фанат дебага. Предпочитаю работающие решения и тесты.
            • +3
              Враперы-оберточки, хоп абстракций кусочечки…
              Польза таких избыточных абстракций в C# сомнительна, но чего греха таить, меня тоже на такие оберточные поделия часто прорывает)

              public IActionResult Get(int id) =>  query
                  .Where(x => x.Id == id)
                  .SingleOrDefault()
                  .PipeTo(x => x != null ? Ok(x) : new NotFoundResult(“Not Found”));
              


              Выглядит уже не так хорошо. Исправим это с помощью метода Either:
              public static TOutput Either<TInput, TOutput>(this TInput o, Func<TInput, bool> condition,
                  Func<TInput, TOutput> ifTrue, Func<TInput, TOutput> ifFalse)
                  => condition(o) ? ifTrue(o) : ifFalse(o);
              
              public IActionResult Get(int id) =>  query
                  .Where(x => x.Id == id)
                  .SingleOrDefault()
                  .Either(x => x != null, Ok, _ => (IActionResult)new NotFoundResult("Not Found"));
              


              Стало ничем не лучше, даже наоборот, переусложнён код, который ещё может быть кому-то предстоит отлаживать…
              • +1
                Начали за здравие, закончили за упокой.
                PipeTo, Do ещё приемлемы, а все остальное выглядит как функциональщина ради функциональщины:
                Either — заменили? и: на, и,
                ById — теперь надо реализовать ещё один интерфейс.
                ById с проекцией — если вставить перед ним Select будет понятнее.
                ToPagedEnumerable — не ленивая
                IQueryableSpecification — В каком порядке отсортирует? сначала по Id потом по Name или наоборот?
                В случае с лямбдой все понятно, а компилятор эту «спецификацию» создаст за нас.
                MaybeWhere — зачем проверять на Expression, если мы передаем всегда IPaging
                Проще реализовать два метода расширения и переложить работу на компилятор.

                ПС: Не написали про методы MinBy, MaxBy.
                Также полезным был бы интерфейс:
                interface ICountedEnumerable<out T> : IEnumerable<T>
                {
                    public int Count {get;}
                }
                

                Как промежуточный между ICollection и IEnumerable, расширения которого избавляют от лишней работы в методах ToArray, ToList.
                ППС: И напоследок пару расширений:
                IEnumerable<Tuple<T, T>> Pack<T>(this IEnumerable<T> source);
                IEnumerable<Tuple<T, T>> Window<T>(this IEnumerable<T> source);
                
                • +1

                  ICountedEnumerable уже есть, только называется IReadOnlyCollection

                  • 0
                    Either — заменили? и: на, и,
                    Я использую только вторую перегрузку (та, что проверяет на null). Получается так:
                    str.Either(x => x + " Some more text", _ => "No Text");. Возможно, первую, та что с condition можно и вправду удалить за ненадобностью. Подумаю об этом.

                    IQueryableSpecification — В каком порядке отсортирует? сначала по Id потом по Name или наоборот?
                    В случае с лямбдой все понятно, а компилятор эту «спецификацию» создаст за нас.
                    Подробнее эта часть описана в другой статье. Эта ссылка есть и в тексте этого топика. Скорее всего вы не перешли, поэтому возник вопрос.

                    MaybeWhere — зачем проверять на Expression, если мы передаем всегда IPaging
                    Проще реализовать два метода расширения и переложить работу на компилятор.
                    Посмотрите на метод Paged. MaybeWhere нужен только для компоновки. Возможно его стоит сделать приватным.
                    • 0
                      > Я использую только вторую перегрузку (та, что проверяет на null).

                      Конечно, если использовать как

                      > .SingleOrDefault().Either(x => x != null, Ok, _ => (IActionResult)new NotFoundResult(«Not Found»));

                      Есть какой-то сакральный смысл в том, что бы сначала самому себе подсовывать Default, а потом вот так изгаляться?

                      > .SingleOrElse(new NotFoundResult(«Not Found»));
                • +3
                  Всё это здорово, пока вы единственный разработчик и помните, что вы спрятали за этими 'PipeTo' и пр. Но отлаживать чужой подобный код то ещё удовольствие. В каждый такой extension method всё равно приходится залезать чтобы понять, делает ли он то, о чём вы подумали или, вообще, что именно он делает. А попробовав F# можно понять, что «крен функциональщины» в C# так себе.
                  • –1
                    Для ловли ошибок есть юнит тесты. А данный подход на оборот приятен, удобен и сокращает код.
                    • 0
                      Наличие юниттестов не исключает отладку, а даже наоборот. Насчет удобства, кому как. Я поделился своим опытом.
                      • +3
                        Ну ок, тест упал, в тестируемом коде ошибка. Это не избавляет от необходимости проанализировать код, а следовательно разобраться во всей этой кастомщине.
                    • +1
                      Можно позанудствовать?
                      Спасибо.
                      ById требует соблюдения некоторых соглашений. Что-то облегчили, в чем-то добавили сложности
                      Ваш Either не совсем та структура (монада, если угодно) Either, которая обычно используется в ФП…
                      Ну, и названия типа MaybeWhere глаз немного режут.
                      А вообще радует Ваш подход
                      • 0
                        MaybeWhere  мне потребовался, чтобы написать
                        query
                            .MaybeWhere(/* может быть ты хочешь применить  where до проекции*/)
                            .Select (...)
                            .MaybeWhere(/* или после?*/)
                            .ToArray()
                        

                        Тогда можно лениться и делать так:
                        public class Spec: IQueryableSpec<TEntity>, IQueryableSpec<TProjection>
                        

                        В таком случае будут применены оба Where. Обычно два не требуется — достаточно одного. Специально для этого в самом конце есть  Paged с явным указанием параметров. Мне в простых Crud проще вешать много интерфейсов на один класс. Главное понимать Flow: Entity => Dto. Соовтетственно фильтрации и сортировки применяются в таком-же порядке.

                        Either здесь действительно не имеет отношения к слове на букву М:) На данный момент я думаю, что в C# проще кидать исключения и писать только один try/catch на все приложение. Получится вполне себе Either<TResult, Exception>. Пытаться конвертнуть все Exception'ы в Failure в .NET Framework — задача так себе.

                        По поводу читабельности мы специально проводили эксперимент и давали нескольким программистам посмотреть код с Either и PipeTo. Все правильно ответили как работает код, поэтому оставили название методов такими. Можете предложить название лучше?

                        • +1
                          Either и PipeTo

                          Можете предложить название лучше?

                          Ну, Either тут больше похож на ContinueWith, не? На мой взгляд. Не претендую. Просто CPS, как он есть…

                          По поводу
                          Either<TResult, Exception>
                          — дело вкуса, конечно. Я, например, в одном из проектов использую Either<TError, TResult>. И так это меня радует, неимоверно. Почти как у взрослых :), вынуждает разработчика обрабатывать оба варианта, иначе не скомпилируется.
                          • 0
                            ContinueWith — вариант, но тогда придется пилить нечто вроде
                            flow.ContinueWith(x => ...).Error(e => ...). А это уже надо опять промежуточный объект создавать и получится тот-же самый Either<TResult, Exception>.

                            А не поделитесь вашей реализацией Either? Я написал одну, показалось не элегантно и отказался. Может есть реализации лучше моей?
                            • +1
                              Не то, чтобы моя реализация с нуля:
                              Как-то так
                              public class Either<TL, TR>
                                  {
                                      [DataMember]
                                      private readonly bool _isLeft;
                              
                                      [DataMember]
                                      private readonly TL _left;
                              
                                      [DataMember]
                                      private readonly TR _right;
                              
                                      public Either(TL left)
                                      {
                                          _left = left;
                                          _isLeft = true;
                                      }
                              
                                      public Either(TR right)
                                      {
                                          _right = right;
                                          _isLeft = false;
                                      }
                              
                                      /// <summary>
                                      /// Checks the type of the value held and invokes the matching handler function.
                                      /// </summary>
                                      /// <typeparam name="T">The return type of the handler functions.</typeparam>
                                      /// <param name="ofLeft">Handler for the Left type.</param>
                                      /// <param name="ofRight">Handler for the Right type.</param>
                                      /// <returns>The value returned by the invoked handler function.</returns>
                                      /// <exception cref="System.ArgumentNullException">
                                      /// </exception>
                                      public T Match<T>(Func<TL, T> ofLeft, Func<TR, T> ofRight)
                                      {
                                          if (ofLeft == null)
                                          {
                                              throw new ArgumentNullException(nameof(ofLeft));
                                          }
                              
                                          if (ofRight == null)
                                          {
                                              throw new ArgumentNullException(nameof(ofRight));
                                          }
                              
                                          return _isLeft ? ofLeft(_left) : ofRight(_right);
                                      }
                              
                                      /// <summary>
                                      /// Checks the type of the value held and invokes the matching handler function.
                                      /// </summary>
                                      /// <param name="ofLeft">Handler for the Left type.</param>
                                      /// <param name="ofRight">Handler for the Right type.</param>
                                      /// <exception cref="System.ArgumentNullException">
                                      /// </exception>
                                      public void Match(Action<TL> ofLeft, Action<TR> ofRight)
                                      {
                                          if (ofLeft == null)
                                          {
                                              throw new ArgumentNullException(nameof(ofLeft));
                                          }
                              
                                          if (ofRight == null)
                                          {
                                              throw new ArgumentNullException(nameof(ofRight));
                                          }
                              
                                          if (_isLeft)
                                          {
                                              ofLeft(_left);
                                          }
                                          else
                                          {
                                              ofRight(_right);
                                          }
                                      }
                              
                                      public TL LeftOrDefault() => Match(l => l, r => default(TL));
                                      public TR RightOrDefault() => Match(l => default(TR), r => r);
                                      public Either<TR, TL> Swap() => Match((Func<TL, Either<TR, TL>>) (Right<TR, TL>), Left<TR, TL>);
                              
                                      public Either<TL, T> Bind<T>(Func<TR, T> f)
                                          => BindMany(x => Right<TL, T>(f(x)));
                              
                                      public Either<TL, T> BindMany<T>(Func<TR, Either<TL, T>> f) => Match(Left<TL, T>, f);
                              
                                      public Either<TL, TResult> BindMany<T, TResult>(Func<TR, Either<TL, T>> f, Func<TR, T, TResult> selector)
                                          => BindMany(x => f(x).Bind(t => selector(_right, t)));
                              
                                      public static implicit operator Either<TL, TR>(TL left) => new Either<TL, TR>(left);
                                      public static implicit operator Either<TL, TR>(TR right) => new Either<TL, TR>(right);
                              
                                      public static Either<TLeft, TRight> Left<TLeft, TRight>(TLeft left)
                                          => new Either<TLeft, TRight>(left);
                              
                                      public static Either<TLeft, TRight> Right<TLeft, TRight>(TRight right)
                                          => new Either<TLeft, TRight>(right);
                              
                                      public static Either<Exception, T> Try<T>(Func<T> f)
                                      {
                                          try
                                          {
                                              return new Either<Exception, T>(f.Invoke());
                                          }
                                          catch (Exception ex)
                                          {
                                              return new Either<Exception, T>(ex);
                                          }
                                      }
                                  }
                              


                              • 0
                                А прикладной код как выглядит?
                                • 0
                                  Примерно так:
                                  public interface ISomeService
                                  {
                                      Task<Either<TradeError, Quote>> GetQuoteAsync(GetQuoteQuery query);
                                  }
                                  ...
                                  var result = await _service.GetQuoteAsync(query);
                                  result.Match(SetQuoteError, SetQuote);
                                  ...
                                  
                                  private void SetQuoteError(TradeError error)
                                  {
                                      // do something
                                  }
                                  
                                  private void SetQuote(Quote quote)
                                  {
                                      // do something
                                  }
                                  
                                  




                                  Или так
                                  public interface ISomeService
                                  {
                                      Task<Either<TradeError, Quote>> GetQuoteAsync(GetQuoteQuery query);
                                  }
                                  
                                  ...
                                  public async Task<Either<TradeError, QuoteModel>> GetQuoteModelAsync(GetQuoteQuery query)
                                          {
                                              var result = await _service.GetQuoteAsync(query.);
                                              return result.Bind(ToQuoteModel);
                                          }
                                  ...
                                  
                                  private static QuoteModel ToQuoteModel(Quote source) => new QuoteModel{ ... };
                                  
                                  



                                  Должно быть понятно, вроде… :)
                                  • 0
                                    А чем это лучше использования task.IsFaulted?
                                    <оффтоп>У вас на работе трейдинг что-ли (трейд эрроры, котировки)?:)</оффтоп>
                                    • 0
                                      А потому, что Error возвращает удаленный сервер, вполне легитимный ответ и его надо обрабатывать. А Task может завалиться и по другой причине.
                                      Что-то вроде :)
                      • +2
                        Почему бы просто не упороться в функциональный язык?
                        • 0
                          Потому что для написания CRUD'а C# по ряду причин подходит лучше. А когда этого CRUD'а ой как много, приходится как-то его структурировать.
                          • +1
                            обычно для круда есть кодогенераторы
                            • 0
                              Кому что больше нравится.
                          • 0
                            Да хотя бы, чтобы не переучивать всю команду в середине проекта.
                            А так — здесь им идейку подкинул, тут структурку, глядишь, а они уже и сами упоролись куда надо.
                          • +1

                            Что это за новая мода — писать всю программу в одну строчку, а потом делать в ней переносы, когда она не входит в экран? Ладно еще запросы linq — это еще куда ни шло, но ветвления? Вы серьезно?

                            • 0
                              В условиях предпочитаю сначала писать rvalue:
                              public static TInput Do<TInput>(this TInput o, Action<TInput> action)
                              {
                                  if (null != o) 
                                      action(o);
                                  return o;
                              }
                              


                              Кроме того что это исключает тривиальные ошибки присваивания в условии, такой подход позволит применять функции и к структурам, хотя для них проверка на null и бессмысленна, но бывает заранее неизвестно что придется обрабатывать.
                              • +2
                                Тяжелое плюсовое детство?
                                • +1

                                  Даже на плюсах так никто уже не пишет.

                                  • 0
                                    В шарпе присваивание в условии вполне может быть
                                    int a = 1;
                                    if ((a = 2) == 3)
                                    {
                                        a = 4;
                                    }
                                    

                                    В чем минус указанного подхода (с rvalue в первом операнде)?
                                    • 0
                                      А в чем преимущество присваивания в условии?
                                      • 0
                                        Все зависит от контекста. Например, при чтении из файла может быть удобно писать
                                        while(null != (line = stream.ReadLine()))
                                        {
                                        }
                                        


                                        Вместо
                                        line = stream.ReadLine();
                                        while(null != line)
                                        {
                                        /* ToDo */
                                        line = stream.ReadLine();
                                        }
                                        


                                        Но мой вопрос был, в чем минус подхода в следующей записи:
                                        if(null != expression)
                                        


                                        При такой же читабельности получаем как плюс, исключение ошибки присваивания, и возможность единообразного использования как reference так и value типов.
                                        • 0
                                          То есть, стараться всунуть как можно больше в одну строчку, получая потенциальный рассадник ошибок и ухудшение понятности, это улучшение?
                                          Не говоря уже о такой «ошибке присваивания, как
                                          null = line;
                                          

                                          Гипотетически, если тип приводится к bool, можно наворотить, но Вы же не пишете
                                          if(b == false)
                                          {
                                          }
                                          
                                          , правда?
                                          • 0
                                            Да, я пишу так:
                                            if(false == b)
                                            {
                                            }
                                            

                                            и не пишу так
                                            if(!b)
                                            {
                                            }
                                            

                                            т.к. при чтении кода намного меньше бросается в глаза

                                            Но вы отвечаете не на тот вопрос. Меня действительно интересует в чем минус, или возможная проблема случая когда в условии первым операндом стоит rvalue.
                                            • +1
                                              Про code style, naming conventions рассказывать?
                                              Про семантику?

                                              Сравните:
                                              если мое_множество равно пустому_множеству то…
                                              против
                                              если пустое_множество равно моему_множеству то…

                                              пустое множество не может равняться чему-то еще, оно одно такое, инициальный объект в этом типе.
                                              • 0
                                                Выше неудачное объяснение, громоздкое.
                                                Смотрите, Вы говорите
                                                If(false == b) {}
                                                false — это константа, она ничему больше, кроме самой себя, равняться не может.
                                                С этим, надеюсь, спорить не будете?
                                                Так вот, странно семантически неверно сравнивать константу с чем-то. Что-то с константой- да, если это что-то может принимать разные значения.

                                                Технически, никто Вам не запрещает писать так, как Вы пишете, ни компилятор, ни рантайм. Но читается это не очень…
                                                Поэтому, собственно, весь остальной мир на шарпе и пишет if(value==null).

                                                • 0
                                                  По итогу, ваш аргумент следующий, так писать не стоит потому что большинство пишет по другому? И никаких побочных, кроме нарушения стиля, нет?

                                                  Из комментария выше про множества, с моей точки зрения оба утверждения семантически равноценны, т.к. операция сравнения не предполагает порядок указания сравниваемых частей.
                                                  • 0
                                                    В том-то и дело, что не равноценны. Если Вы этого не видите, ну, что ж…
                                                    Про стиль и пальцы написали уже. У меня такое не то, что code review не прошло бы, даже commit завернуло бы.
                                                    • +1
                                                      К сожалению, не могу вспомнить точной терминологии про смысловые ударения, но основная идея в том, что предложения «мама мыла раму» и «рама мылась мамой», с одной точки зрения, описывают один и тот же процесс, но с другой, выделяют совершенно разные центральные объекты действия.

                                                      Языки программирования — они ж для человеков, не для роботов :)
                                          • 0

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

                                    • +1
                                      ById тоже используем, только у нас в Code First, есть базовый абстрактный класс, от которого остальные наследуются, что-то типа:

                                          public abstract class Identified
                                          {
                                              public long Id { get; set; }
                                          }
                                      

                                      и уже ему пишем расширение:

                                              public static T ById<T>(this IEnumerable<T> identifies, long id)
                                              where T : Identified
                                              {
                                                  return identifies.SingleOrDefault(c=>c.Id.Equals(id));
                                              }
                                      


                                      Кстати, почему у вас расширение только для IQueryable? можно сразу на IEnumerable, это может помочь дальше применять ById, но просто теперь для всех перечислений с этим типом.
                                      • +1
                                        А у нас так:
                                           public interface IHasId
                                            {
                                                object Id { get; }
                                            }
                                        
                                            public interface IHasId<out TKey> : IHasId
                                                where TKey: IComparable, IComparable<TKey>, IEquatable<TKey>
                                            {
                                                new TKey Id { get; }
                                            }
                                        


                                        Для enumerable тоже бывает делаем расширения, но чаще нужны именно IQueryable.
                                        • 0
                                          Может, конечно, адово туплю, но для чего IHasId реализует IHasId? Честно говоря, не понимаю — зачем вообще первый интерфейс нужен.
                                          • 0
                                            Чтобы можно было сделать каст к IHasId без generic'а. Бывают случаи, когда тип T не доступен.
                                            • +1
                                              Если Т не доступен, то можно привести к
                                              IHasId<object>
                                              
                                              • 0
                                                Спасибо за совет! Нужно проверить, что LINQ нормально отработает с ковариацией и выбросить без generic'а, если все ок.
                                                • 0
                                                  Вспомнил, почему сделали так. У нас на IHasId<T> висит where T:IEquatable<T>. Так что привести к object нельзя, потому что object не реализует IEquatable. Убирать это условие не хочется, потому что тип ключа по определению должен быть сравним (чтобы иметь возможность быть уникальным). Интерфейс без T остался для поддержки композитных ключей.
                                        • 0
                                          А это все можно использовать с Entity framework? конкретно интересует пагинация.
                                        • 0

                                          Вот моя библиотека для программирования на C# в функциональном стиле: github repo
                                          Получается код типа:


                                          public IActionResult Add(string a, string b, string c, string d, int count = 1, int mod=0)
                                            => _dal.GetUrl(a, b, c, d).Convert(_dal.GetProduct)
                                               .IfNotNull(x => 
                                                  _dal.AddProductToCart(_dal.GetCartBySession(HttpContext.Session.Id), x, count, mod)
                                               .Extend(x).Convert(PartialView), () => (IActionResult) NotFound());
                                          • +1
                                            Чейто какой-то magic
                                            • 0
                                              Посмотрел. Пара замечаний:
                                              1) В методах Add, Remove есть побочные эффекты.
                                              2) Возможность добавить/удалить проверяется через list.GetType().IsArray и при передаче ReadOnlyCollection упадет с ошибкой. Лучше проверять через свойство IsReadOnly
                                            • 0
                                              C# — язык мультипарадигмальный

                                              По мне так это язык ООП с элементами ФП, но никак не мультипарадигмальный. Не хватает например expression over statement или readonly на уровне аргументов и переменных для того что бы быть мультипарадигмальным.

                                              Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.