diff --git a/BenchmarkService.cs b/BenchmarkService.cs index d0a958d..c37eff4 100644 --- a/BenchmarkService.cs +++ b/BenchmarkService.cs @@ -1,28 +1,48 @@ using BenchmarkDotNet.Attributes; +using Dapper; using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; using OptimizeMePlease.Context; +using OptimizeMePlease.Entities; +using System; using System.Collections.Generic; using System.Linq; +using System.Linq.Expressions; namespace OptimizeMePlease { + [InProcess] [MemoryDiagnoser] public class BenchmarkService { + PooledDbContextFactory _dbContextFactory; + PooledDbContextFactory _indexedDbContextFactory; + public BenchmarkService() { + var options = new DbContextOptionsBuilder() + .UseSqlServer("Server=localhost;Database=OptimizeMePlease;Trusted_Connection=True;Integrated Security=true;MultipleActiveResultSets=true;Encrypt=false") + .Options; + + _dbContextFactory = new PooledDbContextFactory(options); + + var indexedDbOptions = new DbContextOptionsBuilder() + .UseSqlServer("Server=localhost;Database=OptimizeMePlease-Indexed;Trusted_Connection=True;Integrated Security=true;MultipleActiveResultSets=true;Encrypt=false") + .Options; + + _indexedDbContextFactory = new PooledDbContextFactory(indexedDbOptions); } /// - /// Get top 2 Authors (FirstName, LastName, UserName, Email, Age, Country) + /// Get top 2 Authors (FirstName, LastName, UserName, Email, Age, Country) /// from country Serbia aged 27, with the highest BooksCount /// and all his/her books (Book Name/Title and Publishment Year) published before 1900 /// /// - [Benchmark] + [Benchmark(Baseline = true)] public List GetAuthors() { - using var dbContext = new AppDbContext(); + using var dbContext = _dbContextFactory.CreateDbContext(); var authors = dbContext.Authors .Include(x => x.User) @@ -88,9 +108,234 @@ public List GetAuthors() [Benchmark] public List GetAuthors_Optimized() { - List authors = new List(); + using var dbContext = _dbContextFactory.CreateDbContext(); + + var authors = + dbContext.Authors + .Select(x => new AuthorDTO + { + UserCreated = x.User.Created, + UserEmailConfirmed = x.User.EmailConfirmed, + UserFirstName = x.User.FirstName, + UserLastActivity = x.User.LastActivity, + UserLastName = x.User.LastName, + UserEmail = x.User.Email, + UserName = x.User.UserName, + UserId = x.User.Id, + RoleId = x.User.UserRoles.FirstOrDefault().RoleId, + BooksCount = x.BooksCount, + AllBooks = x.Books.Select(y => new BookDto + { + Id = y.Id, + Name = y.Name, + Published = y.Published, + ISBN = y.ISBN, + PublisherName = y.Publisher.Name, + PublishedYear = y.Published.Year + }).Where(b => b.Published.Year < 1900).ToList(), + AuthorAge = x.Age, + AuthorCountry = x.Country, + AuthorNickName = x.NickName, + Id = x.Id + }) + .Where(x => x.AuthorCountry == "Serbia" && x.AuthorAge == 27); + + var orderedAuthors = authors.OrderByDescending(x => x.BooksCount).Take(2); + + return orderedAuthors.ToList(); + } + + [Benchmark] + public List GetAuthors_Optimized_Indexed() + { + using var dbContext = _indexedDbContextFactory.CreateDbContext(); + + var authors = + dbContext.Authors + .Select(x => new AuthorDTO + { + UserCreated = x.User.Created, + UserEmailConfirmed = x.User.EmailConfirmed, + UserFirstName = x.User.FirstName, + UserLastActivity = x.User.LastActivity, + UserLastName = x.User.LastName, + UserEmail = x.User.Email, + UserName = x.User.UserName, + UserId = x.User.Id, + RoleId = x.User.UserRoles.FirstOrDefault().RoleId, + BooksCount = x.BooksCount, + AllBooks = x.Books.Select(y => new BookDto + { + Id = y.Id, + Name = y.Name, + Published = y.Published, + ISBN = y.ISBN, + PublisherName = y.Publisher.Name, + PublishedYear = y.Published.Year + }).Where(b => b.Published.Year < 1900).ToList(), + AuthorAge = x.Age, + AuthorCountry = x.Country, + AuthorNickName = x.NickName, + Id = x.Id + }) + .Where(x => x.AuthorCountry == "Serbia" && x.AuthorAge == 27); + + var orderedAuthors = authors.OrderByDescending(x => x.BooksCount).Take(2); + + return orderedAuthors.ToList(); + } + + const string dapperSql = """ + SELECT u.Created UserCreated, + u.EmailConfirmed UserEmailConfirmed, + u.FirstName UserFirstName, + u.LastActivity UserLastActivity, + u.LastName UserLastName, + u.Email UserEmail, + u.UserName UserName, + u.Id UserId, + (SELECT TOP 1 RoleId FROM UserRoles ur WHERE ur.UserId = a.UserId) RoleId, + a.BooksCount, + a.Age AuthorAge, a.Country AuthorCountry, a.NickName AuthorNickName, a.Id, + b.Id, b.Name, b.Published, b.ISBN, b.PublisherName, b.PublishedYear + FROM Authors a + JOIN Users u ON u.Id = a.UserId + JOIN ( + SELECT b.Id, b.Name, b.Published, b.ISBN, p.Name PublisherName, DATEPART(YEAR, b.Published) PublishedYear, b.AuthorId + FROM Books b + JOIN Publishers p ON p.Id = b.PublisherId + ) b ON b.AuthorId = a.Id AND PublishedYear < @publishedYear + WHERE a.Country = @country AND a.Age = @age + ORDER BY a.BooksCount, b.Id; + """; + [Benchmark] + public List GetAuthors_Optimized_Dapper() + { + using var dbContext = _dbContextFactory.CreateDbContext(); + + var queriedAuthors = new Dictionary(); + + var authors = dbContext.Database.GetDbConnection().Query( + dapperSql, + (author, book) => + { + if (!queriedAuthors.TryGetValue(author.Id, out var authorEntry)) + { + authorEntry = author; + authorEntry.AllBooks ??= new List(); + queriedAuthors.Add(authorEntry.Id, authorEntry); + } + + if (!authorEntry.AllBooks.Any(b => b.Id == book.Id)) + { + authorEntry.AllBooks.Add(book); + } + + return authorEntry; + }, + new { country = "Serbia", age = 27, publishedYear = 1900 } + ) + .ToList(); - return authors; + return queriedAuthors.Values.ToList(); } + + [Benchmark] + public List GetAuthors_Optimized_DapperIndexed() + { + using var dbContext = _indexedDbContextFactory.CreateDbContext(); + + var queriedAuthors = new Dictionary(); + + var authors = dbContext.Database.GetDbConnection().Query( + dapperSql, + (author, book) => + { + if (!queriedAuthors.TryGetValue(author.Id, out var authorEntry)) + { + authorEntry = author; + authorEntry.AllBooks ??= new List(); + queriedAuthors.Add(authorEntry.Id, authorEntry); + } + + if (!authorEntry.AllBooks.Any(b => b.Id == book.Id)) + { + authorEntry.AllBooks.Add(book); + } + + return authorEntry; + }, + new { country = "Serbia", age = 27, publishedYear = 1900 } + ) + .ToList(); + + return queriedAuthors.Values.ToList(); + } + + /// + /// Not my original work, copied and adapted from https://github.com/qjustfeelitp/OptimizeMePlease_Challange. + /// + /// + [Benchmark] + public IList GetAuthors_Optimized_Expression() + { + using var dbContext = _dbContextFactory.CreateDbContext(); + + return Get(dbContext).ToList(); + } + [Benchmark] + public IList GetAuthors_Optimized_ExpressionIndexed() + { + using var dbContext = _indexedDbContextFactory.CreateDbContext(); + + return Get(dbContext).ToList(); + } + + private const string Serbia = nameof(Serbia); + private const int Age = 27; + private const int Year = 1900; + + private static readonly Expression> AuthorWhereFilterExpression = author => (author.Country == Serbia) && (author.Age == Age); + private static readonly Expression> BookWhereFilterExpression = book => book.Published < EF.Functions.DateFromParts(Year, 1, 1); + + private static readonly Expression> BookSelectorExpression = y => new BookDto + { + Id = y.Id, + Name = y.Name, + Published = y.Published, + ISBN = y.ISBN, + PublisherName = y.Publisher.Name, + PublishedYear = y.Published.Year + }; + + private static readonly Expression> AuthorDtoSelectorExpression = x => new AuthorDTO + { + UserCreated = x.User.Created, + UserEmailConfirmed = x.User.EmailConfirmed, + UserFirstName = x.User.FirstName, + UserLastActivity = x.User.LastActivity, + UserLastName = x.User.LastName, + UserEmail = x.User.Email, + UserName = x.User.UserName, + UserId = x.User.Id, + RoleId = x.User.UserRoles.FirstOrDefault().RoleId, + BooksCount = x.BooksCount, + AllBooks = x.Books.AsQueryable() + .Where(BookWhereFilterExpression) + .Select(BookSelectorExpression) + .ToList(), + AuthorAge = x.Age, + AuthorCountry = x.Country, + AuthorNickName = x.NickName, + Id = x.Id + }; + + private static readonly Func> Get = + EF.CompileQuery((DbContext db) => + db.Set() + .Where(AuthorWhereFilterExpression) + .OrderByDescending(x => x.BooksCount) + .Take(2) + .Select(AuthorDtoSelectorExpression)); } } diff --git a/Context/AppDbContext.cs b/Context/AppDbContext.cs index a4c4b8b..863d527 100644 --- a/Context/AppDbContext.cs +++ b/Context/AppDbContext.cs @@ -5,14 +5,8 @@ namespace OptimizeMePlease.Context { public class AppDbContext : DbContext { - protected override void OnConfiguring(DbContextOptionsBuilder options) + public AppDbContext(DbContextOptions options) : base(options) { - options.UseSqlServer("Server=localhost;Database=OptimizeMePlease;Trusted_Connection=True;Integrated Security=true;MultipleActiveResultSets=true"); - } - - protected override void OnModelCreating(ModelBuilder modelBuilder) - { - base.OnModelCreating(modelBuilder); } public DbSet Users { get; set; } diff --git a/Migrations/20220920221036_Initial.Designer.cs b/Migrations/20220920221036_Initial.Designer.cs index 6516510..13444e4 100644 --- a/Migrations/20220920221036_Initial.Designer.cs +++ b/Migrations/20220920221036_Initial.Designer.cs @@ -4,7 +4,6 @@ using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Metadata; using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; using OptimizeMePlease.Context; namespace OptimizeMePlease.Migrations diff --git a/Migrations/20220925132829_RemovedRoleIdFromUsers.Designer.cs b/Migrations/20220925132829_RemovedRoleIdFromUsers.Designer.cs index b3b0ac4..9dd7fd7 100644 --- a/Migrations/20220925132829_RemovedRoleIdFromUsers.Designer.cs +++ b/Migrations/20220925132829_RemovedRoleIdFromUsers.Designer.cs @@ -4,7 +4,6 @@ using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Metadata; using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; using OptimizeMePlease.Context; namespace OptimizeMePlease.Migrations diff --git a/Migrations/AppDbContextModelSnapshot.cs b/Migrations/AppDbContextModelSnapshot.cs index e9d903d..1d44277 100644 --- a/Migrations/AppDbContextModelSnapshot.cs +++ b/Migrations/AppDbContextModelSnapshot.cs @@ -3,7 +3,6 @@ using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Metadata; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; using OptimizeMePlease.Context; namespace OptimizeMePlease.Migrations diff --git a/OptimizeMePlease.csproj b/OptimizeMePlease.csproj index 7e4fcc9..18c714f 100644 --- a/OptimizeMePlease.csproj +++ b/OptimizeMePlease.csproj @@ -1,20 +1,15 @@ - + Exe - netcoreapp3.1 + net7.0 - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - + + + diff --git a/Program.cs b/Program.cs index b05ee2d..d2a1607 100644 --- a/Program.cs +++ b/Program.cs @@ -1,4 +1,5 @@ using BenchmarkDotNet.Running; +using DeepEqual.Syntax; using Microsoft.Data.SqlClient; using Microsoft.SqlServer.Management.Common; using Microsoft.SqlServer.Management.Smo; @@ -9,33 +10,42 @@ namespace OptimizeMePlease { /// - /// Steps: - /// + /// Steps: + /// /// 1. Create a database with name "OptimizeMePlease" /// 2. Run application Debug/Release mode for the first time. IWillPopulateData method will get the script and populate /// created db. - /// 3. Comment or delete IWillPopulateData() call from Main method. + /// 3. Comment or delete IWillPopulateData() call from Main method. /// 4. Go to BenchmarkService.cs class /// 5. Start coding within GetAuthors_Optimized method - /// GOOD LUCK! :D + /// GOOD LUCK! :D /// public class Program { static void Main(string[] args) { - //Debugging + //Debugging BenchmarkService benchmarkService = new BenchmarkService(); - benchmarkService.GetAuthors(); + var authors = benchmarkService.GetAuthors(); + var optimizedAuthors = benchmarkService.GetAuthors_Optimized(); + var indexedAuthors = benchmarkService.GetAuthors_Optimized_Indexed(); + var expressionAuthors = benchmarkService.GetAuthors_Optimized_Expression(); + var dapperAuthors = benchmarkService.GetAuthors_Optimized_Dapper(); + + authors.ShouldDeepEqual(optimizedAuthors); + authors.ShouldDeepEqual(indexedAuthors); + authors.ShouldDeepEqual(expressionAuthors); + authors.ShouldDeepEqual(dapperAuthors); //Comment me after first execution, please. //IWillPopulateData(); - //BenchmarkRunner.Run(); + BenchmarkRunner.Run(); } public static void IWillPopulateData() { - string sqlConnectionString = @"Server=localhost;Database=OptimizeMePlease;Trusted_Connection=True;Integrated Security=true;MultipleActiveResultSets=true"; + string sqlConnectionString = @"Server=localhost;Database=OptimizeMePlease;Trusted_Connection=True;Integrated Security=true;MultipleActiveResultSets=true;Encrypt=false"; string workingDirectory = Environment.CurrentDirectory; string path = Path.Combine(Directory.GetParent(workingDirectory).Parent.Parent.FullName, @"script.sql");