Как стать автором
Обновить
4
0

Пользователь

Отправить сообщение

Мне иетересно, пробовали ли вы Microsoft Orleans? Как на меня отличнейшее решение для 90% случаев. Но тут я пока только как теоретик, пару раз кластер подымал — проще ничего нету.
Первое что в голову приходит


  • Строгая типизация реквестов через интерфейсы со скоростной сериализацией
  • Автоматическая балансировка вплоть до миграции воркера на другую машину с состоянием (естественно надо persistanse писать)
  • Автоматический failover
  • Поддержка кластерного singleton по умолчанию. Удобно, знеаете ли, знать что три машины не колбасят ту же задачу или данные.
  • Distributed cache в силу того что работает предыдущий пункт
  • Встроенные события/потоки/таймеры/remainders
  • Discovery

Можно еще покопать, но как на меня профит очевиден: пишите себе на чистом .NET забыв о swagger. Расслабляетесь от балансировки "микросервисов", от толпы не нужных инстансов. Не конфигурируете кеши, очереди (ну без них не всегда можно).


Сумбурно как-то так

items.Count()

Не затянули ли вы всю таблицу чтобы подсчитать количество записей?
Ну в общем то что заметил, разве что сложные отчеты это не к EF :).

Не все упирается в выборку, хотя тут linq2db даст фору по качеству SQL.
Например изменение записи в EF это два обращения к базе данных, и то желательно в транзакции.


  • Достать обьект из базы полностью
  • Изменить свойство
  • SaveChanges

В linq2db это однин стейтмент и ничего на клиент тянуть не пришлось


   db.Products
     .Where(p => p.ProductId == dto.ProductId)
     .Set(p => p.SoldDate, () => Sql.CurrentTimestamp)
     .Set(p => p.IsSold, true)
     .Update();

Конечно возможно приаттачить обьект к ChangeTracker, только делают это редко.
Также из примера видно что мы использовали текущую дату сервера. Что в EF проблематично.


Вот из таких кирпичиков и растет перформанс. Чем дальше мы от SQL, тем больше мы полагаемся на архитектуру всемогутора, а она не всегда эфективна.

Это риторический вопрос?
На всякий случай вот dotnet.github.io/orleans
> Не может этот запрос быть быстрее обычного count без оконной функции

Как бы очевидно.
Но план запроса строится один раз в отличии от двух запросов в одном раундтрипе. И мало какой ADO.NET провайдер такое поддерживает. Сказал же в полтора раза быстрее.
Если все-же, количество записей важно, не забываем об оконных функциях.

SELECT COUNT(*) OVER () as TotalCount, * 
FROM Sales.SalesOrderHeader
ORDER BY OrderDate DESC
OFFSET 0 ROWS
FETCH NEXT 50 ROWS ONLY


Теперь в каждой записи будет зашито реальное количество записей. Достаточно прочитать из первой записи это значение, а дальше игнорировать. На вскидку, получается в полтора раза быстрее и один запрос к базе.

Естественно это будет работать только если вы не применяли другие техники выбора страницы.
Ну, можно присмотреться к этой библиотеке, человеку очень не хватало монад и функционального в C#. Все никак не соберусь попробовать, пугает на сколько может упасть скорость работы и что все-таки компилятор с JIT соптимизируют.
github.com/louthy/language-ext
Да, есть библиотека для бескровного склеивания двух миров. Асинки вот только конфликтуют, для решения этой ситуации добавлены дополнительные экстеншины.
github.com/linq2db/linq2db.EntityFrameworkCore
В названии статьи отсутствует LINQ by EF. Так как оно не отражает полную картину. EF всегда был калекой в трансляции дерева выражений в SQL.

Практически все эти детские болезни мы полечили в linq2db. Клеить строки не надо, Dapper не надо, делать хранимки на каждый чих не надо.

  • Хинты можно использовать
  • CTE из каробки, вместе с рекурсивными CTE
  • Union, Concat собирает все что надо, хоть завались ими
  • INSERT FROM, INSERT INTO, UPDATE FROM...
  • Менять названия таблиц налету
  • Использовать временные таблицы
  • Оконные функции из каробки
  • BulkCopy на все поддерживаемые базы данных
  • CRUD по CQRS паттерну, ничего лишнего вплоть до единственного поля
  • ...


“Но они продолжали жевать кактус и дискутировать какой generic repository лучше и когда стартовать UoW.”
Я на визиторы смотрю как на ужас. Наверное наша ORM давно бы погрязла в куче кассов и непонятных иерархий.
Простая, постоянно требующя решения задача подмены параметра в теле лямбды превращается в армагеддон. Да она решается созданием одного ReplacingExpressionVisitor (такой себе класик)
И такого вызова:

var newParam = Expression.Parameter(someType); 
var newBody = ReplacingExpressionVisitor.Replace(lambda.Paramters[0], newParam, lambda.Body);

А еще она решаться может и вот так:
var newParam = Expression.Parameter(someType); 
var newBody = lambda.Body.Transform(e => e == lambda.Paramters[0] ? newParam : e);


Вроде одинаково, но гипотетически усложним задачу, мало того, что надо подменить параметр, так и прибавить единички ко всем целочисленным константам. Тут уже надо городить новые классы или даже пропускать через несколько визиторов, а мы в лямбде для Transform просто усложняем условие. Это может быть ощутимо заметно на гигантском дереве, такие иногда получаются когда запрос к базе содержит монструидальный Where, созданный динамичиски какой-то UI библиотекой.

Минус Transform в том что если вы придумываете кастомные Expression, тут уже сильно не потрансформируешь, или придется расширять метод.

Если интересна реализация, ее можно найти в библиотеке CodeJam или в нашей ORM linq2db.

В защиту данного подхода скажу так, за 5 лет мне ни разу не пришлось писать новый Visitor.
Вы не понимаете что такое энумераторы и роль yield return в коде парсера.

Я бы советовал подучить эту часть.
Как только, кто-то начинает энумерацию, создается стейт машина (класс в который превращается ParseCsv метод компилятором) и при вызове метода Next в энумераторе из файла читается одна линия, не больше. Когда файл заканчивается Next вернет false — все мы перебрали все линии.
Не учить же вас как пользоваться C#. Вот примерная реализация парсера. Пишу на коленке, важен подход.
public IEnumerable<string[]> ParseCsv(string fileName)
{
   using (var file = new System.IO.StreamReader(fileName))
   {
       string line;
       while((line = file.ReadLine()) != null)  
       {  
            yield return line.Split(‘\t’);
       }
    }
}

linq2db внутри считывает из этого энумератора только 1000 записей и запихивает это в BulkCopy (для MSSQL) или готовит компактный SQL с параметрами на всю 1000 записей. Объекты больше не нужны и их подберет GC.
Процесс повторяется пока до конца файла не дойдем. Энумератор диспозится и файл закрывается.

Точно также можно взять данные IEnumerable из одной базы и закинуть в другую. Не важно сколько записей в исходной таблице, хоть миллионы.
Вся магия внутри, по умолчанию обрабатываются пачки по 1000 записей, и это конфигурируется. Догадываетесь почему?

Вы пропустили мое описание что данные должны представляться как IEnumerable — это не все записи сразу, ни в коем случае.
Могу подкинуть другой вариант:
Берется linq2db
Берется любая библиотека которая CSV парсит и потоково возвращает IEnumerable<string[]>, тут я не помощник.

И пишется что-то такое
var csvItems = CSVParser.Parse("filename");

using (var db = new DataConnection(...))
{
    db.BulkCopy(
       csvItems.Select(csv => new DestinationEntity{
          Field1 = int.Parse(csv[0]),
          Field2 = double.Parse(csv[1]),
          LongStr = csv[2],
          ...
       })
    );
}

Гарантирую, что если отключите индексы, данные влетят, даже чай не успеете заварить.
Для баз данных, которые поддерживают такие вставки, и того быстрее. Будут посланы параметризированные запросы и не надо беспокоиться о длине полей.
1) Возможно, специфика есть специфика
2) Так и делают. Тянут ORM на клиент, хотя в случае EF Core это толпа зависимостей. По этому часто используют Remote LINQ или что-то в этом роде.

О случае когда вам IQueryable не хватило можно поподробнее?

p.s. код лучше было оформить в виде ссылки на codebin или что-то вроде того

Возможно, но так как писано на коленке, тогда и не задумывался.
Я, чесно, пока не понимаю зачем вы так усложняете себе жизнь. За час на коленке я написал простенькое решение, которое умеет комбинировать фильтры. Все просто, вы пишете IQueryable расширения и комбинируете как угодно.
К примеру используя одни и те же методы:

class Program
{
   static void Main(string[] args)
   {
       var query = new[]
       {
           new SomeClass { City = "NY", Years = 20 },
           new SomeClass { City = "Seattle", Years = 16 },
       }.AsQueryable();

        var semi = query.CombineOr(q => q.IsReadyToDrink(), q => q.LiveIn("NY", "Seattle"))
            .ToArray();

        var strict = query
            .IsReadyToDrink()
            .LiveIn("NY", "Seattle")
            .ToArray();


        var semi2 = query.CombineOr(q => q.CityAndDrinkability("NY", "Seattle"), q => q.IsReadyToDrink())
        .ToArray();

        var strict2 = query
            .CityAndDrinkability("NY", "Seattle")
            .ToArray();
   }
}

Здесь помогают интерфейсы, которые вы зададите своим сущностям:

public interface IAge
{
    int Years { get; }
}

public interface ICity
{
    string City { get; }
}

public class SomeClass : IAge, ICity
{
    public string SomeValue { get; set; }
    public int Years { get; set; }
    public string City { get; set; }
}

public static class BusinessRules
{
    public static IQueryable<T> IsReadyToDrink<T>(this IQueryable<T> source)
        where T: IAge
    {
        return source.Where(s => s.Years >= 18).Where(s => s.Years <= 70);
    }

    public static IQueryable<T> LiveIn<T>(this IQueryable<T> source, 
          params string[] cities)
        where T: ICity
    {
        return source.Where(s => cities.Contains(s.City));
    }

    public static IQueryable<T> CityAndDrinkability<T>(this IQueryable<T> source, params string[] cities)
        where T: ICity, IAge
    {
        return source.LiveIn(cities).IsReadyToDrink();
    }
}

Ну и сама реализация (использован метод Transform из билиотеки CodeJam)

public static class SpecsExtensions
{
    private static Expression Unwrap(Expression expr)
    {
        if (expr.NodeType == ExpressionType.Quote)
            return Unwrap(((UnaryExpression) expr).Operand);
        return expr;
    }

    private static IEnumerable<Expression> CollectCondition(Expression query, ParameterExpression obj)
    {
        if (query.NodeType == ExpressionType.Call)
        {
            var mc = (MethodCallExpression) query;
            if (mc.Method.IsGenericMethod && mc.Method.GetGenericMethodDefinition() == _whereMethodInfo)
            {
                var unwrapped = (LambdaExpression)Unwrap(mc.Arguments[1]);
                foreach (var cond in CollectCondition(mc.Arguments[0], obj))
                {
                    yield return cond;
                }

                var corrected = unwrapped.Body.Transform(e => e == unwrapped.Parameters[0] ? obj : e);

                yield return corrected;
            }
            else
            {
                var canProcess = mc.Method.DeclaringType != typeof(Queryable) && mc.Arguments.Count > 0;
                if (canProcess)
                {
                    canProcess = mc.Arguments[0].Type.IsGenericType;
                    if (canProcess)
                    {
                        canProcess = mc.Arguments[0].Type.GetGenericTypeDefinition() == typeof(IQueryable<>);
                    }
                }

                if (!canProcess)
                {
                    throw new NotImplementedException();
                }

                // processing user defined functions, so filters can be reused in other filters
                var innerExpression = ((IQueryable) Expression.Lambda(mc).Compile().DynamicInvoke()).Expression;
                foreach (var cond in CollectCondition(innerExpression, obj))
                {
                    yield return cond;
                }               
            }

        }
    }

    public static MethodInfo GetMethodInfo<T>(Expression<Action<T>> expression)
    {
        var member = expression.Body as MethodCallExpression;

        if (member != null)
            return member.Method;

        throw new ArgumentException("Expression is not a method", "expression");
    }

    private static readonly MethodInfo _whereMethodInfo = GetMethodInfo<IQueryable<int>>(q => q.Where((Expression<Func<int, bool>>)null)).GetGenericMethodDefinition();

    public static IQueryable<T> CombineOr<T>(this IQueryable<T> source, params Func<IQueryable<T>, IQueryable<T>>[] queries)
    {
        Expression condition = null;
        var param = Expression.Parameter(typeof(T), "q");
        var fake = Enumerable.Empty<T>().AsQueryable();
        foreach (var query in queries)
        {
            var filter = query(fake);
            var strict = CollectCondition(filter.Expression, param)
                .Aggregate(Expression.AndAlso);

            condition = condition == null ? strict : Expression.OrElse(condition, strict);
        }

        if (condition == null)
            return source;

        var filterBody = Expression.Lambda(condition, param);

        var result = (IQueryable<T>) _whereMethodInfo.MakeGenericMethod(typeof(T))
            .Invoke(null, new object[] { source, filterBody });

        return result;
    }
}

Как на меня просто, наглядно и легко понимаемо. Да тут шаманство с деревьями выражений, но я это сделал за вас.
Все проверяется компилятором и никаких дополнительных абстракций.
  1. try catch {} плохая практика
  2. Сейчас принято писать асинхронные методы, да и хорошая практика для сервера

Ваша абстракция понятна, только, я не помню в своем случае когда коня/ORM меняли на переправе. Будут сидеть на EF до конца. Учитывая что абстракции резко ухудшают производительность как самой программы так и программистов, я бы задумался нужны ли они в таком виде. DbContext уже сам по себе неслабый репозиторий. Пряча от разработчиков LINQ, вы просто усложняете себе жизнь. IMHO, абстракции лучше строить на уровне бизнес задач/сервисов и в крайнем случае над отдельными сущностями.

Простой пример, что вы сделаете когда надо достать Id первых 100 пользователей, у которых установлен язык английский? Что вам дадут два репозитория которые возвращают целые таблицы в память, а вам нужно достать всего сотню целых чисел из данных связанных двумя таблицами?
Тут приходит на помощь LINQ, репозитории могут возвращать IQueryable. Только зачем эти репозитории тогда, если они просто враппер над DbContext? Я, например, не вижу в них смысла, это ухудшает читабельность, нагружает контроллер дополнительными зависимостями. Если контроллер большой, в него ижектят с 20-к репозиториев, несмотря на то что в основном нужны только несколько на один запрос.

Слегка сумбурно, но это я сейчас наблюдаю во всех ASP.NET проектах в которых просят помочь разобраться с производительностью. Написано по патернам, которые я бы уже и сам назвал антипатернами.
Это было познавательно с IIListProvider. Будем знать.
А тест надо переписать чтобы забрать зависимость от этой фичи. Умом понимаешь, что ToList будет быстрее чем ToArray, а тут как обухом.
Вы это сейчас серьезно написали об EF 6?
Если чесно, я думаю они правильно поступили. EF 6 настолько уныло строит SQL запросы, что слеза наворачивается и одна надежда на мозг SQL оптимизатора.
Но, также замечу, что подход с Lazy Loading, на том же проекте привнес значительные тормоза у реальных пользователей системы с большими обьемами данных.
Сам поучавствовал в этом сраче 2015 года. Яркий пример как подход к базе данных как к тупому хранилищу объектов заканчиватся плачевно.

Информация

В рейтинге
Не участвует
Зарегистрирован
Активность