Функциональный 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, который избавил нас от этого мучения.
    Метки:
    Поделиться публикацией
    Похожие публикации
    Реклама помогает поддерживать и развивать наши сервисы

    Подробнее
    Реклама
    Комментарии 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 на уровне аргументов и переменных для того что бы быть мультипарадигмальным.

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