WebApi + ClientApp, GraphQL, Reflection

This commit is contained in:
Boris Milašinović
2026-05-06 20:55:05 +02:00
parent 8f7c704a90
commit 4fb3de19f6
196 changed files with 10395 additions and 0 deletions

View File

@@ -0,0 +1,143 @@
// <auto-generated> This file has been auto generated by EF Core Power Tools. </auto-generated>
#nullable enable
using System;
using System.Collections.Generic;
using Events.EF.Models;
using Microsoft.EntityFrameworkCore;
namespace Events.EF.Data.MSSQL;
public partial class EventsContext : DbContext
{
public EventsContext(DbContextOptions<EventsContext> options)
: base(options)
{
}
public virtual DbSet<Country> Countries { get; set; }
public virtual DbSet<Event> Events { get; set; }
public virtual DbSet<Person> People { get; set; }
public virtual DbSet<Registration> Registrations { get; set; }
public virtual DbSet<Sport> Sports { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Country>(entity =>
{
entity.HasKey(e => e.Code);
entity.ToTable("Country");
entity.HasIndex(e => e.Name, "UQ_Country_Name").IsUnique();
entity.Property(e => e.Code)
.HasMaxLength(3)
.IsUnicode(false);
entity.Property(e => e.Alpha3)
.HasMaxLength(3)
.IsUnicode(false)
.IsFixedLength();
entity.Property(e => e.Name)
.HasMaxLength(100)
.IsUnicode(false);
});
modelBuilder.Entity<Event>(entity =>
{
entity.ToTable("Event");
entity.Property(e => e.Name)
.HasMaxLength(150)
.IsUnicode(false);
});
modelBuilder.Entity<Person>(entity =>
{
entity.ToTable("Person");
entity.HasIndex(e => new { e.DocumentNumber, e.CountryCode }, "UQ_Person_DocumentNumber_CountryCode").IsUnique();
entity.Property(e => e.AddressCountry)
.HasMaxLength(100)
.IsUnicode(false);
entity.Property(e => e.AddressLine)
.HasMaxLength(200)
.IsUnicode(false);
entity.Property(e => e.City)
.HasMaxLength(100)
.IsUnicode(false);
entity.Property(e => e.ContactPhone)
.HasMaxLength(50)
.IsUnicode(false);
entity.Property(e => e.CountryCode)
.HasMaxLength(3)
.IsUnicode(false);
entity.Property(e => e.DocumentNumber)
.HasMaxLength(50)
.IsUnicode(false);
entity.Property(e => e.Email)
.HasMaxLength(255)
.IsUnicode(false);
entity.Property(e => e.FirstName)
.HasMaxLength(100)
.IsUnicode(false);
entity.Property(e => e.FirstNameTranscription)
.HasMaxLength(100)
.IsUnicode(false);
entity.Property(e => e.LastName)
.HasMaxLength(100)
.IsUnicode(false);
entity.Property(e => e.LastNameTranscription)
.HasMaxLength(100)
.IsUnicode(false);
entity.Property(e => e.PostalCode)
.HasMaxLength(20)
.IsUnicode(false);
entity.HasOne(d => d.CountryCodeNavigation).WithMany(p => p.People)
.HasForeignKey(d => d.CountryCode)
.OnDelete(DeleteBehavior.ClientSetNull)
.HasConstraintName("FK_Person_Country");
});
modelBuilder.Entity<Registration>(entity =>
{
entity.ToTable("Registration");
entity.HasIndex(e => new { e.PersonId, e.SportId, e.EventId }, "UQ_Registration_PersonId_SportId_EventId").IsUnique();
entity.Property(e => e.RegisteredAt).HasDefaultValueSql("(sysutcdatetime())", "DF_Registration_RegisteredAt");
entity.HasOne(d => d.Event).WithMany(p => p.Registrations)
.HasForeignKey(d => d.EventId)
.HasConstraintName("FK_Registration_Event");
entity.HasOne(d => d.Person).WithMany(p => p.Registrations)
.HasForeignKey(d => d.PersonId)
.HasConstraintName("FK_Registration_Person");
entity.HasOne(d => d.Sport).WithMany(p => p.Registrations)
.HasForeignKey(d => d.SportId)
.HasConstraintName("FK_Registration_Sport");
});
modelBuilder.Entity<Sport>(entity =>
{
entity.ToTable("Sport");
entity.HasIndex(e => e.Name, "UQ_Sport_Name").IsUnique();
entity.Property(e => e.Name)
.HasMaxLength(100)
.IsUnicode(false);
});
OnModelCreatingPartial(modelBuilder);
}
partial void OnModelCreatingPartial(ModelBuilder modelBuilder);
}

View File

@@ -0,0 +1,165 @@
// <auto-generated> This file has been auto generated by EF Core Power Tools. </auto-generated>
#nullable enable
using System;
using System.Collections.Generic;
using Events.EF.Models;
using Microsoft.EntityFrameworkCore;
namespace Events.EF.Data.Postgres;
public partial class EventsContext : DbContext
{
public EventsContext(DbContextOptions<EventsContext> options)
: base(options)
{
}
public virtual DbSet<Country> Countries { get; set; }
public virtual DbSet<Event> Events { get; set; }
public virtual DbSet<Person> People { get; set; }
public virtual DbSet<Registration> Registrations { get; set; }
public virtual DbSet<Sport> Sports { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Country>(entity =>
{
entity.HasKey(e => e.Code).HasName("country_pkey");
entity.ToTable("country");
entity.HasIndex(e => e.Name, "country_name_key").IsUnique();
entity.Property(e => e.Code)
.HasMaxLength(3)
.HasColumnName("code");
entity.Property(e => e.Alpha3)
.HasMaxLength(3)
.IsFixedLength()
.HasColumnName("alpha3");
entity.Property(e => e.Name)
.HasMaxLength(100)
.HasColumnName("name");
entity.Property(e => e.Translations)
.HasColumnType("jsonb")
.HasColumnName("translations");
});
modelBuilder.Entity<Event>(entity =>
{
entity.HasKey(e => e.Id).HasName("event_pkey");
entity.ToTable("event");
entity.Property(e => e.Id).HasColumnName("id");
entity.Property(e => e.EventDate).HasColumnName("event_date");
entity.Property(e => e.Name)
.HasMaxLength(150)
.HasColumnName("name");
});
modelBuilder.Entity<Person>(entity =>
{
entity.HasKey(e => e.Id).HasName("person_pkey");
entity.ToTable("person");
entity.HasIndex(e => new { e.DocumentNumber, e.CountryCode }, "person_document_number_country_code_key").IsUnique();
entity.Property(e => e.Id).HasColumnName("id");
entity.Property(e => e.AddressCountry)
.HasMaxLength(100)
.HasColumnName("address_country");
entity.Property(e => e.AddressLine)
.HasMaxLength(200)
.HasColumnName("address_line");
entity.Property(e => e.BirthDate).HasColumnName("birth_date");
entity.Property(e => e.City)
.HasMaxLength(100)
.HasColumnName("city");
entity.Property(e => e.ContactPhone)
.HasMaxLength(50)
.HasColumnName("contact_phone");
entity.Property(e => e.CountryCode)
.HasMaxLength(3)
.HasColumnName("country_code");
entity.Property(e => e.DocumentNumber)
.HasMaxLength(50)
.HasColumnName("document_number");
entity.Property(e => e.Email)
.HasMaxLength(255)
.HasColumnName("email");
entity.Property(e => e.FirstName)
.HasMaxLength(100)
.HasColumnName("first_name");
entity.Property(e => e.FirstNameTranscription)
.HasMaxLength(100)
.HasColumnName("first_name_transcription");
entity.Property(e => e.LastName)
.HasMaxLength(100)
.HasColumnName("last_name");
entity.Property(e => e.LastNameTranscription)
.HasMaxLength(100)
.HasColumnName("last_name_transcription");
entity.Property(e => e.PostalCode)
.HasMaxLength(20)
.HasColumnName("postal_code");
entity.HasOne(d => d.CountryCodeNavigation).WithMany(p => p.People)
.HasForeignKey(d => d.CountryCode)
.OnDelete(DeleteBehavior.ClientSetNull)
.HasConstraintName("person_country_code_fkey");
});
modelBuilder.Entity<Registration>(entity =>
{
entity.HasKey(e => e.Id).HasName("registration_pkey");
entity.ToTable("registration");
entity.HasIndex(e => new { e.PersonId, e.SportId, e.EventId }, "registration_person_id_sport_id_event_id_key").IsUnique();
entity.Property(e => e.Id).HasColumnName("id");
entity.Property(e => e.EventId).HasColumnName("event_id");
entity.Property(e => e.PersonId).HasColumnName("person_id");
entity.Property(e => e.RegisteredAt)
.HasDefaultValueSql("CURRENT_TIMESTAMP")
.HasColumnName("registered_at");
entity.Property(e => e.SportId).HasColumnName("sport_id");
entity.HasOne(d => d.Event).WithMany(p => p.Registrations)
.HasForeignKey(d => d.EventId)
.HasConstraintName("registration_event_id_fkey");
entity.HasOne(d => d.Person).WithMany(p => p.Registrations)
.HasForeignKey(d => d.PersonId)
.HasConstraintName("registration_person_id_fkey");
entity.HasOne(d => d.Sport).WithMany(p => p.Registrations)
.HasForeignKey(d => d.SportId)
.HasConstraintName("registration_sport_id_fkey");
});
modelBuilder.Entity<Sport>(entity =>
{
entity.HasKey(e => e.Id).HasName("sport_pkey");
entity.ToTable("sport");
entity.HasIndex(e => e.Name, "sport_name_key").IsUnique();
entity.Property(e => e.Id).HasColumnName("id");
entity.Property(e => e.Name)
.HasMaxLength(100)
.HasColumnName("name");
});
OnModelCreatingPartial(modelBuilder);
}
partial void OnModelCreatingPartial(ModelBuilder modelBuilder);
}

View File

@@ -0,0 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="10.0.7" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="10.0.1" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,19 @@
// <auto-generated> This file has been auto generated by EF Core Power Tools. </auto-generated>
#nullable enable
using System;
using System.Collections.Generic;
namespace Events.EF.Models;
public partial class Country
{
public string Code { get; set; } = null!;
public string Alpha3 { get; set; } = null!;
public string Name { get; set; } = null!;
public string? Translations { get; set; }
public virtual ICollection<Person> People { get; set; } = new List<Person>();
}

View File

@@ -0,0 +1,17 @@
// <auto-generated> This file has been auto generated by EF Core Power Tools. </auto-generated>
#nullable enable
using System;
using System.Collections.Generic;
namespace Events.EF.Models;
public partial class Event
{
public int Id { get; set; }
public string Name { get; set; } = null!;
public DateOnly EventDate { get; set; }
public virtual ICollection<Registration> Registrations { get; set; } = new List<Registration>();
}

View File

@@ -0,0 +1,41 @@
// <auto-generated> This file has been auto generated by EF Core Power Tools. </auto-generated>
#nullable enable
using System;
using System.Collections.Generic;
namespace Events.EF.Models;
public partial class Person
{
public int Id { get; set; }
public string? FirstName { get; set; }
public string? LastName { get; set; }
public string FirstNameTranscription { get; set; } = null!;
public string LastNameTranscription { get; set; } = null!;
public string? AddressLine { get; set; }
public string? PostalCode { get; set; }
public string? City { get; set; }
public string? AddressCountry { get; set; }
public string? Email { get; set; }
public string? ContactPhone { get; set; }
public DateOnly BirthDate { get; set; }
public string DocumentNumber { get; set; } = null!;
public string CountryCode { get; set; } = null!;
public virtual Country CountryCodeNavigation { get; set; } = null!;
public virtual ICollection<Registration> Registrations { get; set; } = new List<Registration>();
}

View File

@@ -0,0 +1,25 @@
// <auto-generated> This file has been auto generated by EF Core Power Tools. </auto-generated>
#nullable enable
using System;
using System.Collections.Generic;
namespace Events.EF.Models;
public partial class Registration
{
public int Id { get; set; }
public int PersonId { get; set; }
public int SportId { get; set; }
public int EventId { get; set; }
public DateTime RegisteredAt { get; set; }
public virtual Event Event { get; set; } = null!;
public virtual Person Person { get; set; } = null!;
public virtual Sport Sport { get; set; } = null!;
}

View File

@@ -0,0 +1,15 @@
// <auto-generated> This file has been auto generated by EF Core Power Tools. </auto-generated>
#nullable enable
using System;
using System.Collections.Generic;
namespace Events.EF.Models;
public partial class Sport
{
public int Id { get; set; }
public string Name { get; set; } = null!;
public virtual ICollection<Registration> Registrations { get; set; } = new List<Registration>();
}

View File

@@ -0,0 +1,70 @@
{
"CodeGenerationMode": 6,
"ContextClassName": "EventsContext",
"ContextNamespace": null,
"FilterSchemas": false,
"IncludeConnectionString": false,
"IrregularWords": null,
"MinimumProductVersion": "2.6.1465",
"ModelNamespace": null,
"OutputContextPath": "Data\/MSSQL",
"OutputPath": "Models",
"PluralRules": null,
"PreserveCasingWithRegex": true,
"ProjectRootNamespace": "Events.EF",
"Schemas": null,
"SelectedHandlebarsLanguage": 2,
"SelectedToBeGenerated": 0,
"SingularRules": null,
"T4TemplatePath": null,
"Tables": [
{
"Name": "[dbo].[Country]",
"ObjectType": 0
},
{
"Name": "[dbo].[Event]",
"ObjectType": 0
},
{
"Name": "[dbo].[Person]",
"ObjectType": 0
},
{
"Name": "[dbo].[Registration]",
"ObjectType": 0
},
{
"Name": "[dbo].[Sport]",
"ObjectType": 0
}
],
"UiHint": null,
"UncountableWords": null,
"UseAsyncStoredProcedureCalls": true,
"UseBoolPropertiesWithoutDefaultSql": false,
"UseDatabaseNames": false,
"UseDatabaseNamesForRoutines": true,
"UseDateOnlyTimeOnly": true,
"UseDbContextSplitting": false,
"UseDecimalDataAnnotationForSprocResult": true,
"UseFluentApiOnly": true,
"UseHandleBars": false,
"UseHierarchyId": false,
"UseInflector": true,
"UseInternalAccessModifiersForSprocsAndFunctions": false,
"UseLegacyPluralizer": false,
"UseManyToManyEntity": false,
"UseNoDefaultConstructor": true,
"UseNoNavigations": false,
"UseNoObjectFilter": false,
"UseNodaTime": false,
"UseNullableReferences": true,
"UsePrefixNavigationNaming": false,
"UseSchemaFolders": false,
"UseSchemaNamespaces": false,
"UseSpatial": false,
"UseT4": false,
"UseT4Split": false,
"UseTypedTvpParameters": true
}

View File

@@ -0,0 +1,70 @@
{
"CodeGenerationMode": 6,
"ContextClassName": "EventsContext",
"ContextNamespace": null,
"FilterSchemas": false,
"IncludeConnectionString": false,
"IrregularWords": null,
"MinimumProductVersion": "2.6.1465",
"ModelNamespace": null,
"OutputContextPath": "Data\/Postgres",
"OutputPath": "Models",
"PluralRules": null,
"PreserveCasingWithRegex": true,
"ProjectRootNamespace": "Events.EF",
"Schemas": null,
"SelectedHandlebarsLanguage": 2,
"SelectedToBeGenerated": 0,
"SingularRules": null,
"T4TemplatePath": null,
"Tables": [
{
"Name": "public.country",
"ObjectType": 0
},
{
"Name": "public.event",
"ObjectType": 0
},
{
"Name": "public.person",
"ObjectType": 0
},
{
"Name": "public.registration",
"ObjectType": 0
},
{
"Name": "public.sport",
"ObjectType": 0
}
],
"UiHint": null,
"UncountableWords": null,
"UseAsyncStoredProcedureCalls": true,
"UseBoolPropertiesWithoutDefaultSql": false,
"UseDatabaseNames": false,
"UseDatabaseNamesForRoutines": true,
"UseDateOnlyTimeOnly": true,
"UseDbContextSplitting": false,
"UseDecimalDataAnnotationForSprocResult": true,
"UseFluentApiOnly": true,
"UseHandleBars": false,
"UseHierarchyId": false,
"UseInflector": true,
"UseInternalAccessModifiersForSprocsAndFunctions": false,
"UseLegacyPluralizer": false,
"UseManyToManyEntity": false,
"UseNoDefaultConstructor": true,
"UseNoNavigations": false,
"UseNoObjectFilter": false,
"UseNodaTime": false,
"UseNullableReferences": true,
"UsePrefixNavigationNaming": false,
"UseSchemaFolders": false,
"UseSchemaNamespaces": false,
"UseSpatial": false,
"UseT4": false,
"UseT4Split": false,
"UseTypedTvpParameters": true
}

View File

@@ -0,0 +1,4 @@
<Solution>
<Project Path="Events.EF/Events.EF.csproj" />
<Project Path="ReflectionBenchmark/ReflectionBenchmark.csproj" />
</Solution>

View File

@@ -0,0 +1,45 @@
using System.Linq.Expressions;
using System.Reflection;
namespace ReflectionBenchmark;
public static class CompiledMapper
{
public static Func<TSource, TDest> Create<TSource, TDest>()
where TDest : new()
{
var sourceParam = Expression.Parameter(typeof(TSource), "src");
var sourceProps = typeof(TSource)
.GetProperties(BindingFlags.Public | BindingFlags.Instance);
var destProps = typeof(TDest)
.GetProperties(BindingFlags.Public | BindingFlags.Instance);
var bindings = new List<MemberBinding>();
foreach (var destProp in destProps)
{
var sourceProp = sourceProps
.FirstOrDefault(p => p.Name == destProp.Name &&
p.PropertyType == destProp.PropertyType);
if (sourceProp == null)
continue;
var sourceAccess = Expression.Property(sourceParam, sourceProp);
var bind = Expression.Bind(destProp, sourceAccess);
bindings.Add(bind);
}
var newDest = Expression.New(typeof(TDest));
var body = Expression.MemberInit(newDest, bindings);
var lambda = Expression.Lambda<Func<TSource, TDest>>(body, sourceParam);
return lambda.Compile();
}
}

View File

@@ -0,0 +1,124 @@
using AutoMapper;
using BenchmarkDotNet.Attributes;
using Events.EF.Data.Postgres;
using Events.EF.Models;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging.Abstractions;
using Npgsql;
namespace ReflectionBenchmark;
[MemoryDiagnoser]
public class DBAccess
{
IMapper mapper = null!;
IHost host = null!;
IDbContextFactory<EventsContext> dbContextFactory = null!;
string connectionString = string.Empty;
[Params(1, 10, 1000, 10000)]
public int Top { get; set; }
[GlobalSetup]
public void PrepareMappings()
{
host = DISetup.BuildHost([]);
dbContextFactory = host.Services.GetRequiredService<IDbContextFactory<EventsContext>>();
var config = new MapperConfiguration(cfg => cfg.CreateMap<Person, PersonDest>()
.ForMember(d => d.FirstName,
opt => opt.MapFrom(e => e.FirstNameTranscription))
.ForMember(d => d.LastName,
opt => opt.MapFrom(e => e.LastNameTranscription))
.ForMember(d => d.Country,
opt => opt.MapFrom(e => e.CountryCodeNavigation.Name)),
NullLoggerFactory.Instance);
mapper = new Mapper(config);
connectionString = host.Services.GetRequiredService<IConfiguration>().GetConnectionString("EventsPostgres")
?? throw new InvalidOperationException("Missing connection string 'EventsPostgres'.");
}
[GlobalCleanup]
public void Cleanup()
{
host.Dispose();
}
/// <summary>
/// Load top Top people ordered by last name, and then first name
/// storing them in PersonDest manually
/// </summary>
/// <returns></returns>
[Benchmark]
public List<PersonDest> MapManually()
{
using var ctx = dbContextFactory.CreateDbContext();
var query = ctx.People
.OrderBy(p => p.LastNameTranscription)
.ThenBy(p => p.FirstNameTranscription)
.Take(Top)
.Select(p => new PersonDest
{
FirstName = p.FirstNameTranscription,
LastName = p.LastNameTranscription,
Country = p.CountryCodeNavigation.Name
});
var list = query.ToList();
return list;
}
[Benchmark(Baseline = true)]
public List<PersonDest> AdoNet()
{
List<PersonDest> list = new();
using var connection = new NpgsqlConnection(connectionString);
using var command = connection.CreateCommand();
command.CommandText = """
SELECT person.first_name_transcription, person.last_name_transcription, country.name
FROM person
INNER JOIN country ON country.code = person.country_code
ORDER BY person.last_name_transcription, person.first_name_transcription
LIMIT @top
""";
command.CommandType = System.Data.CommandType.Text;
command.Parameters.AddWithValue("top", Top);
connection.Open();
using var reader = command.ExecuteReader();
while(reader.Read())
{
list.Add(new PersonDest
{
FirstName = reader.GetString(0),
LastName = reader.GetString(1),
Country = reader.GetString(2)
});
}
return list;
}
/// <summary>
/// Load top Top people ordered by last name, and then first name
/// storing them in PersonDest using AutoMapper
/// </summary>
/// <returns></returns>
[Benchmark]
public List<PersonDest> MapAutoMapper()
{
using var ctx = dbContextFactory.CreateDbContext();
var query = ctx.People
.OrderBy(p => p.LastNameTranscription)
.ThenBy(p => p.FirstNameTranscription)
.Take(Top);
IQueryable<PersonDest> projectedQuery = mapper.ProjectTo<PersonDest>(query);
var list = projectedQuery.ToList();
return list;
}
}

View File

@@ -0,0 +1,21 @@
using Events.EF.Data.Postgres;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
namespace ReflectionBenchmark;
internal static class DISetup
{
public static IHost BuildHost(string[] args)
{
HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);
builder.Services.AddDbContextFactory<EventsContext>(options => {
options.UseNpgsql(builder.Configuration.GetConnectionString("EventsPostgres"));
});
return builder.Build();
}
}

View File

@@ -0,0 +1,9 @@
namespace ReflectionBenchmark;
public record PersonDest
{
public string FirstName { get; set; } = string.Empty;
public string LastName { get; set; } = string.Empty;
public string Country { get; set; } = string.Empty;
public DateTime Birthday { get; set; }
}

View File

@@ -0,0 +1,9 @@
namespace ReflectionBenchmark;
public class PersonSource
{
public string FirstName { get; set; } = string.Empty;
public string LastName { get; set; } = string.Empty;
public string Country { get; set; } = string.Empty;
public DateTime Birthday { get; set; }
}

View File

@@ -0,0 +1,202 @@
using System.Reflection;
using AutoMapper;
using BenchmarkDotNet.Attributes;
using Bogus;
using Microsoft.Extensions.Logging.Abstractions;
namespace ReflectionBenchmark;
[MemoryDiagnoser]
public class PersonUtil
{
Mapper mapper = null!;
Faker<PersonSource> faker = null!;
List<PersonSource> source = null!;
public record Pair<T, U>(T DestProp, U SourceProp);
static List<Pair<PropertyInfo, PropertyInfo>> mapping = new();
static Dictionary<string, Func<PersonSource, object>> getters = new();
static Dictionary<string, Action<PersonDest, object>> setters = new();
static Func<PersonSource, PersonDest> compiledMapper = null!;
public record AccessPair(
Func<PersonSource, object> Getter,
Action<PersonDest, object> Setter);
static List<AccessPair> accessorMapping = new();
[Params(1000)]
public int ListSize { get; set; }
[GlobalSetup]
public void PrepareMappingsAndData()
{
var config = new MapperConfiguration(cfg => cfg.CreateMap<PersonSource, PersonDest>(),
NullLoggerFactory.Instance);
mapper = new Mapper(config);
faker = new Faker<PersonSource>()
.RuleFor(p => p.FirstName, f => f.Name.FirstName())
.RuleFor(p => p.LastName, f => f.Name.LastName())
.RuleFor(p => p.Country, f => f.Address.Country())
.RuleFor(p => p.Birthday, f => f.Date.Between(new DateTime(1900), DateTime.Now));
source = faker.Generate(ListSize);
Type sourceType = typeof(PersonSource);
if (getters.Count == 0)
{
getters = PropertyAccessorCache.CreateAccessors<PersonSource>();
setters = PropertyAccessorCache.CreateSetters<PersonDest>();
Type destType = typeof(PersonDest);
PropertyInfo[] sourceProperties = sourceType.GetProperties(BindingFlags.Public | BindingFlags.Instance);
PropertyInfo[] destProperties = destType.GetProperties(BindingFlags.Public | BindingFlags.Instance);
foreach (PropertyInfo destProperty in destProperties)
{
PropertyInfo? sourceProperty = sourceProperties.FirstOrDefault(d => d.Name == destProperty.Name);
if (sourceProperty != null)
{
mapping.Add(new Pair<PropertyInfo, PropertyInfo>(destProperty, sourceProperty));
}
if (getters.TryGetValue(destProperty.Name, out var getter) &&
setters.TryGetValue(destProperty.Name, out var setter))
{
accessorMapping.Add(new AccessPair(getter, setter));
}
}
compiledMapper = CompiledMapper.Create<PersonSource, PersonDest>();
}
}
/// <summary>
/// Copy source list to destination list converting each object to an object of another type
/// "Conversion" is done line by line in foreach loop
/// </summary>
/// <returns></returns>
[Benchmark]
public List<PersonDest> MapForeach()
{
List<PersonDest> dest = new List<PersonDest>(source.Count);
foreach (var item in source)
{
dest.Add(new PersonDest
{
FirstName = item.FirstName,
LastName = item.LastName,
Country = item.Country,
Birthday = item.Birthday
});
}
return dest;
}
[Benchmark(Baseline = true)]
public List<PersonDest> MapFor()
{
List<PersonDest> dest = new List<PersonDest>(source.Count);
for (int i = 0; i < source.Count; i++)
{
PersonSource item = source[i];
dest.Add(new PersonDest
{
FirstName = item.FirstName,
LastName = item.LastName,
Country = item.Country,
Birthday = item.Birthday
});
}
return dest;
}
/// <summary>
/// Copy source list to destination list converting each object to an object of another type
/// It is done using Linq instead of foreach loop
/// </summary>
/// <returns></returns>
[Benchmark]
public List<PersonDest> MapLinq()
{
return source.Select(s => new PersonDest
{
FirstName = s.FirstName,
LastName = s.LastName,
Country = s.Country,
Birthday = s.Birthday
}).ToList();
}
/// <summary>
/// Copy source list to destination list converting each object to an object of another type
/// "Conversion" is done using reflection, where mapping is established in each test iteration
/// </summary>
/// <returns></returns>
[Benchmark]
public List<PersonDest> MapWithReflection()
{
List<PersonDest> dest = new List<PersonDest>(source.Count);
foreach (var sourceItem in source)
{
PersonDest personDest = new ();
foreach (var pair in mapping)
{
pair.DestProp.SetValue(personDest, pair.SourceProp.GetValue(sourceItem));
}
dest.Add(personDest);
}
return dest;
}
/// <summary>
/// Copy source list to destination list converting each object to an object of another type
/// "Conversion" is done using reflection, where mapping is established in each test iteration
/// </summary>
/// <returns></returns>
[Benchmark]
public List<PersonDest> WithPropertyAccessor()
{
List<PersonDest> dest = new List<PersonDest>(source.Count);
foreach (var sourceItem in source)
{
PersonDest personDest = new();
foreach (var pair in accessorMapping)
{
pair.Setter(personDest, pair.Getter(sourceItem));
}
dest.Add(personDest);
}
return dest;
}
[Benchmark]
public List<PersonDest> MapWithCompiledExpression()
{
List<PersonDest> dest = new(source.Count);
foreach (var item in source)
{
dest.Add(compiledMapper(item));
}
return dest;
}
/// <summary>
/// Copy source list to destination list converting each object to an object of another type
/// "Conversion" is done with AutoMapper (library made for this purpose)
/// </summary>
/// <returns></returns>
[Benchmark]
public List<PersonDest> MapWithAutoMapper()
{
List<PersonDest> dest = new List<PersonDest>(source.Count);
foreach (var item in source)
{
dest.Add(mapper.Map<PersonDest>(item));
}
return dest;
}
}

View File

@@ -0,0 +1,7 @@
// See https://aka.ms/new-console-template for more information
using BenchmarkDotNet.Running;
using ReflectionBenchmark;
var summary = BenchmarkRunner.Run<PersonUtil>();
//var summary = BenchmarkRunner.Run<DBAccess>();

View File

@@ -0,0 +1,61 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
namespace ReflectionBenchmark;
public class PropertyAccessorCache
{
public static Dictionary<string, Func<T, object>> CreateAccessors<T>()
{
var dict = new Dictionary<string, Func<T, object>>();
foreach (var prop in typeof(T).GetProperties(BindingFlags.Instance | BindingFlags.Public))
{
if (!prop.CanRead) continue;
var param = Expression.Parameter(typeof(T));
var property = Expression.Property(param, prop);
var convert = Expression.Convert(property, typeof(object));
var lambda = Expression.Lambda<Func<T, object>>(convert, param).Compile();
dict[prop.Name] = lambda;
}
return dict;
}
public static Dictionary<string, Action<T, object>> CreateSetters<T>()
{
var dict = new Dictionary<string, Action<T, object>>();
foreach (var prop in typeof(T).GetProperties(BindingFlags.Instance | BindingFlags.Public))
{
if (!prop.CanWrite) continue;
var instanceParam = Expression.Parameter(typeof(T), "instance");
var valueParam = Expression.Parameter(typeof(object), "value");
var property = Expression.Property(instanceParam, prop);
var valueCast = Expression.Convert(valueParam, prop.PropertyType);
var assign = Expression.Assign(property, valueCast);
var lambda = Expression
.Lambda<Action<T, object>>(assign, instanceParam, valueParam)
.Compile();
dict[prop.Name] = lambda;
}
return dict;
}
}

View File

@@ -0,0 +1,38 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<PropertyGroup>
<UserSecretsId>PI</UserSecretsId>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="AutoMapper" Version="16.1.1" />
<PackageReference Include="BenchmarkDotNet" Version="0.15.8" />
<PackageReference Include="Bogus" Version="35.6.5" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.7">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="10.0.7" />
<PackageReference Include="Microsoft.Extensions.Configuration.UserSecrets" Version="10.0.7" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="10.0.7" />
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="10.0.7" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Events.EF\Events.EF.csproj" />
</ItemGroup>
<ItemGroup>
<None Update="appsettings.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>

View File

@@ -0,0 +1,13 @@
{
"ConnectionStrings": {
"EventsPostgres": "Host=localhost;Port=5432;Database=events;Username=sport;Password=pogledaj u user secrets;Persist Security Info=True"
},
"Logging": {
"LogLevel": {
"Default": "Error",
"System": "Error",
"Microsoft": "Error",
"Microsoft.EntityFrameworkCore.Database.Command" : "Error"
}
}
}

View File

@@ -0,0 +1,144 @@
BenchmarkDotNet=v0.13.3, OS=Windows 11 (10.0.22621.963)
11th Gen Intel Core i7-1165G7 2.80GHz, 1 CPU, 8 logical and 4 physical cores
.NET SDK=7.0.101
[Host] : .NET 6.0.12 (6.0.1222.56807), X64 RyuJIT AVX2
DefaultJob : .NET 6.0.12 (6.0.1222.56807), X64 RyuJIT AVX2
| Method | ListSize | Mean | Error | StdDev | Ratio | RatioSD |
|------------------ |--------- |-----------:|---------:|----------:|------:|--------:|
| MapForeach | 10 | 108.5 ns | 2.17 ns | 2.50 ns | 1.00 | 0.00 |
| MapLinq | 10 | 120.2 ns | 2.26 ns | 2.12 ns | 1.11 | 0.03 |
| MapWithReflection | 10 | 4,818.1 ns | 95.43 ns | 194.94 ns | 45.30 | 2.68 |
| MapWithAutoMapper | 10 | 546.3 ns | 10.90 ns | 12.12 ns | 5.04 | 0.15 |
BenchmarkDotNet v0.13.11, Windows 11 (10.0.22621.2861/22H2/2022Update/SunValley2)
13th Gen Intel Core i5-1340P, 1 CPU, 16 logical and 12 physical cores
.NET SDK 8.0.100
[Host] : .NET 6.0.25 (6.0.2523.51912), X64 RyuJIT AVX2
DefaultJob : .NET 6.0.25 (6.0.2523.51912), X64 RyuJIT AVX2
| Method | ListSize | Mean | Error | StdDev | Ratio | RatioSD |
|------------------ |--------- |------------:|----------:|---------:|------:|--------:|
| MapForeach | 10 | 83.14 ns | 0.907 ns | 0.848 ns | 1.00 | 0.00 |
| MapLinq | 10 | 117.53 ns | 1.680 ns | 1.571 ns | 1.41 | 0.03 |
| MapWithReflection | 10 | 3,042.57 ns | 12.641 ns | 9.869 ns | 36.57 | 0.35 |
| MapWithAutoMapper | 10 | 499.42 ns | 3.913 ns | 3.469 ns | 6.00 | 0.08 |
BenchmarkDotNet v0.13.11, Windows 11 (10.0.22621.2861/22H2/2022Update/SunValley2)
13th Gen Intel Core i5-1340P, 1 CPU, 16 logical and 12 physical cores
.NET SDK 8.0.100
[Host] : .NET 7.0.14 (7.0.1423.51910), X64 RyuJIT AVX2
DefaultJob : .NET 7.0.14 (7.0.1423.51910), X64 RyuJIT AVX2
| Method | ListSize | Mean | Error | StdDev | Ratio | RatioSD |
|------------------ |--------- |------------:|----------:|---------:|------:|--------:|
| MapForeach | 10 | 84.94 ns | 0.814 ns | 0.680 ns | 1.00 | 0.00 |
| MapLinq | 10 | 111.17 ns | 2.226 ns | 2.286 ns | 1.31 | 0.03 |
| MapWithReflection | 10 | 1,008.92 ns | 10.223 ns | 9.562 ns | 11.87 | 0.14 |
| MapWithAutoMapper | 10 | 504.64 ns | 5.971 ns | 5.585 ns | 5.94 | 0.07 |
BenchmarkDotNet v0.13.11, Windows 11 (10.0.22621.2861/22H2/2022Update/SunValley2)
13th Gen Intel Core i5-1340P, 1 CPU, 16 logical and 12 physical cores
.NET SDK 8.0.100
[Host] : .NET 8.0.0 (8.0.23.53103), X64 RyuJIT AVX2
DefaultJob : .NET 8.0.0 (8.0.23.53103), X64 RyuJIT AVX2
| Method | ListSize | Mean | Error | StdDev | Ratio | RatioSD |
|------------------ |--------- |----------:|----------:|---------:|------:|--------:|
| MapForeach | 10 | 80.67 ns | 0.663 ns | 0.554 ns | 1.00 | 0.00 |
| MapLinq | 10 | 107.38 ns | 2.131 ns | 1.993 ns | 1.33 | 0.03 |
| MapWithReflection | 10 | 668.54 ns | 10.255 ns | 9.593 ns | 8.30 | 0.14 |
| MapWithAutoMapper | 10 | 424.24 ns | 6.710 ns | 5.603 ns | 5.26 | 0.08 |
BenchmarkDotNet v0.14.0, Windows 11 (10.0.22631.4460/23H2/2023Update/SunValley3)
13th Gen Intel Core i5-1340P, 1 CPU, 16 logical and 12 physical cores
.NET SDK 8.0.404
[Host] : .NET 8.0.11 (8.0.1124.51707), X64 RyuJIT AVX2
DefaultJob : .NET 8.0.11 (8.0.1124.51707), X64 RyuJIT AVX2
| Method | ListSize | Mean | Error | StdDev | Ratio | RatioSD |
|------------------ |--------- |----------:|---------:|---------:|------:|--------:|
| MapForeach | 10 | 78.10 ns | 0.764 ns | 0.715 ns | 1.00 | 0.01 |
| MapLinq | 10 | 108.61 ns | 0.945 ns | 0.838 ns | 1.39 | 0.02 |
| MapWithReflection | 10 | 641.86 ns | 6.721 ns | 6.287 ns | 8.22 | 0.11 |
| MapWithAutoMapper | 10 | 418.63 ns | 2.366 ns | 2.098 ns | 5.36 | 0.05 |
--------------------------
Database in local container:
BenchmarkDotNet v0.13.11, Windows 11 (10.0.22621.2861/22H2/2022Update/SunValley2)
13th Gen Intel Core i5-1340P, 1 CPU, 16 logical and 12 physical cores
.NET SDK 8.0.100
[Host] : .NET 6.0.25 (6.0.2523.51912), X64 RyuJIT AVX2
DefaultJob : .NET 6.0.25 (6.0.2523.51912), X64 RyuJIT AVX2
| Method | Top | Mean | Error | StdDev | Ratio | RatioSD |
|-------------- |---- |---------:|----------:|----------:|------:|--------:|
| MapManually | 10 | 3.391 ms | 0.2923 ms | 0.8527 ms | 1.00 | 0.00 |
| MapAutoMapper | 10 | 3.223 ms | 0.3683 ms | 1.0801 ms | 1.00 | 0.39 |
BenchmarkDotNet v0.13.11, Windows 11 (10.0.22621.2861/22H2/2022Update/SunValley2)
13th Gen Intel Core i5-1340P, 1 CPU, 16 logical and 12 physical cores
.NET SDK 8.0.100
[Host] : .NET 7.0.14 (7.0.1423.51910), X64 RyuJIT AVX2
DefaultJob : .NET 7.0.14 (7.0.1423.51910), X64 RyuJIT AVX2
| Method | Top | Mean | Error | StdDev | Ratio | RatioSD |
|-------------- |---- |---------:|----------:|----------:|------:|--------:|
| MapManually | 10 | 3.283 ms | 0.3443 ms | 1.0044 ms | 1.00 | 0.00 |
| MapAutoMapper | 10 | 2.824 ms | 0.3019 ms | 0.8757 ms | 0.95 | 0.43 |
BenchmarkDotNet v0.14.0, Windows 11 (10.0.22631.4460/23H2/2023Update/SunValley3)
13th Gen Intel Core i5-1340P, 1 CPU, 16 logical and 12 physical cores
.NET SDK 8.0.404
[Host] : .NET 8.0.11 (8.0.1124.51707), X64 RyuJIT AVX2
DefaultJob : .NET 8.0.11 (8.0.1124.51707), X64 RyuJIT AVX2
| Method | Top | Mean | Error | StdDev | Ratio | RatioSD |
|-------------- |---- |---------:|----------:|----------:|------:|--------:|
| MapManually | 10 | 1.143 ms | 0.0227 ms | 0.0651 ms | 1.00 | 0.08 |
| MapAutoMapper | 10 | 1.077 ms | 0.0213 ms | 0.0351 ms | 0.95 | 0.06 |
BenchmarkDotNet v0.14.0, Windows 11 (10.0.26200.7171)
13th Gen Intel Core i5-1340P, 1 CPU, 16 logical and 12 physical cores
.NET SDK 9.0.308
[Host] : .NET 8.0.22 (8.0.2225.52707), X64 RyuJIT AVX2
DefaultJob : .NET 8.0.22 (8.0.2225.52707), X64 RyuJIT AVX2
| Method | Top | Mean | Error | StdDev | Median | Ratio | RatioSD | Gen0 | Gen1 | Allocated | Alloc Ratio |
|-------------- |------ |-----------:|---------:|----------:|-----------:|------:|--------:|--------:|-------:|----------:|------------:|
| MapManually | 1 | 1,562.0 us | 89.21 us | 261.63 us | 1,530.7 us | 1.94 | 0.39 | 11.7188 | 3.9063 | 90.31 KB | 31.23 |
| AdoNet | 1 | 818.2 us | 37.96 us | 103.93 us | 802.5 us | 1.01 | 0.17 | - | - | 2.89 KB | 1.00 |
| MapAutoMapper | 1 | 1,258.5 us | 29.91 us | 82.87 us | 1,234.8 us | 1.56 | 0.20 | 13.6719 | 3.9063 | 88.38 KB | 30.57 |
| | | | | | | | | | | | |
| MapManually | 10 | 1,313.9 us | 48.35 us | 132.35 us | 1,294.8 us | 1.65 | 0.18 | 11.7188 | 3.9063 | 93.7 KB | 19.69 |
| AdoNet | 10 | 796.4 us | 15.77 us | 39.57 us | 788.2 us | 1.00 | 0.07 | - | - | 4.76 KB | 1.00 |
| MapAutoMapper | 10 | 1,335.9 us | 44.21 us | 122.50 us | 1,317.6 us | 1.68 | 0.17 | 11.7188 | 3.9063 | 92.04 KB | 19.34 |
| | | | | | | | | | | | |
| MapManually | 1000 | 1,750.7 us | 66.48 us | 186.41 us | 1,716.0 us | 1.27 | 0.15 | 23.4375 | 3.9063 | 157.96 KB | 4.15 |
| AdoNet | 1000 | 1,387.4 us | 30.22 us | 82.72 us | 1,374.0 us | 1.00 | 0.08 | 5.8594 | - | 38.09 KB | 1.00 |
| MapAutoMapper | 1000 | 1,746.5 us | 49.52 us | 134.71 us | 1,729.9 us | 1.26 | 0.12 | 23.4375 | 3.9063 | 156.25 KB | 4.10 |
| | | | | | | | | | | | |
| MapManually | 10000 | 1,750.1 us | 70.01 us | 198.61 us | 1,707.3 us | 1.28 | 0.17 | 23.4375 | 3.9063 | 157.95 KB | 4.15 |
| AdoNet | 10000 | 1,374.6 us | 33.90 us | 95.63 us | 1,366.8 us | 1.00 | 0.10 | 5.8594 | - | 38.09 KB | 1.00 |
| MapAutoMapper | 10000 | 1,736.7 us | 42.34 us | 116.63 us | 1,726.7 us | 1.27 | 0.12 | 23.4375 | 3.9063 | 156.24 KB | 4.10 |