From 05745a6d25598bda0738ce088e5ebc4bcd8a00ef Mon Sep 17 00:00:00 2001 From: Daniel Beus Date: Thu, 23 Dec 2021 11:10:59 -0700 Subject: [PATCH 01/17] Added the generator project and updated NuGet --- src/.editorconfig | 16 +- .../ContextTests.cs | 61 +++++ ...pository.Generator.IntegrationTests.csproj | 29 +++ .../Post.cs | 21 ++ .../TestingContext.cs | 19 ++ .../User.cs | 28 +++ .../EFRepository.Generator.Tests.csproj | 37 +++ .../GeneratorTests.cs | 52 +++++ .../TestData/AllClasses.txt | 62 +++++ .../EFRepository.Generator.csproj | 62 +++++ .../ExtensionMethodGenerator.cs | 220 ++++++++++++++++++ src/EFRepository.Generator/GlobalUsings.cs | 8 + src/EFRepository.Generator/IsExternalInit.cs | 12 + src/EFRepository.Generator/SyntaxReceiver.cs | 15 ++ src/EFRepository.Generator/tools/install.ps1 | 7 + .../tools/uninstall.ps1 | 7 + src/EFRepository.sln | 62 +++-- src/EFRepository/EFRepository.csproj | 174 +++++++------- src/EFRepository/Mindfire.EFRepository.nuspec | 53 ----- src/EFRepository/Properties/AssemblyInfo.cs | 36 --- src/EFRepository/Repository.cs | 8 +- src/EFRepository/packages.config | 4 - 22 files changed, 772 insertions(+), 221 deletions(-) create mode 100644 src/EFRepository.Generator.IntegrationTests/ContextTests.cs create mode 100644 src/EFRepository.Generator.IntegrationTests/EFRepository.Generator.IntegrationTests.csproj create mode 100644 src/EFRepository.Generator.IntegrationTests/Post.cs create mode 100644 src/EFRepository.Generator.IntegrationTests/TestingContext.cs create mode 100644 src/EFRepository.Generator.IntegrationTests/User.cs create mode 100644 src/EFRepository.Generator.Tests/EFRepository.Generator.Tests.csproj create mode 100644 src/EFRepository.Generator.Tests/GeneratorTests.cs create mode 100644 src/EFRepository.Generator.Tests/TestData/AllClasses.txt create mode 100644 src/EFRepository.Generator/EFRepository.Generator.csproj create mode 100644 src/EFRepository.Generator/ExtensionMethodGenerator.cs create mode 100644 src/EFRepository.Generator/GlobalUsings.cs create mode 100644 src/EFRepository.Generator/IsExternalInit.cs create mode 100644 src/EFRepository.Generator/SyntaxReceiver.cs create mode 100644 src/EFRepository.Generator/tools/install.ps1 create mode 100644 src/EFRepository.Generator/tools/uninstall.ps1 delete mode 100644 src/EFRepository/Mindfire.EFRepository.nuspec delete mode 100644 src/EFRepository/Properties/AssemblyInfo.cs delete mode 100644 src/EFRepository/packages.config diff --git a/src/.editorconfig b/src/.editorconfig index ac61513..ceebb82 100644 --- a/src/.editorconfig +++ b/src/.editorconfig @@ -25,19 +25,15 @@ indent_style = tab # Visual Studio XML Project Files [*.{csproj,vbproj,vcxproj,vcxproj.filters,proj,projitems,shproj}] -indent_size = 2 # XML Configuration Files [*.{xml,config,props,targets,nuspec,resx,ruleset,vsixmanifest,vsct}] -indent_size = 2 # JSON Files [*.{json,json5}] -indent_size = 2 # YAML Files [*.{yml,yaml}] -indent_size = 2 # Markdown Files [*.md] @@ -45,7 +41,6 @@ trim_trailing_whitespace = false # Web Files [*.{htm,html,js,ts,tsx,css,sass,scss,less,svg,vue}] -indent_size = 4 insert_final_newline = true # Batch Files @@ -104,13 +99,13 @@ dotnet_style_null_propagation = true:warning [*.{cs,csx,cake}] # Implicit and explicit types # https://docs.microsoft.com/en-us/visualstudio/ide/editorconfig-code-style-settings-reference#implicit-and-explicit-types -csharp_style_var_for_built_in_types = true:warning +csharp_style_var_for_built_in_types = false:warning csharp_style_var_when_type_is_apparent = true:warning csharp_style_var_elsewhere = true:warning # Expression-bodied members # https://docs.microsoft.com/en-us/visualstudio/ide/editorconfig-code-style-settings-reference#expression_bodied_members -csharp_style_expression_bodied_methods = true:suggestion -csharp_style_expression_bodied_constructors = true:suggestion +csharp_style_expression_bodied_methods = true:warning +csharp_style_expression_bodied_constructors = false:warning csharp_style_expression_bodied_operators = true:warning csharp_style_expression_bodied_properties = true:warning csharp_style_expression_bodied_indexers = true:warning @@ -163,7 +158,7 @@ csharp_space_after_cast = false csharp_space_after_keywords_in_control_flow_statements = true csharp_space_between_method_declaration_parameter_list_parentheses = false csharp_space_between_method_call_parameter_list_parentheses = false -csharp_space_between_parentheses = expressions +csharp_space_between_parentheses = false csharp_space_before_colon_in_inheritance_clause = true csharp_space_after_colon_in_inheritance_clause = true csharp_space_around_binary_operators = before_and_after @@ -181,7 +176,7 @@ csharp_indent_braces = false csharp_space_after_comma = true csharp_space_after_dot = false csharp_space_after_semicolon_in_for_statement = true -csharp_space_around_declaration_statements = do_not_ignore +csharp_space_around_declaration_statements = false csharp_space_before_comma = false csharp_space_before_dot = false csharp_space_before_semicolon_in_for_statement = false @@ -189,6 +184,7 @@ csharp_space_before_open_square_brackets = false csharp_space_between_empty_square_brackets = false csharp_space_between_method_declaration_name_and_open_parenthesis = false csharp_space_between_square_brackets = false +csharp_indent_case_contents_when_block = false ######################### # .NET Naming conventions diff --git a/src/EFRepository.Generator.IntegrationTests/ContextTests.cs b/src/EFRepository.Generator.IntegrationTests/ContextTests.cs new file mode 100644 index 0000000..3bf56d0 --- /dev/null +++ b/src/EFRepository.Generator.IntegrationTests/ContextTests.cs @@ -0,0 +1,61 @@ +using System.Linq; +using Microsoft.EntityFrameworkCore; +using Xunit; +using Shouldly; +using System; + +namespace EFRepository.Generator.IntegrationTests +{ + public class ContextTests + { + [Fact] + public void UserMethodsGenerated() + { + var now = DateTime.Now; + + var users = Enumerable.Range(1, 10) + .Select(i => new User + { + Id = i, + Name = $"User {i}", + Address = $"{i} Fake St.", + Phone = new string(i.ToString().ToCharArray()[0], 10), + Created = now.AddHours(-i), + IsDeleted = i % 2 == 0, + Score = double.Parse($"{i}.{i}{i}{i}") + }); + + var usersQueryable = users.AsQueryable(); + + var user = usersQueryable.ById(1).FirstOrDefault(); + + user.ShouldNotBeNull(); + user.Id.ShouldBe(1); + + user = usersQueryable.ByIsDeleted(false).FirstOrDefault(); + + user.ShouldNotBeNull(); + user.IsDeleted.ShouldBeFalse(); + + user = usersQueryable.ByScore(3.333).FirstOrDefault(); + + user.ShouldNotBeNull(); + user.Score.ShouldBe(3.333); + + var filteredUsers = usersQueryable.WhereCreatedIsAfter(now.AddHours(-5.5)); + + filteredUsers.ShouldNotBeNull(); + filteredUsers.Count().ShouldBe(5); + + filteredUsers = usersQueryable.WhereCreatedIsBefore(now.AddHours(-5.5)); + + filteredUsers.ShouldNotBeNull(); + filteredUsers.Count().ShouldBe(5); + + filteredUsers = usersQueryable.WhereCreatedIsBetween(start: now.AddHours(-5.5), end: now.AddHours(-2.5)); + + filteredUsers.ShouldNotBeNull(); + filteredUsers.Count().ShouldBe(3); + } + } +} \ No newline at end of file diff --git a/src/EFRepository.Generator.IntegrationTests/EFRepository.Generator.IntegrationTests.csproj b/src/EFRepository.Generator.IntegrationTests/EFRepository.Generator.IntegrationTests.csproj new file mode 100644 index 0000000..64d5510 --- /dev/null +++ b/src/EFRepository.Generator.IntegrationTests/EFRepository.Generator.IntegrationTests.csproj @@ -0,0 +1,29 @@ + + + + net6.0 + enable + + false + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + diff --git a/src/EFRepository.Generator.IntegrationTests/Post.cs b/src/EFRepository.Generator.IntegrationTests/Post.cs new file mode 100644 index 0000000..422774e --- /dev/null +++ b/src/EFRepository.Generator.IntegrationTests/Post.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace EFRepository.Generator.IntegrationTests +{ + public record Post + { + public int Id { get; set; } + public string Title { get; set; } + public string Content { get; set; } + + public Post() + { + Title = string.Empty; + Content = string.Empty; + } + } +} diff --git a/src/EFRepository.Generator.IntegrationTests/TestingContext.cs b/src/EFRepository.Generator.IntegrationTests/TestingContext.cs new file mode 100644 index 0000000..9a25bbf --- /dev/null +++ b/src/EFRepository.Generator.IntegrationTests/TestingContext.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; + +namespace EFRepository.Generator.IntegrationTests +{ + public class TestingContext : DbContext + { + public virtual DbSet Users { get; set; } + public virtual DbSet Posts { get; set; } + + public TestingContext(DbContextOptions options) : base(options) + { + } + } +} diff --git a/src/EFRepository.Generator.IntegrationTests/User.cs b/src/EFRepository.Generator.IntegrationTests/User.cs new file mode 100644 index 0000000..f995fa2 --- /dev/null +++ b/src/EFRepository.Generator.IntegrationTests/User.cs @@ -0,0 +1,28 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace EFRepository.Generator.IntegrationTests +{ + public record User + { + public int Id { get; set; } + public string Name { get; set; } + public string Phone { get; set; } + public string Address { get; set; } + public DateTime Created { get; set; } + public bool IsDeleted { get; set; } + public double Score { get; set; } + + public virtual ICollection Posts { get; set; } + + public User() + { + Name = string.Empty; + Phone = string.Empty; + Address = string.Empty; + } + } +} diff --git a/src/EFRepository.Generator.Tests/EFRepository.Generator.Tests.csproj b/src/EFRepository.Generator.Tests/EFRepository.Generator.Tests.csproj new file mode 100644 index 0000000..d3dd159 --- /dev/null +++ b/src/EFRepository.Generator.Tests/EFRepository.Generator.Tests.csproj @@ -0,0 +1,37 @@ + + + net6.0 + enable + enable + + false + + + + + + + PreserveNewest + + + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + diff --git a/src/EFRepository.Generator.Tests/GeneratorTests.cs b/src/EFRepository.Generator.Tests/GeneratorTests.cs new file mode 100644 index 0000000..110d275 --- /dev/null +++ b/src/EFRepository.Generator.Tests/GeneratorTests.cs @@ -0,0 +1,52 @@ +using System; +using System.Reflection; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.EntityFrameworkCore; +using Shouldly; +using Xunit; + +namespace EFRepository.Generator.Tests +{ + public class GeneratorTests + { + [Fact] + public void ExtensionsWereGenerated() + { + string? file = File.ReadAllText(@"./TestData/AllClasses.txt"); + var compilation = CreateCompilation(file); + var generator = new ExtensionMethodGenerator(); + var driver = CSharpGeneratorDriver.Create(generator); + + var name = typeof(DbSet<>); + + driver.RunGeneratorsAndUpdateCompilation(compilation, out var outputCompilation, out var diagnostics); + + diagnostics.IsDefaultOrEmpty.ShouldBeTrue(); + var outputDiag = outputCompilation.GetDiagnostics(); + + var allClasses = outputCompilation.SyntaxTrees.Where(st => st.FilePath.EndsWith(".EFExtensions.g.cs")); + + allClasses.ShouldNotBeNull("No classes were generated"); + + allClasses.Count().ShouldBe(3, "Not all classes were generated."); + + + } + + protected static Compilation CreateCompilation(string source) + => CSharpCompilation.Create("compilation", + new[] { CSharpSyntaxTree.ParseText(source) }, + new[] + { + MetadataReference.CreateFromFile(typeof(DateTime).GetTypeInfo().Assembly.Location), + MetadataReference.CreateFromFile(typeof(Binder).GetTypeInfo().Assembly.Location), + MetadataReference.CreateFromFile(typeof(Attribute).GetTypeInfo().Assembly.Location), + MetadataReference.CreateFromFile(typeof(System.ComponentModel.INotifyPropertyChanged).GetTypeInfo().Assembly.Location), + MetadataReference.CreateFromFile(typeof(MulticastDelegate).GetTypeInfo().Assembly.Location), + MetadataReference.CreateFromFile(typeof(DbContext).GetTypeInfo().Assembly.Location), + MetadataReference.CreateFromFile(typeof(DbSet<>).GetTypeInfo().Assembly.Location), + }, + new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)); + } +} diff --git a/src/EFRepository.Generator.Tests/TestData/AllClasses.txt b/src/EFRepository.Generator.Tests/TestData/AllClasses.txt new file mode 100644 index 0000000..89c04f1 --- /dev/null +++ b/src/EFRepository.Generator.Tests/TestData/AllClasses.txt @@ -0,0 +1,62 @@ +using System; +using Microsoft.EntityFrameworkCore; + +namespace EFRepository.Tests.Classes; + +public class TestContext : DbContext +{ + public virtual DbSet? Orders { get; set; } + public virtual DbSet? OrderItems { get; set; } + public virtual DbSet? Payments { get; set; } + + public TestContext(DbContextOptions options) : base(options) + { } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + + modelBuilder.Entity(entity => + { + + }); + + modelBuilder.Entity(entity => + { + + }); + + modelBuilder.Entity(entity => + { + + }); + } +} + +public record Order +{ + public int Id { get; set; } + public DateTime Created { get; set; } + public decimal Amount => Items.Sum(i => i.Price); + public string CurrencyCode { get; set; } + + public virtual ICollection Items { get; set; } + + public Order() + { + CurrencyCode = string.Empty; + } +} + +public record OrderItem +{ + public int Id { get; set; } + public decimal Price { get; set; } +} + +public record Payment +{ + public int Id { get; set; } + public decimal Amount { get; set; } + public DateTime Created { get; set; } +} \ No newline at end of file diff --git a/src/EFRepository.Generator/EFRepository.Generator.csproj b/src/EFRepository.Generator/EFRepository.Generator.csproj new file mode 100644 index 0000000..ff991d5 --- /dev/null +++ b/src/EFRepository.Generator/EFRepository.Generator.csproj @@ -0,0 +1,62 @@ + + + + netstandard2.0 + enable + enable + 10.0 + + false + + true + EFRepository.Generator + EFRepository.Generator + 1.0.1-alpha2 + Source generator that creates filter methods automatically for IQueryable operations + CodeGenerators, repository, EntityFramework, Mindfire + MIT + https://github.com/MindfireTechnology/EFRepository + https://github.com/MindfireTechnology/EFRepository + git + Dan Beus + Mindfire Technology + 1.0.0.1 + 1.0.0.1 + Mindfire.EFRepository.Generator + Mindfire.EFRepository.Generator + Initial testing release + true + true + Logo.png + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + + + + True + \ + + + diff --git a/src/EFRepository.Generator/ExtensionMethodGenerator.cs b/src/EFRepository.Generator/ExtensionMethodGenerator.cs new file mode 100644 index 0000000..714afee --- /dev/null +++ b/src/EFRepository.Generator/ExtensionMethodGenerator.cs @@ -0,0 +1,220 @@ +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.Text; + +namespace EFRepository.Generator; + +[Generator] +public class ExtensionMethodGenerator : ISourceGenerator +{ + + public void Initialize(GeneratorInitializationContext context) => context.RegisterForSyntaxNotifications(() => new SyntaxReceiver()); + + public void Execute(GeneratorExecutionContext context) + { + if (context.SyntaxReceiver is not SyntaxReceiver receiver) + return; + + var options = ((CSharpCompilation)context.Compilation).SyntaxTrees[0].Options as CSharpParseOptions; + var compilation = context.Compilation; + + var dbContextSymbol = compilation.GetTypeByMetadataName("Microsoft.EntityFrameworkCore.DbContext"); + + if (dbContextSymbol == null) + dbContextSymbol = compilation.GetTypeByMetadataName("Microsoft.EntityFramework.DbContext"); + + if (dbContextSymbol == null) + return; + + var dbSetSymbol = compilation.GetTypeByMetadataName("Microsoft.EntityFrameworkCore.DbSet`1"); + + if (dbSetSymbol == null) + dbSetSymbol = compilation.GetTypeByMetadataName("Microsoft.EntityFramework.DbSet"); + + if (dbSetSymbol == null) + return; + + var dbContexts = receiver.Classes.Select(c => + { + var sourceModel = compilation.GetSemanticModel(c.SyntaxTree); + var sourceSymbol = sourceModel.GetDeclaredSymbol(c); + + if (sourceModel == null || sourceSymbol == null) + return null; + + return new SourceRecord + { + Model = sourceModel, + Symbol = sourceSymbol, + Syntax = c + }; + }) + .Where(c => c != null) + .Where(c => c.Symbol != null && c.Symbol.ContainingSymbol.Equals(c.Symbol.ContainingNamespace, SymbolEqualityComparer.Default) + && c.Symbol.BaseType.Equals(dbContextSymbol, SymbolEqualityComparer.Default)); + // TODO: Need to check that this is actually a DbContext class + + var extensions = dbContexts.SelectMany(dbc => ProcessDbContext(dbc, dbSetSymbol, compilation)); + + foreach (var (FileName, Content) in extensions) + { + context.AddSource(FileName, SourceText.From(Content, Encoding.UTF8)); + } + } + + protected IEnumerable<(string FileName, string Content)> ProcessDbContext(SourceRecord record, INamedTypeSymbol dbSetSymbol, Compilation compilation) + { + var dbSets = record.Symbol.GetMembers() + .Where(m => !m.IsStatic && !m.IsAbstract && !m.IsImplicitlyDeclared + && m.Kind is SymbolKind.Property && m.IsDefinition) + .Cast() + .Where(p => !p.IsReadOnly && !p.IsExtern) + .Where(p => + { + return p.Type.OriginalDefinition.Equals(dbSetSymbol, SymbolEqualityComparer.Default); + }); + + var list = new List<(string FileName, string Content)>(); + + foreach (var dbSet in dbSets) + { + var genericType = ((INamedTypeSymbol)dbSet.Type).TypeArguments[0]; + + if (genericType == null) + continue; + + var sourceCode = BuildExtensionMethod((INamedTypeSymbol)genericType, compilation); + + list.Add(sourceCode); + } + + return list; + } + + protected (string FileName, string Content) BuildExtensionMethod(INamedTypeSymbol dbSetClass, Compilation compilation) + { + string fileName = $"{dbSetClass.Name}.EFExtensions.g.cs"; + + var builder = new StringBuilder($@" +using System; +using System.Linq; + +namespace {dbSetClass.ContainingNamespace} +{{ + public static class {dbSetClass.Name}Extensions + {{ +"); + + var boolSymbol = compilation.GetTypeByMetadataName("System.Boolean"); + var byteSymbol = compilation.GetTypeByMetadataName("System.Byte"); + var shortSymbol = compilation.GetTypeByMetadataName("System.Int16"); + var intSymbol = compilation.GetTypeByMetadataName("System.Int32"); + var longSymbol = compilation.GetTypeByMetadataName("System.Int64"); + var floatSymbol = compilation.GetTypeByMetadataName("System.Single"); + var doubleSymbol = compilation.GetTypeByMetadataName("System.Double"); + var decimalSymbol = compilation.GetTypeByMetadataName("System.Decimal"); + var dateTimeSymbol = compilation.GetTypeByMetadataName("System.DateTime"); + + var members = dbSetClass.GetMembers() + .Where((m) => !m.IsStatic && !m.IsAbstract && !m.IsImplicitlyDeclared && m.IsDefinition + && m.Kind is SymbolKind.Property or SymbolKind.Field); + + foreach (var member in members) + { + ITypeSymbol type; + + if (member.Kind is SymbolKind.Property) + { + var property = (IPropertySymbol)member; + type = property.Type; + } + else + { + var field = (IFieldSymbol)member; + type = field.Type; + } + + if (type.Equals(boolSymbol, SymbolEqualityComparer.Default)) + { + builder.AppendLine(CreateMethod("bool", member.Name)); + } + else if (type.Equals(byteSymbol, SymbolEqualityComparer.Default)) + { + builder.AppendLine(CreateMethod("byte", member.Name)); + } + else if (type.Equals(shortSymbol, SymbolEqualityComparer.Default)) + { + builder.AppendLine(CreateMethod("short", member.Name)); + } + else if (type.Equals(intSymbol, SymbolEqualityComparer.Default)) + { + builder.AppendLine(CreateMethod("int", member.Name)); + } + else if (type.Equals(longSymbol, SymbolEqualityComparer.Default)) + { + builder.AppendLine(CreateMethod("long", member.Name)); + } + else if (type.Equals(floatSymbol, SymbolEqualityComparer.Default)) + { + builder.AppendLine(CreateMethod("float", member.Name)); + } + else if (type.Equals(doubleSymbol, SymbolEqualityComparer.Default)) + { + builder.AppendLine(CreateMethod("double", member.Name)); + } + else if (type.Equals(decimalSymbol, SymbolEqualityComparer.Default)) + { + builder.AppendLine(CreateMethod("decimal", member.Name)); + } + else if (type.Equals(dateTimeSymbol, SymbolEqualityComparer.Default)) + { + builder.AppendLine($@" public static IQueryable<{dbSetClass}> Where{member.Name}IsBefore(this IQueryable<{dbSetClass.Name}> queryable, DateTime value) + {{ + if (queryable == null) + return queryable; + + return queryable.Where(n => n.{member.Name} < value); + }}"); + + builder.AppendLine($@" public static IQueryable<{dbSetClass}> Where{member.Name}IsAfter(this IQueryable<{dbSetClass.Name}> queryable, DateTime value) + {{ + if (queryable == null) + return queryable; + + return queryable.Where(n => n.{member.Name} > value); + }}"); + + builder.AppendLine($@" public static IQueryable<{dbSetClass}> Where{member.Name}IsBetween(this IQueryable<{dbSetClass.Name}> queryable, DateTime start, DateTime end) + {{ + if (queryable == null) + return queryable; + + return queryable.Where(n => n.{member.Name} > start && n.{member.Name} < end); + }}"); + } + + } + + builder.AppendLine(" }") + .AppendLine("}"); + + return (fileName, builder.ToString()); + + string CreateMethod(string type, string memberName) + { + return $@" public static IQueryable<{dbSetClass.Name}> By{memberName}(this IQueryable<{dbSetClass.Name}> queryable, {type} value) + {{ + if (queryable == null) + return queryable; + + return queryable.Where(n => n.{memberName} == value); + }}"; + } + } +} + +public record SourceRecord +{ + public SemanticModel Model { get; init; } + public INamedTypeSymbol Symbol { get; init; } + public TypeDeclarationSyntax Syntax { get; init; } +} diff --git a/src/EFRepository.Generator/GlobalUsings.cs b/src/EFRepository.Generator/GlobalUsings.cs new file mode 100644 index 0000000..f496313 --- /dev/null +++ b/src/EFRepository.Generator/GlobalUsings.cs @@ -0,0 +1,8 @@ +global using System; +global using System.Collections.Generic; +global using System.Linq; +global using System.Text; +global using System.Threading.Tasks; +global using Microsoft.CodeAnalysis; +global using Microsoft.CodeAnalysis.CSharp.Syntax; + diff --git a/src/EFRepository.Generator/IsExternalInit.cs b/src/EFRepository.Generator/IsExternalInit.cs new file mode 100644 index 0000000..e7d203f --- /dev/null +++ b/src/EFRepository.Generator/IsExternalInit.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Text; + +namespace System.Runtime.CompilerServices +{ + [EditorBrowsable(EditorBrowsableState.Never)] + public class IsExternalInit + { + } +} diff --git a/src/EFRepository.Generator/SyntaxReceiver.cs b/src/EFRepository.Generator/SyntaxReceiver.cs new file mode 100644 index 0000000..7c6eee0 --- /dev/null +++ b/src/EFRepository.Generator/SyntaxReceiver.cs @@ -0,0 +1,15 @@ +namespace EFRepository.Generator; + +public class SyntaxReceiver : ISyntaxReceiver +{ + public IEnumerable Classes => ClassList; + protected HashSet ClassList { get; } = new HashSet(); + + public void OnVisitSyntaxNode(SyntaxNode syntaxNode) + { + if (syntaxNode is TypeDeclarationSyntax typeDeclarationSyntax) + { + ClassList.Add(typeDeclarationSyntax); + } + } +} diff --git a/src/EFRepository.Generator/tools/install.ps1 b/src/EFRepository.Generator/tools/install.ps1 new file mode 100644 index 0000000..848d869 --- /dev/null +++ b/src/EFRepository.Generator/tools/install.ps1 @@ -0,0 +1,7 @@ +foreach ($analyzerFilePath in Get-ChildItem $analyzersPath -Filter *.dll) +{ + if($project.Object.AnalyzerReferences) + { + $project.Object.AnalyzerReferences.Add($analyzerFilePath.FullName) + } +} \ No newline at end of file diff --git a/src/EFRepository.Generator/tools/uninstall.ps1 b/src/EFRepository.Generator/tools/uninstall.ps1 new file mode 100644 index 0000000..09e9ada --- /dev/null +++ b/src/EFRepository.Generator/tools/uninstall.ps1 @@ -0,0 +1,7 @@ +foreach ($analyzerFilePath in Get-ChildItem $analyzersPath -Filter *.dll) +{ + if($project.Object.AnalyzerReferences) + { + $project.Object.AnalyzerReferences.Remove($analyzerFilePath.FullName) + } +} \ No newline at end of file diff --git a/src/EFRepository.sln b/src/EFRepository.sln index 1c6292b..3a603d3 100644 --- a/src/EFRepository.sln +++ b/src/EFRepository.sln @@ -1,24 +1,19 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 16 -VisualStudioVersion = 16.0.29519.87 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.31912.275 MinimumVisualStudioVersion = 10.0.40219.1 -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EFRepositoryCore", "EFRepositoryCore\EFRepositoryCore.csproj", "{6C42DC49-A54C-4FFC-80CE-A64551337D3F}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EFRepository-Standard-2_0", "EFRepository-Standard-2_0\EFRepository-Standard-2_0.csproj", "{9AD9EE32-C4F8-4230-B934-A44B7F0C42D0}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UnitTests", "UnitTests\UnitTests.csproj", "{0C6142A2-DC38-4E93-95D3-1B2C78F7FA2A}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EFRepository-Core-3_0", "EFRepository-Core-3_0\EFRepository-Core-3_0.csproj", "{47EDA84E-E593-4C7A-AAFE-42279E20308D}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EFRepository.Generator", "EFRepository.Generator\EFRepository.Generator.csproj", "{C8968136-BD14-4CE0-B603-46375C1316C8}" EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{FF0B9269-0D2A-40E9-9143-EDE411AA80E4}" - ProjectSection(SolutionItems) = preProject - ..\README.md = ..\README.md - EndProjectSection +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Generator", "Generator", "{9D3C7B8A-ABCA-470F-BE2B-8755A86D71F0}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "UnitTestsCore", "UnitTestsCore\UnitTestsCore.csproj", "{3BEE89EA-5183-4A5E-A0CB-54D3A0708243}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EFRepository.Generator.IntegrationTests", "EFRepository.Generator.IntegrationTests\EFRepository.Generator.IntegrationTests.csproj", "{FD1A33DE-FDA2-491D-A66F-9A247D810DA0}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EFRepository", "EFRepository\EFRepository.csproj", "{9D130311-BF55-4C25-8411-3E5117B30AAA}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EFRepository.Generator.Tests", "EFRepository.Generator.Tests\EFRepository.Generator.Tests.csproj", "{C6A4D0BC-6D44-47FA-BE67-496FE156F783}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UnitTests", "UnitTests\UnitTests.csproj", "{0C6142A2-DC38-4E93-95D3-1B2C78F7FA2A}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EFRepository", "EFRepository\EFRepository.csproj", "{5B80FD35-149B-4DA0-A4AE-51E47E38F9E6}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -26,34 +21,35 @@ Global Release|Any CPU = Release|Any CPU EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution - {6C42DC49-A54C-4FFC-80CE-A64551337D3F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {6C42DC49-A54C-4FFC-80CE-A64551337D3F}.Debug|Any CPU.Build.0 = Debug|Any CPU - {6C42DC49-A54C-4FFC-80CE-A64551337D3F}.Release|Any CPU.ActiveCfg = Release|Any CPU - {6C42DC49-A54C-4FFC-80CE-A64551337D3F}.Release|Any CPU.Build.0 = Release|Any CPU - {9AD9EE32-C4F8-4230-B934-A44B7F0C42D0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {9AD9EE32-C4F8-4230-B934-A44B7F0C42D0}.Debug|Any CPU.Build.0 = Debug|Any CPU - {9AD9EE32-C4F8-4230-B934-A44B7F0C42D0}.Release|Any CPU.ActiveCfg = Release|Any CPU - {9AD9EE32-C4F8-4230-B934-A44B7F0C42D0}.Release|Any CPU.Build.0 = Release|Any CPU - {47EDA84E-E593-4C7A-AAFE-42279E20308D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {47EDA84E-E593-4C7A-AAFE-42279E20308D}.Debug|Any CPU.Build.0 = Debug|Any CPU - {47EDA84E-E593-4C7A-AAFE-42279E20308D}.Release|Any CPU.ActiveCfg = Release|Any CPU - {47EDA84E-E593-4C7A-AAFE-42279E20308D}.Release|Any CPU.Build.0 = Release|Any CPU - {3BEE89EA-5183-4A5E-A0CB-54D3A0708243}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {3BEE89EA-5183-4A5E-A0CB-54D3A0708243}.Debug|Any CPU.Build.0 = Debug|Any CPU - {3BEE89EA-5183-4A5E-A0CB-54D3A0708243}.Release|Any CPU.ActiveCfg = Release|Any CPU - {3BEE89EA-5183-4A5E-A0CB-54D3A0708243}.Release|Any CPU.Build.0 = Release|Any CPU - {9D130311-BF55-4C25-8411-3E5117B30AAA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {9D130311-BF55-4C25-8411-3E5117B30AAA}.Debug|Any CPU.Build.0 = Debug|Any CPU - {9D130311-BF55-4C25-8411-3E5117B30AAA}.Release|Any CPU.ActiveCfg = Release|Any CPU - {9D130311-BF55-4C25-8411-3E5117B30AAA}.Release|Any CPU.Build.0 = Release|Any CPU {0C6142A2-DC38-4E93-95D3-1B2C78F7FA2A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {0C6142A2-DC38-4E93-95D3-1B2C78F7FA2A}.Debug|Any CPU.Build.0 = Debug|Any CPU {0C6142A2-DC38-4E93-95D3-1B2C78F7FA2A}.Release|Any CPU.ActiveCfg = Release|Any CPU {0C6142A2-DC38-4E93-95D3-1B2C78F7FA2A}.Release|Any CPU.Build.0 = Release|Any CPU + {C8968136-BD14-4CE0-B603-46375C1316C8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C8968136-BD14-4CE0-B603-46375C1316C8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C8968136-BD14-4CE0-B603-46375C1316C8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C8968136-BD14-4CE0-B603-46375C1316C8}.Release|Any CPU.Build.0 = Release|Any CPU + {FD1A33DE-FDA2-491D-A66F-9A247D810DA0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FD1A33DE-FDA2-491D-A66F-9A247D810DA0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FD1A33DE-FDA2-491D-A66F-9A247D810DA0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FD1A33DE-FDA2-491D-A66F-9A247D810DA0}.Release|Any CPU.Build.0 = Release|Any CPU + {C6A4D0BC-6D44-47FA-BE67-496FE156F783}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C6A4D0BC-6D44-47FA-BE67-496FE156F783}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C6A4D0BC-6D44-47FA-BE67-496FE156F783}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C6A4D0BC-6D44-47FA-BE67-496FE156F783}.Release|Any CPU.Build.0 = Release|Any CPU + {5B80FD35-149B-4DA0-A4AE-51E47E38F9E6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5B80FD35-149B-4DA0-A4AE-51E47E38F9E6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5B80FD35-149B-4DA0-A4AE-51E47E38F9E6}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5B80FD35-149B-4DA0-A4AE-51E47E38F9E6}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {C8968136-BD14-4CE0-B603-46375C1316C8} = {9D3C7B8A-ABCA-470F-BE2B-8755A86D71F0} + {FD1A33DE-FDA2-491D-A66F-9A247D810DA0} = {9D3C7B8A-ABCA-470F-BE2B-8755A86D71F0} + {C6A4D0BC-6D44-47FA-BE67-496FE156F783} = {9D3C7B8A-ABCA-470F-BE2B-8755A86D71F0} + EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {30540B78-91A1-49D9-81FA-D1FEB032E049} EndGlobalSection diff --git a/src/EFRepository/EFRepository.csproj b/src/EFRepository/EFRepository.csproj index 638d8ba..d89f53f 100644 --- a/src/EFRepository/EFRepository.csproj +++ b/src/EFRepository/EFRepository.csproj @@ -1,81 +1,93 @@ - - - - - - Debug - AnyCPU - {9D130311-BF55-4C25-8411-3E5117B30AAA} - Library - Properties - EFRepository - EFRepository - v4.5.2 - win - 512 - true - - - - - true - full - false - bin\Debug\ - TRACE;DEBUG;DOTNETFULL - prompt - 4 - - - pdbonly - true - bin\Release\ - TRACE;DOTNETFULL - prompt - 4 - bin\Release\EFRepository.xml - - - - ..\packages\EntityFramework.6.4.4\lib\net45\EntityFramework.dll - - - ..\packages\EntityFramework.6.4.4\lib\net45\EntityFramework.SqlServer.dll - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. - - - - - - \ No newline at end of file + + + + net6.0;net5.0;netstandard2.0;netstandard2.1;netcoreapp3.0;net452 + 10.0 + + + + Mindfire.EFRepository + 2.0.0-alpha4 + Mindfire Technology + Mindfire EFRepository + EFRepository allows you to use a LINQ-Enabled version of the Repository Pattern for Entity Framework + 2017 + https://github.com/MindfireTechnology/EFRepository + https://github.com/MindfireTechnology/EFRepository + EF, EntityFramework, Entity Framework, Core, NetStandard, Repository Pattern + Added source generator to add helper methods for classes in the DbContext + True + Nate Zaugg, Dan Beus + MIT + True + Logo.png + true + + + + + 1.1.6 + + + + C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.6.2\System.Transactions.dll + + + + + + 2.2.6 + + + + + + 3.1.22 + + + + + + 3.1.22 + + + + + + 5.0.13 + + + + + + 6.0.1 + + + + + + + + + + + + + + + + + + + + + True + \ + + + + + + + + diff --git a/src/EFRepository/Mindfire.EFRepository.nuspec b/src/EFRepository/Mindfire.EFRepository.nuspec deleted file mode 100644 index f1bcb62..0000000 --- a/src/EFRepository/Mindfire.EFRepository.nuspec +++ /dev/null @@ -1,53 +0,0 @@ - - - - Mindfire.EFRepository - 2.0.0-alpha - Nate Zaugg - Mindfire Technology - EF Repository - https://github.com/MindfireTechnology/EFRepository - https://avatars3.githubusercontent.com/u/13372702?v=3&s=200 - false - EFRepository allows you to use a LINQ-Enabled version of the Repository Pattern for Entity Framework - Allows you to use a LINQ-Enabled version of the Repository Pattern for Entity Framework - 2020 - EF, EntityFramework, Entity Framework, Repository Pattern - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/src/EFRepository/Properties/AssemblyInfo.cs b/src/EFRepository/Properties/AssemblyInfo.cs deleted file mode 100644 index 7ec38c7..0000000 --- a/src/EFRepository/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System.Reflection; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; - -// General Information about an assembly is controlled through the following -// set of attributes. Change these attribute values to modify the information -// associated with an assembly. -[assembly: AssemblyTitle("EFRepository")] -[assembly: AssemblyDescription("")] -[assembly: AssemblyConfiguration("")] -[assembly: AssemblyCompany("")] -[assembly: AssemblyProduct("EFRepository")] -[assembly: AssemblyCopyright("Copyright © 2020")] -[assembly: AssemblyTrademark("")] -[assembly: AssemblyCulture("")] - -// Setting ComVisible to false makes the types in this assembly not visible -// to COM components. If you need to access a type in this assembly from -// COM, set the ComVisible attribute to true on that type. -[assembly: ComVisible(false)] - -// The following GUID is for the ID of the typelib if this project is exposed to COM -[assembly: Guid("9d130311-bf55-4c25-8411-3e5117b30aaa")] - -// Version information for an assembly consists of the following four values: -// -// Major Version -// Minor Version -// Build Number -// Revision -// -// You can specify all the values or you can default the Build and Revision Numbers -// by using the '*' as shown below: -// [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("2.0.0.0")] -[assembly: AssemblyFileVersion("2.0.0.0")] diff --git a/src/EFRepository/Repository.cs b/src/EFRepository/Repository.cs index f1bcf4a..9970049 100644 --- a/src/EFRepository/Repository.cs +++ b/src/EFRepository/Repository.cs @@ -7,7 +7,7 @@ using System.Threading.Tasks; using System.ComponentModel; -#if DOTNETFULL +#if NET45_OR_GREATER || NET452_OR_GREATER || NET452 using System.Data.Entity.Infrastructure; using System.Data.Entity; #else @@ -87,7 +87,7 @@ public Repository(DbContext context, bool ownsDataContext = true) { throw new NotSupportedException("A different entity object with the same key already exists in the ChangeTracker"); } - + entry.State = EntityState.Modified; ItemModifing?.Invoke(entity); } @@ -121,7 +121,7 @@ public Repository(DbContext context, bool ownsDataContext = true) public virtual int Save() { CheckDetectChanges(); - + return DataContext.SaveChanges(); } @@ -190,7 +190,7 @@ protected PropertyInfo[] GetKeyProperties() return result; } -#if DOTNETFULL +#if NET45_OR_GREATER public virtual DbEntityEntry GetEntryByKey(TEntity entity) where TEntity : class, new() #else public EntityEntry GetEntryByKey(TEntity entity) where TEntity : class, new() diff --git a/src/EFRepository/packages.config b/src/EFRepository/packages.config deleted file mode 100644 index 9203452..0000000 --- a/src/EFRepository/packages.config +++ /dev/null @@ -1,4 +0,0 @@ - - - - \ No newline at end of file From b494e1caca1f9a251b2407f611935a09429a1103 Mon Sep 17 00:00:00 2001 From: Daniel Beus Date: Thu, 23 Dec 2021 13:21:27 -0700 Subject: [PATCH 02/17] Removing old style of package generation --- .../EFRepository-Core-3_0.csproj | 28 --------- .../EFRepository-Standard-2_0.csproj | 28 --------- .../EFRepository.Generator.csproj | 5 +- src/EFRepository.Generator/tools/install.ps1 | 52 +++++++++++++-- .../tools/uninstall.ps1 | 59 ++++++++++++++++-- src/EFRepository/EFRepository.csproj | 12 ++++ src/EFRepository/Repository.cs | 4 +- src/EFRepository/Transaction.cs | 5 ++ src/EFRepository/logo.png | Bin 9076 -> 0 bytes src/EFRepositoryCore/EFRepositoryCore.csproj | 44 ------------- 10 files changed, 123 insertions(+), 114 deletions(-) delete mode 100644 src/EFRepository-Core-3_0/EFRepository-Core-3_0.csproj delete mode 100644 src/EFRepository-Standard-2_0/EFRepository-Standard-2_0.csproj delete mode 100644 src/EFRepository/logo.png delete mode 100644 src/EFRepositoryCore/EFRepositoryCore.csproj diff --git a/src/EFRepository-Core-3_0/EFRepository-Core-3_0.csproj b/src/EFRepository-Core-3_0/EFRepository-Core-3_0.csproj deleted file mode 100644 index b74e954..0000000 --- a/src/EFRepository-Core-3_0/EFRepository-Core-3_0.csproj +++ /dev/null @@ -1,28 +0,0 @@ - - - netcoreapp3.0 - win - EFRepository - 2.0.0 - Mindfire Technology - EFRepository allows you to use a LINQ-Enabled version of the Repository Pattern for Entity Framework - 2019 - https://github.com/MindfireTechnology/EFRepository - https://avatars3.githubusercontent.com/u/13372702?v=3&s=200 - https://github.com/MindfireTechnology/EFRepository - EF, EntityFramework, Entity Framework, Core, NetStandard, Repository Pattern - EFRepository - - - D:\Projects\EFRepository\src\EFRepository-Core-3_0\EFRepository.xml - - - - - - - - - - - diff --git a/src/EFRepository-Standard-2_0/EFRepository-Standard-2_0.csproj b/src/EFRepository-Standard-2_0/EFRepository-Standard-2_0.csproj deleted file mode 100644 index 922e405..0000000 --- a/src/EFRepository-Standard-2_0/EFRepository-Standard-2_0.csproj +++ /dev/null @@ -1,28 +0,0 @@ - - - netstandard2.0 - win - EFRepository - 2.0.0 - Mindfire Technology - EFRepository allows you to use a LINQ-Enabled version of the Repository Pattern for Entity Framework - 2019 - https://github.com/MindfireTechnology/EFRepository - https://avatars3.githubusercontent.com/u/13372702?v=3&s=200 - https://github.com/MindfireTechnology/EFRepository - EF, EntityFramework, Entity Framework, Core, NetStandard, Repository Pattern - EFRepository - - - D:\Projects\EFRepository\src\EFRepository-Standard-2_0\EFRepository.xml - - - - - - - - - - - diff --git a/src/EFRepository.Generator/EFRepository.Generator.csproj b/src/EFRepository.Generator/EFRepository.Generator.csproj index ff991d5..b725600 100644 --- a/src/EFRepository.Generator/EFRepository.Generator.csproj +++ b/src/EFRepository.Generator/EFRepository.Generator.csproj @@ -7,6 +7,7 @@ 10.0 false + PackageReference true EFRepository.Generator @@ -55,8 +56,8 @@ - True - \ + True + \ diff --git a/src/EFRepository.Generator/tools/install.ps1 b/src/EFRepository.Generator/tools/install.ps1 index 848d869..8019b73 100644 --- a/src/EFRepository.Generator/tools/install.ps1 +++ b/src/EFRepository.Generator/tools/install.ps1 @@ -1,7 +1,49 @@ -foreach ($analyzerFilePath in Get-ChildItem $analyzersPath -Filter *.dll) +param($installPath, $toolsPath, $package, $project) + +$analyzersPaths = Join-Path (Join-Path (Split-Path -Path $toolsPath -Parent) "analyzers" ) * -Resolve + +foreach($analyzersPath in $analyzersPaths) { - if($project.Object.AnalyzerReferences) - { - $project.Object.AnalyzerReferences.Add($analyzerFilePath.FullName) - } + # Install the language agnostic analyzers. + if (Test-Path $analyzersPath) + { + foreach ($analyzerFilePath in Get-ChildItem $analyzersPath -Filter *.dll) + { + if($project.Object.AnalyzerReferences) + { + $project.Object.AnalyzerReferences.Add($analyzerFilePath.FullName) + } + } + } +} + +$project.Type # gives the language name like (C# or VB.NET) +$languageFolder = "" +if($project.Type -eq "C#") +{ + $languageFolder = "cs" +} +if($project.Type -eq "VB.NET") +{ + $languageFolder = "vb" +} +if($languageFolder -eq "") +{ + return +} + +foreach($analyzersPath in $analyzersPaths) +{ + # Install language specific analyzers. + $languageAnalyzersPath = join-path $analyzersPath $languageFolder + if (Test-Path $languageAnalyzersPath) + { + foreach ($analyzerFilePath in Get-ChildItem $languageAnalyzersPath -Filter *.dll) + { + if($project.Object.AnalyzerReferences) + { + $project.Object.AnalyzerReferences.Add($analyzerFilePath.FullName) + } + } + } } \ No newline at end of file diff --git a/src/EFRepository.Generator/tools/uninstall.ps1 b/src/EFRepository.Generator/tools/uninstall.ps1 index 09e9ada..d49eb31 100644 --- a/src/EFRepository.Generator/tools/uninstall.ps1 +++ b/src/EFRepository.Generator/tools/uninstall.ps1 @@ -1,7 +1,56 @@ -foreach ($analyzerFilePath in Get-ChildItem $analyzersPath -Filter *.dll) +param($installPath, $toolsPath, $package, $project) + +$analyzersPaths = Join-Path (Join-Path (Split-Path -Path $toolsPath -Parent) "analyzers" ) * -Resolve + +foreach($analyzersPath in $analyzersPaths) { - if($project.Object.AnalyzerReferences) - { - $project.Object.AnalyzerReferences.Remove($analyzerFilePath.FullName) - } + # Uninstall the language agnostic analyzers. + if (Test-Path $analyzersPath) + { + foreach ($analyzerFilePath in Get-ChildItem $analyzersPath -Filter *.dll) + { + if($project.Object.AnalyzerReferences) + { + $project.Object.AnalyzerReferences.Remove($analyzerFilePath.FullName) + } + } + } +} + +$project.Type # gives the language name like (C# or VB.NET) +$languageFolder = "" +if($project.Type -eq "C#") +{ + $languageFolder = "cs" +} +if($project.Type -eq "VB.NET") +{ + $languageFolder = "vb" +} +if($languageFolder -eq "") +{ + return +} + +foreach($analyzersPath in $analyzersPaths) +{ + # Uninstall language specific analyzers. + $languageAnalyzersPath = join-path $analyzersPath $languageFolder + if (Test-Path $languageAnalyzersPath) + { + foreach ($analyzerFilePath in Get-ChildItem $languageAnalyzersPath -Filter *.dll) + { + if($project.Object.AnalyzerReferences) + { + try + { + $project.Object.AnalyzerReferences.Remove($analyzerFilePath.FullName) + } + catch + { + + } + } + } + } } \ No newline at end of file diff --git a/src/EFRepository/EFRepository.csproj b/src/EFRepository/EFRepository.csproj index d89f53f..143a60d 100644 --- a/src/EFRepository/EFRepository.csproj +++ b/src/EFRepository/EFRepository.csproj @@ -32,36 +32,48 @@ C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.6.2\System.Transactions.dll + + 2.2.6 + + 3.1.22 + + 3.1.22 + + 5.0.13 + + 6.0.1 + + diff --git a/src/EFRepository/Repository.cs b/src/EFRepository/Repository.cs index 9970049..d70fd7a 100644 --- a/src/EFRepository/Repository.cs +++ b/src/EFRepository/Repository.cs @@ -7,7 +7,7 @@ using System.Threading.Tasks; using System.ComponentModel; -#if NET45_OR_GREATER || NET452_OR_GREATER || NET452 +#if NET45_OR_GREATER using System.Data.Entity.Infrastructure; using System.Data.Entity; #else @@ -141,7 +141,7 @@ public virtual Task SaveAsync(CancellationToken cancellationToken) protected virtual void CheckDetectChanges() { -#if DOTNETFULL +#if NET45_OR_GREATER if(!DataContext.Configuration.AutoDetectChangesEnabled && DataContext.Configuration.ProxyCreationEnabled) #else if (!DataContext.ChangeTracker.AutoDetectChangesEnabled && DataContext.ChangeTracker.QueryTrackingBehavior == QueryTrackingBehavior.NoTracking) diff --git a/src/EFRepository/Transaction.cs b/src/EFRepository/Transaction.cs index 45633f2..3c6697d 100644 --- a/src/EFRepository/Transaction.cs +++ b/src/EFRepository/Transaction.cs @@ -3,7 +3,12 @@ using System.Linq; using System.Text; using System.Threading.Tasks; + +#if NETSTANDARD1_4 +using System.Transactions; +#else using System.Transactions; +#endif namespace EFRepository { diff --git a/src/EFRepository/logo.png b/src/EFRepository/logo.png deleted file mode 100644 index e28dcbf9a9cc069823ee9da9926f5c9644c028f5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 9076 zcmdUVRa6{Z&}{=DxFoof-~-Q(GzHU$^RuWr&PP?NJ@I2PU)6w8XnlMI71t};7Uh* z22KvHODl_WCUbKQEe3wIl~vB=bF8!WdXl+cmW^H!*E1$dqy?{k1CUPrah31QQ`5rn zCAJmAA zt{lwxObPgh-03%*m*UPm;C!_76QW-N6h+an{@*QVEcNf8C7rM_^bI3*+Oe+BHI@^FIZm|=X?Ev`(QJy}5`rj%_68}0KD%Ri zKqvfyg3dz19N=p?vIh(Wlq}xo?62po)n)N8O-*lx$%JqkmpIBQnTsQ%ZUD15@ISV$ z?>mR+*KE&L=9e$a1=;8)9!w5Cq2ItRes*5;?=&nJTEf#HGmJeWAYwDQQ-H4FoAV-MZbW4w{BAHZ^5Lh8*+u(+M))O+UM@Na4-nER#HgC753Pb-$iopM;F>+^+Ck|D9!yOhPs3w-q!~H{zYlGungksV*bIM`0`~5rp1@#0& zQV{Hl_5lbZ5WZbXF+D<1pqfIiKoHaLWe3JPxof8xQ^J}7iFLzRG0F~bUrkU5OoS91 z_&7+Z0zvo+c zN{Z^D_IZN1{zdW(fqLJ4)xOrKPIwBs(K;FcU%WHs(^+u~!CX>NxHE?uyu(it*Nz`^3!n!h3%UG4-*4oiWd}s`W+B1E2IbT;Ytm*?dYYp@hegs@>zu zo+#Z$W^m|#3-XDex%k!hv)u8NkgBKAnN8A}c;gr2pX$NKR>tT4riKT^&rr?77%*PC z{)Y%4@qseArjKgpK=GCwiBdrY066rn3TFDwF6L>PFC;?#a*gN#8M`>XXux+}NsHo$ zsDR2%PL+RY5C(cBBZZ{s0|QKPL?NgQz23spU$naPi1F;oiRuVH%^U!xM-nn$Y1UtO zQvDJ4O)qLWr4um%5uy2ld*+jqA7zCX_8xiVCG>V~CO}0ihTvDc0MftAO;d$X>9el9 z&L`wf^=#V$+Jn?Yb~PI#J-xLS2J_8ArpnI=i7+Gwd}8^Je?AT$`{?cMbPEN}g?7aX zG^Ohp%kJ&Gq}b!T{)~c()1-LM4qQs=`^)oRw7mJ%ej;R2Vs7-1y+3Sz0mjBcJA z7cbBxL-e`1=Pf3>1qKaOtBi4gnsTgDy4B2gTjK)W1t;L;TGzNk6F}a>?bl-dE=4wK z@W&(3SM(E3Js{at>8#{WLXm5OXQ(k0CzV7KJO=NbIuMt;v!cJv>}+}SQm z5YSH;k~p3u(7hVH)Bto?I}y4jX#ZXwx%iy8h(P%$7#?~ttJ zcCIlcNF%XhpR4^DFyxz;Eu!maTO;C9SVUCRR`^Ly%gJvxPU(gN)bh^NfLCLvs8g^3 zTGUq|$Ag#s@Aw;!vO137o&~wZlrzSQH65%WPB1h{oyq!Lslw7}SGVJPr)`SZQKy`R z788$zfqa56(Zf;d4QX{+Dy6V!fVL~U2^7d`oA%-l9xWB8k?SX4>k-4JK~D@C;LLqx zC{=52_XxO=-L^w|mH0OU7&hSMXykR6DPIfiq5luC#kGm!O`vn<%_1niM-^ARSgI*} zUS%)O_*B?WtBqF^H0)FDOspostEYN`(2D>&YBpQjMUPYy+vc})SR8{I8+V8Ig8v1g z`sz>eU+d@$=03tou2C`sMiOwZXBv6scocC8#?>3QeJ*xnwTh!YXZeU$-sY9%-Jrq* zFvaa7`@FE^Il2F!ZtM!C{uA$?|Dp@s=Msf@t7aL;sBx;!Qjan7sRoHwE>;CjM}3() zd^n;5t~u4}T`{_(5=Hl4jahVJNv|&*6Z~F+jWhFAd=jlphZGmA=`(pqp)N)tslYl52TfV=K>FrWN`v zS~kEFvfLX?@}(O-J8GV?s7fjN@oB8q{QY=23V=&E(+G5SGBI`> z#&x^+&4le-c9U-~=0lYkx@St;vcum)gpA3#f7_ zgXXJ<_=xaa6Zp#9HphI+HpfSC+_H+!tLg-TKy!OqSZbg6W zP*o5X_A= zmHkSaEu`KTeBLG@pYc&|%`cl>8Nu?$P^VVTwBhTe4Timt-)h3@(hb=jwoiz}K*kOB zl50fu!I#ysn40|OX6}K4UD!x;?dX^VJpw6X4QD^K+0aj%hA(^L#9|ZEqZMG3EO_FE-y4IR zwHt+%1-w4N@3A8;Z(>R}BH4FgrJ!o^kG2J2#wS?vzxb8VwieV3=`C&I@HT9uomOR3@_E=P$&a%6Dk8&M;Unazm>9g+ zn2gldiSpEz3n*<1mT#%V{Vzv`nN;r+mZqi11mDhk4t=*%8{U5ZgbZMo|Itkv{BEeL zdmB%{^l3hSdh#9L7z)aEjpM>c7#h&es4@_^-~}S8eR^EL%q$#l~?N810As3^|FnJVY^Sot7!8j zIXV9;0ewL&4jQ2sfE1b8vUZ?7bo)TNY(s^cyEe>vR8b5CSe>)8gKlRuK%QKR+dI~E zWnAFKSFv&NIX=k17NzSlb9DL(8gXs~4w@EIc$M1K)pvO*DJjvNu`KU|ypmF*n?hz* z4mLIs4gvj})}tkHAF1N$4+F3AFs%jktRZ4T4J_9eTiI6LBbn64H+-cU#h+tH@Xm=S z5g3p~!9;VtQvJVjQ@=p*gk5c7x6|oZ;sZ6!{qjy(dq+n&b=C}PG?Wzq5icUzvCU0n z9b-&fIKn4$T%zK0uBqb%*j({2$sg^^H!NWcZj_NC1!~^mRm9j-9P-epBs{z9g%ZbIr6U1wugn%9zU^Gq6$1F5P~Lp%pnJ}fXJ~J( zJT)j$kVy_)y(xTD(*L!7mHK&JEX%~GO0A+XvuP|S_d4-k(+kdR+K2NB_H+RisNH$@ z=_*!Md#&~NZmOcsF$DaF)#>#c!$X?IjLs-iCz--mAy%&wZJ}RF3V3Fo0nicH-$;`o z^Bg)C)A*AJS62f5Oo7v|6$;J8P;}>!)Za{0skt80{cqXX8K*8+dUg)cfMTlAHBe}^ zdi#|)8Req;q=rz*RmKiPC{&!nteqy?q^s8JrH|-^gml@GZrm@1m3fUXR5di#J7y5W zR_yHTvetSP2jKrvpmka&#@~zWTf*llz;?hdSr2Xl(HX~zc>4KFw`B>3EYm_}%9}5fG zu8S@tArOeX^8L!;#6*oRNOoM{*x1(Z8^56MD7kHmV)^u>_%v@|OGycrR&oAOgGK5q z$OSkw5wq_veVQ^cdkP+o`RxZO3eLKN2HDqX{uV3ZSeswO-Z5;?o*Ml}K0^@SzXbib zXLNv1ZLhD*-6fv*?l6I!Xe?#vKMY*mZ)?J;tUpj=q+g2{aoRs%V>US5nSPLw*@d=~ z(KT}M@;Zhp?}(eQ{)kYd|Mqia#_EOp$#d?jD3NM=U}D-sCqh;3W3HW0T|t3BU7pm^ zAiiTcE%wmZ7+ZW4*&Sze-lHuMKP#j4VLSNFkSOe2-nv=-rELyiUP4!N(9r1eS0-B1 zp}{+wc&{Jloj%U~L(%MXV}L(KI?txX=d0>(%g3XjpFbtiV;TDTc5}o%_Bti=Cohv< z$I2J?^~VJw0WbY0P;EslIBO%s2ikmitP>NZ-bME8aEBjQYUUCUFvUm5WF$yw%mj`o z4H9Ri-_mC_HDTm~7sS;x2=~>Zj^xosA}z~pbQPv`mZqj~I|5J57rvH`2H=H;H_yz? zNqzXRFHU-~60RwTU^%mP`<|Lws{nL16g+wF?=y|Q&mN=FgPMY@(g5A2lv-YhmyPYd z6EQmu1J~kvF79Sob9c6__UR>s@Fg?P)+fWefy9)qjXous9Nv9zR6-f{8ZF8!+N+Dx zfv5Y!?<~{uLh&Awlt$0MrE@-=#>W$`dat3pW2{`b;F_9N1G9Q9Hp!%y1WexPNH%y| zvJnvxb;M&+Q#otUzm14m2%B^eW9s7ZC_iWp(MgzfJJ^rOiP@ z-ZK+gAw>`+&B@51vJ$j6(Ul?Mu3(lX9<&}qC^Nb>_}b2bc*;XZLrF=A;k?~l*2q{v z%Zw<;AI*YpaZn&Hef)-QfC~cCH5|LawoPwq7UG`)U}lSDvIesM)WT$ zw-w=F@K#n6Z$3L*;2}4BAT2_9u9yQ{p5|GN|nZ65a1Nh0SH6Muc=Yj7E%pXnk|DO~q%=J84?k{7DWv zz!!ZjppLV=rp6R^97a7YCH1&A8pXn<=E8>u&Xs7~DwdUz{b{O5;1)|d0rYW>Dwp&s zKMuZ&obgOyz27XjdwcKVcF6*TYaHOkb<@*oXVGCU$3|u=mq#=e(+3v^%^TP>V&*0( zi)$Y~q?H1>jba=xqmpIi1{PNoqNB)$?MZn_v1zuqw=aihg<(&RF0TNWBiwB@jhU~A zOcu5}fsF=YBBF{3+nL5$hoWjsvGTTwZ6L*2Q3n-;gV-xqN- z;+Mqxmu}KzHr_bMu?XzvJp*i~0h^mlBIasEdHEJAg4kjGhlkTk6?lIBO1FI_xV{#X z9y*M2HJwu*6&RB+1dYxx$QBmTDjZO+Y{I29XyFN>DtbqC_PXw`6R(#CUuc8bmGNbZ zfOk~_I%@`<{$ydd9Hykg;7z4tmP}Nyjg`E7iAsh{z&P!zmGP1OaA`vsOJ2XpL%w10jjL?Qc(8D9#89R+{axl^TjcGIkwyc zi*bUB_LqL$)=28fzHYLAO_MhAgv7KxFhXRg!ZydK<##)XJymOtp%@z#MK$1sTRQY5 zc)nXvDdb^f?eE_X4@c^&c7EoBElIE3S{4^lt(q!*&5YoA^TvJc;c9%zvONWLKclxdRtHRa zdcHRB@>sa=eKvC$2I(7+UEP=^VVmxpJiKtVb9#Asyz)SNOJnrVMxI`E+lAq-UuKs+ zCIY6}9lYew!DI;mhgA1nOcn1n(araX^8dWcY5xml(Vz{xBU#6(sro_DdDP|#vMCw< zk#L4vFOO9oa-7Tu>_qwAT^ZqAGb0tDg>}-Qjz_<>fb61{_w%d)ZR;QlmKMCe5JgEV zy_f2CCdIWd?-%WwYko_LEj*U>rXC7(I_ZV&dDKUyDNSMgRR3*7L-v;|Qko;=dT2Y& zKiPefJ!ES;tMqxWF`J}V5~GR%Z+bR2gYOpDRktrGIyke7- ztg~S%9<+R{$W8$R_8N3r=COeQ-M6-6Hsxvb#oR0f?-c^aY7}N3d!MC+nE45N z=+io8((q?zMdi%HnZAd^bi8gfmaP$^L^8InD9U21g5n&@l@D`?BLC!~qcP)If8;9) z=<-nhw{_qIcLpZ^C8D#4VG@vBM{?joB<&pW~ z&v-PCRB|dD$%FTBwY0ZRqMLVfy_2;hHNLWj6MMamSArv3?YlpQG~@)24NcW2{x<%m zp^LMwLV95saCh`r<9s1GV#|v-;5CN!jU6OYihsiPc0>(^z!ox!;0I6ka@u;tz3BH>J4*sdG* zFKSUkW>kP$!VpiKWqz;>D4_8T`37L^cEAA{$*iku;^_D-oro6Wv%xUL>KEELi-!`{JT6?L%+y5jIrb2i^dU~6Vp7Sw?C zhbVi>u$*GyuXU-Pd;QM(aRm;MMf>T;nenuZqK!^>)RLJ<|DVp7d!EAwaLMU6|*eXPmW{YC*Bj!TS(S`Myv=fc7naP) z$vGT8?ScB0abQm9x6!-%#68-0@!qG0l-6@Tm7~@%>q;6|0ElZ6*Emb0vx7aUcTy=4 z1bQ7H3_IfHefEzKkq%q>|E8pK*oCL6AMN@`e&9O2At`jB`M1;d3Xh37VmMU?v)b}I zc_Zgj-s=mg!mxL_o75v;anJOjplQaSX{sL<+)}Ge)e##%IwnM;UID=wwax7A!>8#fTDViV%`?ha1YM2ggb+YtkB%l~d?`x{ z&daXMd%dI+Jr4Z&rIg)r>X_Zao$?{!jl9lerV{Lc+@Q7YUm*Z6l4dNCD;SKdeK3As z-V;LvJ_+Meg!@pqRKG@9XY&4N@*HO=!kOde_;^B5$4w`GYbL!QEgt6>VqO3sUH!@{ z3Yg|;v(0~R(RaSIO-`_A2q@^^1zX_q+Xyc2DJJ0#*%c#5<0)fYcV>)dZWtKviQlraTyAptL*( zgR#xa7bj}|p9V7vGb~06T0WRDLuPBQ+zJ-3o9i~8*djdXYW4G~s04}W7j0XTBpVC#Y%gxJgK z+qgdqnx}DtCM1*2A06oK7?~SjyD7*^pfpU}s$-XA$@Fa*+53yz3!1*0Pjn1nzAy3o za3FinOoa7V1eV@BM`3Fm|7>aK!T#Y&#RZ+m7G#rh;mL!Ey|g%(@E0O$Uk{?3dL!_@ zI_F{aN6@DqT6&YC6@G!gENwmKZePsiu#p*9SlrcC{Y3_@9Y1lI8rFBW8+L15H%Fob z|Fw1Pan}5UzLgY!23&#t%;T9nRR;QruUmba!#<^i^k+&> z&@&<*g?WlMz;WH@q=O~En5m*&Zit^|kVF1F04yGwpa4*9w(Af}?;8@LjKNcC&u25U zji2f|a$$jzyRn)Up6`ZO&@o&nGOO(n$BKI@cjpKEd*oFiGwxjU za=r_jicU!)o|_D>wtunGPaI8y#64~}T6!oe$PR;?WxWl8^BJ}NxKyw`X?5G4r91|3 z0NP|fTxzfj+DuHfwT=aNb0rnalhr__aGB6B$w|H(=ME>Fme}EtT-hqkhz98ymdkcEOLU@B_1~p7LITaG>A&9!yg9D!fn5fo3loILokH^b}BC1tfTcFJ=Quu(X#VCMeA&4nD;E%>psrXuED@G-EWL`DbxP}k3YL#69;P;tu&GJjpPST~ zL({yKzeI{kool^E0o})%_WP}TjL4c2+N%X+6NB5m+% P8bCotRk}*bH1z)f7JgC7 diff --git a/src/EFRepositoryCore/EFRepositoryCore.csproj b/src/EFRepositoryCore/EFRepositoryCore.csproj deleted file mode 100644 index 8db1b61..0000000 --- a/src/EFRepositoryCore/EFRepositoryCore.csproj +++ /dev/null @@ -1,44 +0,0 @@ - - - - netstandard1.4 - win - True - Mindfire.EFRepositoryCore - 2.0.0 - Nate Zaugg - Mindfire Technology - EFRepository allows you to use a LINQ-Enabled version of the Repository Pattern for Entity Framework - 2019 - https://github.com/MindfireTechnology/EFRepository - https://avatars3.githubusercontent.com/u/13372702?v=3&s=200 - https://github.com/MindfireTechnology/EFRepository - EF, EntityFramework, Entity Framework, Core, NetStandard, Repository Pattern - EFRepository - EFRepository - - - - bin\Release\netstandard1.4\EFRepository.xml - - - - - - - - - - - - - - - - - - C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.6.2\System.dll - - - - \ No newline at end of file From d31f8e17206fc7e0957314c1e89fde16cd736175 Mon Sep 17 00:00:00 2001 From: Daniel Beus Date: Mon, 27 Dec 2021 14:46:16 -0700 Subject: [PATCH 03/17] Updated csproj to properly include the generator in the build --- Resources/Logo.png | Bin 0 -> 18437 bytes src/EFRepository/EFRepository.csproj | 12 ------------ 2 files changed, 12 deletions(-) create mode 100644 Resources/Logo.png diff --git a/Resources/Logo.png b/Resources/Logo.png new file mode 100644 index 0000000000000000000000000000000000000000..09ff7143581cedb23fef193fd9fa8bce91a3399a GIT binary patch literal 18437 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 literal 0 HcmV?d00001 diff --git a/src/EFRepository/EFRepository.csproj b/src/EFRepository/EFRepository.csproj index 143a60d..d89f53f 100644 --- a/src/EFRepository/EFRepository.csproj +++ b/src/EFRepository/EFRepository.csproj @@ -32,48 +32,36 @@ C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.6.2\System.Transactions.dll - - 2.2.6 - - 3.1.22 - - 3.1.22 - - 5.0.13 - - 6.0.1 - - From 629fb12c68dff322bdb4fc9abfff323760d9c7a3 Mon Sep 17 00:00:00 2001 From: Daniel Beus Date: Tue, 11 Jan 2022 13:19:50 -0700 Subject: [PATCH 04/17] Finished the update for the generator --- README.md | 231 ++++++++++++++++++--------- build.cmd | 5 - src/EFRepository/EFRepository.csproj | 4 +- src/EFRepository/IRepository.cs | 2 +- 4 files changed, 162 insertions(+), 80 deletions(-) delete mode 100644 build.cmd diff --git a/README.md b/README.md index d8fb5c4..4df3af0 100644 --- a/README.md +++ b/README.md @@ -3,81 +3,168 @@ Use a LINQ-Enabled version of the Repository Pattern for Entity Framework ## Interface +```C# +/// +/// Interface for interacting with data storage through the repository pattern +/// +/// +public interface IRepository : IDisposable +{ + + /// Event that fires when an item is added + event Action ItemAdding; + + /// Event that fires when an itemis modified + event Action ItemModifing; + + /// Event that fires when an item is deleted + event Action ItemDeleting; + + + /// Queriable Entity + IQueryable Query() where TEntity : class, new(); + /// - /// Interface for interacting with data storage through the repository pattern + /// Join another entity /// /// - public interface IRepository : IDisposable where TEntity : class, new() - { - /// Queriable Entity - IQueryable Entity { get; } - - /// - /// Find an entity based on key(s) - /// - /// The key(s) for the table - /// Entity if found, otherwise null - TEntity FindOne(params object[] keys); - - /// - /// Add or update entities - /// - /// Entities to add - void AddOrUpdate(params TEntity[] values); - - /// - /// Add or update entities - /// - /// Entities to add - void AddOrUpdate(IEnumerable collection); - - /// - /// Delete a single entity by key(s) - /// - /// The key(s) for the table - void DeleteOne(params object[] keys); - - /// - /// Delete one or more entities - /// - /// Entities to delete - void Delete(params TEntity[] values); - - /// - /// Delete one or more entities - /// - /// Entities to delete - void Delete(IEnumerable collection); - - /// - /// Save pending changes for the collection - /// - /// Number of affected entities - int Save(); - - /// - /// Save pending changes for the collection async - /// - /// Number of affected entities - Task SaveAsync(); - - /// - /// Save pending changes for the collection async with cancellation - /// - /// Cancelation Token - /// Number of affected entities - Task SaveAsync(CancellationToken cancellationToken); - - /// Event that fires when an item is added - event Action ItemAdded; - - /// Event that fires when an itemis modified - event Action ItemModified; - - /// Event that fires when an item is deleted - event Action ItemDeleted; - -## Goals for 2.0 + /// + IQueryable Join() where TEntity : class, new(); + + /// + /// Find an entity based on key(s) + /// + /// The key(s) for the table + /// Entity if found, otherwise null + TEntity FindOne(params object[] keys) where TEntity : class, new(); + + /// + /// Find an entity based on key(s) + /// + /// The key(s) for the table + /// Entity if found, otherwise null + Task FindOneAsync(params object[] keys) where TEntity : class, new(); + + /// + /// Adds entities explicily, even if a key is present + /// + /// Entities to add + void AddNew(params TEntity[] values) where TEntity : class, new(); + + /// + /// Add or update entities + /// + /// + /// If the key field of the entity is populated with a non-default value, the framework + /// will assume that the entity is being updated. + /// + /// Entities to add + void AddOrUpdate(params TEntity[] values) where TEntity : class, new(); + + /// + /// Add or update entities + /// + /// Entities to add + void AddOrUpdate(IEnumerable collection) where TEntity : class, new(); + + /// + /// Delete a single entity by key(s) + /// + /// The key(s) for the table + void DeleteOne(params object[] keys) where TEntity : class, new(); + + /// + /// Delete one or more entities + /// + /// Entities to delete + void Delete(params TEntity[] values) where TEntity : class, new(); + + /// + /// Delete one or more entities + /// + /// Entities to delete + void Delete(IEnumerable collection) where TEntity : class, new(); + + /// + /// Save pending changes for the collection + /// + /// Number of affected entities + int Save(); + + /// + /// Save pending changes for the collection async + /// + /// Number of affected entities + Task SaveAsync(); + + /// + /// Save pending changes for the collection async with cancellation + /// + /// Cancelation Token + /// Number of affected entities + Task SaveAsync(CancellationToken cancellationToken = default); + + /// + /// Begins a transaction at the specified isolation level + /// + /// Will throw an excpetion if there is already a transaction in progress + /// The desired transaction isolation level + void StartTransaction(IsolationLevel isolation); + + /// + /// Begins a transaction at the specified isolation level + /// + /// Will throw an excpetion if there is already a transaction in progress + /// The desired transaction isolation level + /// Optional cancelation token + void StartTransactionAsync(IsolationLevel isolation, CancellationToken cancellationToken = default); + + /// + /// Begins a transaction at the ReadCommitted isolation level + /// + /// Will throw an excpetion if there is already a transaction in progress + void StartTransaction(); + + /// + /// Begins a transaction at the ReadCommitted isolation level + /// + /// Will throw an excpetion if there is already a transaction in progress + /// Optional cancelation token + void StartTransactionAsync(CancellationToken cancellationToken = default); + + /// + /// Commits an active transaction + /// + /// Will throw an exception if there is not a transaction already in progress + void CommitTransaction(); + + /// + /// Commits an active transaction + /// + /// Will throw an exception if there is not a transaction already in progress + /// Optional cancelation token + void CommitTransactionAsync(CancellationToken cancellationToken = default); + + /// + /// Rolls back the changes for a given transaction + /// + /// Will throw an exception if there is not a transaction already in progress + void RollbackTransaction(); + + /// + /// Rolls back the changes for a given transaction + /// + /// Will throw an exception if there is not a transaction already in progress + /// Optional cancelation token + void RollbackTransactionAsync(CancellationToken cancellationToken = default); + + void EnlistTransaction(IDbTransaction transaction); + + IDbTransaction GetCurrentTransaction(); +} +``` + +## Future Goals - Better handling of client-provided ID's - What is the expected behavior for AddOrUpdate on a new Client side generated ID? - Add an attribute? diff --git a/build.cmd b/build.cmd deleted file mode 100644 index 194a71d..0000000 --- a/build.cmd +++ /dev/null @@ -1,5 +0,0 @@ -pushd src -msbuild EFRepository.sln -p:Configuration=Release --verbosity:quiet - -dotnet pack EFRepository-Core-3_0/EFRepository-Core-3_0.csproj --include-symbols -p:NuspecFile=../EFRepository/Mindfire.EFRepository.nuspec --output ../Releases -c Release -popd diff --git a/src/EFRepository/EFRepository.csproj b/src/EFRepository/EFRepository.csproj index d89f53f..d36a3fe 100644 --- a/src/EFRepository/EFRepository.csproj +++ b/src/EFRepository/EFRepository.csproj @@ -7,7 +7,7 @@ Mindfire.EFRepository - 2.0.0-alpha4 + 2.0.0-alpha5 Mindfire Technology Mindfire EFRepository EFRepository allows you to use a LINQ-Enabled version of the Repository Pattern for Entity Framework @@ -15,7 +15,7 @@ https://github.com/MindfireTechnology/EFRepository https://github.com/MindfireTechnology/EFRepository EF, EntityFramework, Entity Framework, Core, NetStandard, Repository Pattern - Added source generator to add helper methods for classes in the DbContext + Updated to use the latest version of EFRepository in the package. True Nate Zaugg, Dan Beus MIT diff --git a/src/EFRepository/IRepository.cs b/src/EFRepository/IRepository.cs index f3c11b2..f6c54ac 100644 --- a/src/EFRepository/IRepository.cs +++ b/src/EFRepository/IRepository.cs @@ -11,7 +11,7 @@ namespace EFRepository /// Interface for interacting with data storage through the repository pattern /// /// - public interface IRepository : IDisposable + public interface IRepository : IDisposable { /// Event that fires when an item is added From fef7decde6af28feea4ba13ed841b20c23ff452a Mon Sep 17 00:00:00 2001 From: Daniel Beus Date: Tue, 11 Jan 2022 14:55:35 -0700 Subject: [PATCH 05/17] Removed old folders, and added NDOC comments to generated methods --- .../Mindfire.EFRepository.1.0.0-alpha.nupkg | Bin 19525 -> 0 bytes releases/Mindfire.EFRepository.1.0.1.nupkg | Bin 19505 -> 0 bytes .../Mindfire.EFRepository.nuspec | 33 ------ .../Mindfire.EFRepository.nuspec | 33 ------ .../9be12e6db38d45c3b500c40656d3f0ea.psmdcp | 9 -- src/.editorconfig | 10 +- src/EFRepository-Core-3_0/EFRepository.xml | 102 ------------------ .../EFRepository.xml | 102 ------------------ .../ExtensionMethodGenerator.cs | 17 ++- src/EFRepository/EFRepository.csproj | 18 +++- src/EFRepository/IRepository.cs | 58 ---------- src/EFRepository/Repository.cs | 62 +---------- 12 files changed, 36 insertions(+), 408 deletions(-) delete mode 100644 releases/Mindfire.EFRepository.1.0.0-alpha.nupkg delete mode 100644 releases/Mindfire.EFRepository.1.0.1.nupkg delete mode 100644 releases/Mindfire.EFRepository.2.0.0-alpha.symbols/Mindfire.EFRepository.nuspec delete mode 100644 releases/Mindfire.EFRepository.2.0.0/Mindfire.EFRepository.nuspec delete mode 100644 releases/Mindfire.EFRepository.2.0.0/package/services/metadata/core-properties/9be12e6db38d45c3b500c40656d3f0ea.psmdcp delete mode 100644 src/EFRepository-Core-3_0/EFRepository.xml delete mode 100644 src/EFRepository-Standard-2_0/EFRepository.xml diff --git a/releases/Mindfire.EFRepository.1.0.0-alpha.nupkg b/releases/Mindfire.EFRepository.1.0.0-alpha.nupkg deleted file mode 100644 index b97167588a3cb9adc38ce4c7b33738a63fa07e92..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 19525 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 diff --git a/releases/Mindfire.EFRepository.1.0.1.nupkg b/releases/Mindfire.EFRepository.1.0.1.nupkg deleted file mode 100644 index 5183607dec2b48022b583d922fb3e75bc2039828..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 19505 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 - - - Mindfire.EFRepository - 2.0.0-alpha - EF Repository - Nate Zaugg - Mindfire Technology - false - https://github.com/MindfireTechnology/EFRepository - https://avatars3.githubusercontent.com/u/13372702?v=3&s=200 - EFRepository allows you to use a LINQ-Enabled version of the Repository Pattern for Entity Framework - Allows you to use a LINQ-Enabled version of the Repository Pattern for Entity Framework - 2020 - EF, EntityFramework, Entity Framework, Repository Pattern - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/releases/Mindfire.EFRepository.2.0.0/Mindfire.EFRepository.nuspec b/releases/Mindfire.EFRepository.2.0.0/Mindfire.EFRepository.nuspec deleted file mode 100644 index 185a9d3..0000000 --- a/releases/Mindfire.EFRepository.2.0.0/Mindfire.EFRepository.nuspec +++ /dev/null @@ -1,33 +0,0 @@ - - - - Mindfire.EFRepository - 2.0.0 - EF Repository - Nate Zaugg - Mindfire Technology - false - https://github.com/MindfireTechnology/EFRepository - https://avatars3.githubusercontent.com/u/13372702?v=3&s=200 - EFRepository allows you to use a LINQ-Enabled version of the Repository Pattern for Entity Framework - Allows you to use a LINQ-Enabled version of the Repository Pattern for Entity Framework - 2020 - EF, EntityFramework, Entity Framework, Repository Pattern - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/releases/Mindfire.EFRepository.2.0.0/package/services/metadata/core-properties/9be12e6db38d45c3b500c40656d3f0ea.psmdcp b/releases/Mindfire.EFRepository.2.0.0/package/services/metadata/core-properties/9be12e6db38d45c3b500c40656d3f0ea.psmdcp deleted file mode 100644 index 2f17955..0000000 --- a/releases/Mindfire.EFRepository.2.0.0/package/services/metadata/core-properties/9be12e6db38d45c3b500c40656d3f0ea.psmdcp +++ /dev/null @@ -1,9 +0,0 @@ - - - Nate Zaugg - EFRepository allows you to use a LINQ-Enabled version of the Repository Pattern for Entity Framework - Mindfire.EFRepository - 2.0.0 - EF, EntityFramework, Entity Framework, Repository Pattern - NuGet.Build.Tasks.Pack, Version=5.6.0.5, Culture=neutral, PublicKeyToken=31bf3856ad364e35; - \ No newline at end of file diff --git a/src/.editorconfig b/src/.editorconfig index ceebb82..7faee3d 100644 --- a/src/.editorconfig +++ b/src/.editorconfig @@ -104,12 +104,12 @@ csharp_style_var_when_type_is_apparent = true:warning csharp_style_var_elsewhere = true:warning # Expression-bodied members # https://docs.microsoft.com/en-us/visualstudio/ide/editorconfig-code-style-settings-reference#expression_bodied_members -csharp_style_expression_bodied_methods = true:warning +csharp_style_expression_bodied_methods = false:warning csharp_style_expression_bodied_constructors = false:warning -csharp_style_expression_bodied_operators = true:warning -csharp_style_expression_bodied_properties = true:warning -csharp_style_expression_bodied_indexers = true:warning -csharp_style_expression_bodied_accessors = true:warning +csharp_style_expression_bodied_operators = false:warning +csharp_style_expression_bodied_properties = false:warning +csharp_style_expression_bodied_indexers = false:warning +csharp_style_expression_bodied_accessors = false:warning # Pattern matching # https://docs.microsoft.com/en-us/visualstudio/ide/editorconfig-code-style-settings-reference#pattern_matching csharp_style_pattern_matching_over_is_with_cast_check = true:warning diff --git a/src/EFRepository-Core-3_0/EFRepository.xml b/src/EFRepository-Core-3_0/EFRepository.xml deleted file mode 100644 index 8b2846f..0000000 --- a/src/EFRepository-Core-3_0/EFRepository.xml +++ /dev/null @@ -1,102 +0,0 @@ - - - - EFRepository - - - - - Interface for interacting with data storage through the repository pattern - - - - - Event that fires when an item is added - - - Event that fires when an itemis modified - - - Event that fires when an item is deleted - - - Queriable Entity - - - - Join another entity - - - - - - - Find an entity based on key(s) - - The key(s) for the table - Entity if found, otherwise null - - - - Find an entity based on key(s) - - The key(s) for the table - Entity if found, otherwise null - - - - Add or update entities - - Entities to add - - - - Add or update entities - - Entities to add - - - - Add or update entities - - Entities to add - - - - Delete a single entity by key(s) - - The key(s) for the table - - - - Delete one or more entities - - Entities to delete - - - - Delete one or more entities - - Entities to delete - - - - Save pending changes for the collection - - Number of affected entities - - - - Save pending changes for the collection async - - Number of affected entities - - - - Save pending changes for the collection async with cancellation - - Cancelation Token - Number of affected entities - - - diff --git a/src/EFRepository-Standard-2_0/EFRepository.xml b/src/EFRepository-Standard-2_0/EFRepository.xml deleted file mode 100644 index 8b2846f..0000000 --- a/src/EFRepository-Standard-2_0/EFRepository.xml +++ /dev/null @@ -1,102 +0,0 @@ - - - - EFRepository - - - - - Interface for interacting with data storage through the repository pattern - - - - - Event that fires when an item is added - - - Event that fires when an itemis modified - - - Event that fires when an item is deleted - - - Queriable Entity - - - - Join another entity - - - - - - - Find an entity based on key(s) - - The key(s) for the table - Entity if found, otherwise null - - - - Find an entity based on key(s) - - The key(s) for the table - Entity if found, otherwise null - - - - Add or update entities - - Entities to add - - - - Add or update entities - - Entities to add - - - - Add or update entities - - Entities to add - - - - Delete a single entity by key(s) - - The key(s) for the table - - - - Delete one or more entities - - Entities to delete - - - - Delete one or more entities - - Entities to delete - - - - Save pending changes for the collection - - Number of affected entities - - - - Save pending changes for the collection async - - Number of affected entities - - - - Save pending changes for the collection async with cancellation - - Cancelation Token - Number of affected entities - - - diff --git a/src/EFRepository.Generator/ExtensionMethodGenerator.cs b/src/EFRepository.Generator/ExtensionMethodGenerator.cs index 714afee..c5e6019 100644 --- a/src/EFRepository.Generator/ExtensionMethodGenerator.cs +++ b/src/EFRepository.Generator/ExtensionMethodGenerator.cs @@ -167,7 +167,9 @@ public static class {dbSetClass.Name}Extensions } else if (type.Equals(dateTimeSymbol, SymbolEqualityComparer.Default)) { - builder.AppendLine($@" public static IQueryable<{dbSetClass}> Where{member.Name}IsBefore(this IQueryable<{dbSetClass.Name}> queryable, DateTime value) + builder.AppendLine($@"/// Filter the of {dbSetClass.Name} by whether or not the provided is after {member.Name} + /// The that {member.Name} should be before + public static IQueryable<{dbSetClass}> Where{member.Name}IsBefore(this IQueryable<{dbSetClass.Name}> queryable, DateTime value) {{ if (queryable == null) return queryable; @@ -175,7 +177,9 @@ public static class {dbSetClass.Name}Extensions return queryable.Where(n => n.{member.Name} < value); }}"); - builder.AppendLine($@" public static IQueryable<{dbSetClass}> Where{member.Name}IsAfter(this IQueryable<{dbSetClass.Name}> queryable, DateTime value) + builder.AppendLine($@" /// Filter the of {dbSetClass.Name} by whether or not the provided is before {member.Name} + /// The that {member.Name} should be before + public static IQueryable<{dbSetClass}> Where{member.Name}IsAfter(this IQueryable<{dbSetClass.Name}> queryable, DateTime value) {{ if (queryable == null) return queryable; @@ -183,7 +187,10 @@ public static class {dbSetClass.Name}Extensions return queryable.Where(n => n.{member.Name} > value); }}"); - builder.AppendLine($@" public static IQueryable<{dbSetClass}> Where{member.Name}IsBetween(this IQueryable<{dbSetClass.Name}> queryable, DateTime start, DateTime end) + builder.AppendLine($@" /// Filter the of {dbSetClass.Name} by whether or not {member.Name} is between the two provided values. + /// The that should be before {member.Name} + /// The that should be after {member.Name} + public static IQueryable<{dbSetClass}> Where{member.Name}IsBetween(this IQueryable<{dbSetClass.Name}> queryable, DateTime start, DateTime end) {{ if (queryable == null) return queryable; @@ -201,7 +208,9 @@ public static class {dbSetClass.Name}Extensions string CreateMethod(string type, string memberName) { - return $@" public static IQueryable<{dbSetClass.Name}> By{memberName}(this IQueryable<{dbSetClass.Name}> queryable, {type} value) + return $@" ///Filter the of {dbSetClass.Name} by {memberName} + The {type} that {memberName} should be equal to + public static IQueryable<{dbSetClass.Name}> By{memberName}(this IQueryable<{dbSetClass.Name}> queryable, {type} value) {{ if (queryable == null) return queryable; diff --git a/src/EFRepository/EFRepository.csproj b/src/EFRepository/EFRepository.csproj index d36a3fe..c7c68bf 100644 --- a/src/EFRepository/EFRepository.csproj +++ b/src/EFRepository/EFRepository.csproj @@ -1,4 +1,4 @@ - + net6.0;net5.0;netstandard2.0;netstandard2.1;netcoreapp3.0;net452 @@ -11,7 +11,7 @@ Mindfire Technology Mindfire EFRepository EFRepository allows you to use a LINQ-Enabled version of the Repository Pattern for Entity Framework - 2017 + 2021 https://github.com/MindfireTechnology/EFRepository https://github.com/MindfireTechnology/EFRepository EF, EntityFramework, Entity Framework, Core, NetStandard, Repository Pattern @@ -22,12 +22,15 @@ True Logo.png true + snupkg + true 1.1.6 + C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.6.2\System.Transactions.dll @@ -38,30 +41,35 @@ 2.2.6 + 3.1.22 + 3.1.22 + 5.0.13 + 6.0.1 + @@ -90,4 +98,10 @@ + diff --git a/src/EFRepository/IRepository.cs b/src/EFRepository/IRepository.cs index f6c54ac..878c36d 100644 --- a/src/EFRepository/IRepository.cs +++ b/src/EFRepository/IRepository.cs @@ -106,63 +106,5 @@ public interface IRepository : IDisposable /// Cancelation Token /// Number of affected entities Task SaveAsync(CancellationToken cancellationToken = default); - - /// - /// Begins a transaction at the specified isolation level - /// - /// Will throw an excpetion if there is already a transaction in progress - /// The desired transaction isolation level - void StartTransaction(IsolationLevel isolation); - - /// - /// Begins a transaction at the specified isolation level - /// - /// Will throw an excpetion if there is already a transaction in progress - /// The desired transaction isolation level - /// Optional cancelation token - void StartTransactionAsync(IsolationLevel isolation, CancellationToken cancellationToken = default); - - /// - /// Begins a transaction at the ReadCommitted isolation level - /// - /// Will throw an excpetion if there is already a transaction in progress - void StartTransaction(); - - /// - /// Begins a transaction at the ReadCommitted isolation level - /// - /// Will throw an excpetion if there is already a transaction in progress - /// Optional cancelation token - void StartTransactionAsync(CancellationToken cancellationToken = default); - - /// - /// Commits an active transaction - /// - /// Will throw an exception if there is not a transaction already in progress - void CommitTransaction(); - - /// - /// Commits an active transaction - /// - /// Will throw an exception if there is not a transaction already in progress - /// Optional cancelation token - void CommitTransactionAsync(CancellationToken cancellationToken = default); - - /// - /// Rolls back the changes for a given transaction - /// - /// Will throw an exception if there is not a transaction already in progress - void RollbackTransaction(); - - /// - /// Rolls back the changes for a given transaction - /// - /// Will throw an exception if there is not a transaction already in progress - /// Optional cancelation token - void RollbackTransactionAsync(CancellationToken cancellationToken = default); - - void EnlistTransaction(IDbTransaction transaction); - - IDbTransaction GetCurrentTransaction(); } } diff --git a/src/EFRepository/Repository.cs b/src/EFRepository/Repository.cs index d70fd7a..32d902a 100644 --- a/src/EFRepository/Repository.cs +++ b/src/EFRepository/Repository.cs @@ -34,10 +34,7 @@ public Repository(DbContext context, bool ownsDataContext = true) OwnsDataContext = ownsDataContext; } - public virtual IQueryable Query() where TEntity : class, new() - { - return DataContext.Set(); - } + public virtual IQueryable Query() where TEntity : class, new() => DataContext.Set(); public virtual IQueryable Join() where TEntity : class, new() { @@ -181,7 +178,7 @@ protected PropertyInfo[] GetKeyProperties() if (keyProperties.Length != keyValues.Length) throw new ArgumentOutOfRangeException(nameof(keyValues), $"Expected {keyProperties.Length} values, but got {keyValues?.Length ?? 0} instead."); - TEntity result = new TEntity(); + var result = new TEntity(); for (int index = 0; index < keyProperties.Length; index++) { keyProperties[index].SetValue(result, keyValues[index]); @@ -239,61 +236,6 @@ protected PropertyInfo[] GetKeyProperties() return false; } - - public void StartTransaction(System.Data.IsolationLevel isolation) - { - throw new NotImplementedException(); -#if DOTNETFULL - DataContext.Database.BeginTransaction(isolation); -#else - DataContext.Database.BeginTransaction(); -#endif - } - - public void StartTransactionAsync(System.Data.IsolationLevel isolation, CancellationToken cancellationToken = default) - { - throw new NotImplementedException(); - } - - public void StartTransaction() - { - throw new NotImplementedException(); - } - - public void StartTransactionAsync(CancellationToken cancellationToken = default) - { - throw new NotImplementedException(); - } - - public void CommitTransaction() - { - throw new NotImplementedException(); - } - - public void CommitTransactionAsync(CancellationToken cancellationToken = default) - { - throw new NotImplementedException(); - } - - public void RollbackTransaction() - { - throw new NotImplementedException(); - } - - public void RollbackTransactionAsync(CancellationToken cancellationToken = default) - { - throw new NotImplementedException(); - } - - public void EnlistTransaction(System.Data.IDbTransaction transaction) - { - throw new NotImplementedException(); - } - - public System.Data.IDbTransaction GetCurrentTransaction() - { - throw new NotImplementedException(); - } } } #pragma warning restore CS1591 // Missing XML comment for publicly visible type or member From a2f5ec3ce076f586bf91210712131c5a5d9804ea Mon Sep 17 00:00:00 2001 From: Daniel Beus Date: Tue, 11 Jan 2022 16:25:10 -0700 Subject: [PATCH 06/17] Removed an unimplemented method and cleaned up some comment code --- .../ExtensionMethodGenerator.cs | 20 +++++++++++++------ src/EFRepository/EFRepository.csproj | 15 +++++++++++--- src/EFRepository/IRepository.cs | 7 ------- src/EFRepository/Repository.cs | 7 ++----- 4 files changed, 28 insertions(+), 21 deletions(-) diff --git a/src/EFRepository.Generator/ExtensionMethodGenerator.cs b/src/EFRepository.Generator/ExtensionMethodGenerator.cs index c5e6019..bba9f3e 100644 --- a/src/EFRepository.Generator/ExtensionMethodGenerator.cs +++ b/src/EFRepository.Generator/ExtensionMethodGenerator.cs @@ -100,7 +100,7 @@ public void Execute(GeneratorExecutionContext context) namespace {dbSetClass.ContainingNamespace} {{ - public static class {dbSetClass.Name}Extensions + public static partial class {dbSetClass.Name}Extensions {{ "); @@ -167,7 +167,9 @@ public static class {dbSetClass.Name}Extensions } else if (type.Equals(dateTimeSymbol, SymbolEqualityComparer.Default)) { - builder.AppendLine($@"/// Filter the of {dbSetClass.Name} by whether or not the provided is after {member.Name} + builder.AppendLine($@" /// + /// Filter the of {dbSetClass.Name} by whether or not the provided is after {member.Name} + /// /// The that {member.Name} should be before public static IQueryable<{dbSetClass}> Where{member.Name}IsBefore(this IQueryable<{dbSetClass.Name}> queryable, DateTime value) {{ @@ -177,7 +179,9 @@ public static class {dbSetClass.Name}Extensions return queryable.Where(n => n.{member.Name} < value); }}"); - builder.AppendLine($@" /// Filter the of {dbSetClass.Name} by whether or not the provided is before {member.Name} + builder.AppendLine($@" /// + /// Filter the of {dbSetClass.Name} by whether or not the provided is before {member.Name} + /// /// The that {member.Name} should be before public static IQueryable<{dbSetClass}> Where{member.Name}IsAfter(this IQueryable<{dbSetClass.Name}> queryable, DateTime value) {{ @@ -187,7 +191,9 @@ public static class {dbSetClass.Name}Extensions return queryable.Where(n => n.{member.Name} > value); }}"); - builder.AppendLine($@" /// Filter the of {dbSetClass.Name} by whether or not {member.Name} is between the two provided values. + builder.AppendLine($@" /// + /// Filter the of {dbSetClass.Name} by whether or not {member.Name} is between the two provided values. + /// /// The that should be before {member.Name} /// The that should be after {member.Name} public static IQueryable<{dbSetClass}> Where{member.Name}IsBetween(this IQueryable<{dbSetClass.Name}> queryable, DateTime start, DateTime end) @@ -208,8 +214,10 @@ public static class {dbSetClass.Name}Extensions string CreateMethod(string type, string memberName) { - return $@" ///Filter the of {dbSetClass.Name} by {memberName} - The {type} that {memberName} should be equal to + return $@" /// + /// Filter the of {dbSetClass.Name} by {memberName} + /// + /// The {type} that {memberName} should be equal to public static IQueryable<{dbSetClass.Name}> By{memberName}(this IQueryable<{dbSetClass.Name}> queryable, {type} value) {{ if (queryable == null) diff --git a/src/EFRepository/EFRepository.csproj b/src/EFRepository/EFRepository.csproj index c7c68bf..be609f2 100644 --- a/src/EFRepository/EFRepository.csproj +++ b/src/EFRepository/EFRepository.csproj @@ -1,4 +1,4 @@ - + net6.0;net5.0;netstandard2.0;netstandard2.1;netcoreapp3.0;net452 @@ -7,7 +7,7 @@ Mindfire.EFRepository - 2.0.0-alpha5 + 2.0.0 Mindfire Technology Mindfire EFRepository EFRepository allows you to use a LINQ-Enabled version of the Repository Pattern for Entity Framework @@ -15,7 +15,8 @@ https://github.com/MindfireTechnology/EFRepository https://github.com/MindfireTechnology/EFRepository EF, EntityFramework, Entity Framework, Core, NetStandard, Repository Pattern - Updated to use the latest version of EFRepository in the package. + Updated the interface to allow for using a single repository objects for multiple different types. +Updated to include a source generator that creates helper methods for filtering by object properties. True Nate Zaugg, Dan Beus MIT @@ -26,6 +27,14 @@ true + + 5 + + + + 5 + + 1.1.6 diff --git a/src/EFRepository/IRepository.cs b/src/EFRepository/IRepository.cs index 878c36d..0069b15 100644 --- a/src/EFRepository/IRepository.cs +++ b/src/EFRepository/IRepository.cs @@ -27,13 +27,6 @@ public interface IRepository : IDisposable /// Queriable Entity IQueryable Query() where TEntity : class, new(); - /// - /// Join another entity - /// - /// - /// - IQueryable Join() where TEntity : class, new(); - /// /// Find an entity based on key(s) /// diff --git a/src/EFRepository/Repository.cs b/src/EFRepository/Repository.cs index 32d902a..670edf1 100644 --- a/src/EFRepository/Repository.cs +++ b/src/EFRepository/Repository.cs @@ -34,14 +34,11 @@ public Repository(DbContext context, bool ownsDataContext = true) OwnsDataContext = ownsDataContext; } - public virtual IQueryable Query() where TEntity : class, new() => DataContext.Set(); - - public virtual IQueryable Join() where TEntity : class, new() + public virtual IQueryable Query() where TEntity : class, new() { - throw new NotImplementedException(); + return DataContext.Set(); } - public virtual TEntity FindOne(params object[] keys) where TEntity : class, new() { return DataContext.Set().Find(keys); From 13eba12c026982cde43abdc933142f27047bd48a Mon Sep 17 00:00:00 2001 From: Nate Zaugg Date: Tue, 11 Jan 2022 22:20:37 -0700 Subject: [PATCH 07/17] Adding extension methods to the generator --- .../ContextTests.cs | 150 +++-- .../ExtensionMethodGenerator.cs | 625 +++++++++++------- 2 files changed, 478 insertions(+), 297 deletions(-) diff --git a/src/EFRepository.Generator.IntegrationTests/ContextTests.cs b/src/EFRepository.Generator.IntegrationTests/ContextTests.cs index 3bf56d0..38920b0 100644 --- a/src/EFRepository.Generator.IntegrationTests/ContextTests.cs +++ b/src/EFRepository.Generator.IntegrationTests/ContextTests.cs @@ -1,61 +1,91 @@ -using System.Linq; -using Microsoft.EntityFrameworkCore; -using Xunit; -using Shouldly; -using System; - -namespace EFRepository.Generator.IntegrationTests -{ - public class ContextTests - { - [Fact] - public void UserMethodsGenerated() - { - var now = DateTime.Now; - - var users = Enumerable.Range(1, 10) - .Select(i => new User - { - Id = i, - Name = $"User {i}", - Address = $"{i} Fake St.", - Phone = new string(i.ToString().ToCharArray()[0], 10), - Created = now.AddHours(-i), - IsDeleted = i % 2 == 0, - Score = double.Parse($"{i}.{i}{i}{i}") - }); - - var usersQueryable = users.AsQueryable(); - - var user = usersQueryable.ById(1).FirstOrDefault(); - - user.ShouldNotBeNull(); - user.Id.ShouldBe(1); - - user = usersQueryable.ByIsDeleted(false).FirstOrDefault(); - - user.ShouldNotBeNull(); - user.IsDeleted.ShouldBeFalse(); - - user = usersQueryable.ByScore(3.333).FirstOrDefault(); - - user.ShouldNotBeNull(); - user.Score.ShouldBe(3.333); - - var filteredUsers = usersQueryable.WhereCreatedIsAfter(now.AddHours(-5.5)); - - filteredUsers.ShouldNotBeNull(); - filteredUsers.Count().ShouldBe(5); - - filteredUsers = usersQueryable.WhereCreatedIsBefore(now.AddHours(-5.5)); - - filteredUsers.ShouldNotBeNull(); - filteredUsers.Count().ShouldBe(5); - - filteredUsers = usersQueryable.WhereCreatedIsBetween(start: now.AddHours(-5.5), end: now.AddHours(-2.5)); - - filteredUsers.ShouldNotBeNull(); - filteredUsers.Count().ShouldBe(3); - } - } +using System.Linq; +using Microsoft.EntityFrameworkCore; +using Xunit; +using Shouldly; +using System; + +namespace EFRepository.Generator.IntegrationTests +{ + public class ContextTests + { + [Fact] + public void UserMethodsGenerated() + { + var now = DateTime.Now; + + var users = Enumerable.Range(1, 10) + .Select(i => new User + { + Id = i, + Name = $"User {i}", + Address = $"{i} Fake St.", + Phone = new string(i.ToString().ToCharArray()[0], 10), + Created = now.AddHours(-i), + IsDeleted = i % 2 == 0, + Score = double.Parse($"{i}.{i}{i}{i}") + }); + + var usersQueryable = users.AsQueryable(); + + var user = usersQueryable.ById(1).FirstOrDefault(); + + user.ShouldNotBeNull(); + user.Id.ShouldBe(1); + + user = usersQueryable.ByIsDeleted(false).FirstOrDefault(); + + user.ShouldNotBeNull(); + user.IsDeleted.ShouldBeFalse(); + + user = usersQueryable.ByScore(3.333).FirstOrDefault(); + + user.ShouldNotBeNull(); + user.Score.ShouldBe(3.333); + + var filteredUsers = usersQueryable.ByCreatedIsAfter(now.AddHours(-5.5)); + + filteredUsers.ShouldNotBeNull(); + filteredUsers.Count().ShouldBe(5); + + filteredUsers = usersQueryable.ByCreatedOnDate(now); + filteredUsers.ShouldNotBeNull(); + + filteredUsers = usersQueryable.ByCreatedIsBefore(now.AddHours(-5.5)); + + filteredUsers.ShouldNotBeNull(); + filteredUsers.Count().ShouldBe(5); + + filteredUsers = usersQueryable.ByCreatedBetween(start: now.AddHours(-5.5), end: now.AddHours(-2.5)); + + filteredUsers.ShouldNotBeNull(); + filteredUsers.Count().ShouldBe(3); + + // String functions + usersQueryable.ByAddress("1 Fake St.") + .ShouldNotBeNull() + .Count().ShouldBe(1); + + usersQueryable.ByAddressStartsWith("1") + .ShouldNotBeNull() + .Count().ShouldBe(2); + + usersQueryable.ByAddressEndsWith("St.") + .ShouldNotBeNull() + .Count().ShouldBe(10); + + usersQueryable.ByAddressContains("Fake") + .ShouldNotBeNull() + .Count().ShouldBe(10); + + usersQueryable.ByAddressIsNotNull() + .ShouldNotBeNull() + .Count().ShouldBe(10); + + usersQueryable.ByAddressIsNull() + .ShouldNotBeNull() + .Count().ShouldBe(0); + + + } + } } \ No newline at end of file diff --git a/src/EFRepository.Generator/ExtensionMethodGenerator.cs b/src/EFRepository.Generator/ExtensionMethodGenerator.cs index bba9f3e..0e9dac1 100644 --- a/src/EFRepository.Generator/ExtensionMethodGenerator.cs +++ b/src/EFRepository.Generator/ExtensionMethodGenerator.cs @@ -1,237 +1,388 @@ -using Microsoft.CodeAnalysis.CSharp; -using Microsoft.CodeAnalysis.Text; - -namespace EFRepository.Generator; - -[Generator] -public class ExtensionMethodGenerator : ISourceGenerator -{ - - public void Initialize(GeneratorInitializationContext context) => context.RegisterForSyntaxNotifications(() => new SyntaxReceiver()); - - public void Execute(GeneratorExecutionContext context) - { - if (context.SyntaxReceiver is not SyntaxReceiver receiver) - return; - - var options = ((CSharpCompilation)context.Compilation).SyntaxTrees[0].Options as CSharpParseOptions; - var compilation = context.Compilation; - - var dbContextSymbol = compilation.GetTypeByMetadataName("Microsoft.EntityFrameworkCore.DbContext"); - - if (dbContextSymbol == null) - dbContextSymbol = compilation.GetTypeByMetadataName("Microsoft.EntityFramework.DbContext"); - - if (dbContextSymbol == null) - return; - - var dbSetSymbol = compilation.GetTypeByMetadataName("Microsoft.EntityFrameworkCore.DbSet`1"); - - if (dbSetSymbol == null) - dbSetSymbol = compilation.GetTypeByMetadataName("Microsoft.EntityFramework.DbSet"); - - if (dbSetSymbol == null) - return; - - var dbContexts = receiver.Classes.Select(c => - { - var sourceModel = compilation.GetSemanticModel(c.SyntaxTree); - var sourceSymbol = sourceModel.GetDeclaredSymbol(c); - - if (sourceModel == null || sourceSymbol == null) - return null; - - return new SourceRecord - { - Model = sourceModel, - Symbol = sourceSymbol, - Syntax = c - }; - }) - .Where(c => c != null) - .Where(c => c.Symbol != null && c.Symbol.ContainingSymbol.Equals(c.Symbol.ContainingNamespace, SymbolEqualityComparer.Default) - && c.Symbol.BaseType.Equals(dbContextSymbol, SymbolEqualityComparer.Default)); - // TODO: Need to check that this is actually a DbContext class - - var extensions = dbContexts.SelectMany(dbc => ProcessDbContext(dbc, dbSetSymbol, compilation)); - - foreach (var (FileName, Content) in extensions) - { - context.AddSource(FileName, SourceText.From(Content, Encoding.UTF8)); - } - } - - protected IEnumerable<(string FileName, string Content)> ProcessDbContext(SourceRecord record, INamedTypeSymbol dbSetSymbol, Compilation compilation) - { - var dbSets = record.Symbol.GetMembers() - .Where(m => !m.IsStatic && !m.IsAbstract && !m.IsImplicitlyDeclared - && m.Kind is SymbolKind.Property && m.IsDefinition) - .Cast() - .Where(p => !p.IsReadOnly && !p.IsExtern) - .Where(p => - { - return p.Type.OriginalDefinition.Equals(dbSetSymbol, SymbolEqualityComparer.Default); - }); - - var list = new List<(string FileName, string Content)>(); - - foreach (var dbSet in dbSets) - { - var genericType = ((INamedTypeSymbol)dbSet.Type).TypeArguments[0]; - - if (genericType == null) - continue; - - var sourceCode = BuildExtensionMethod((INamedTypeSymbol)genericType, compilation); - - list.Add(sourceCode); - } - - return list; - } - - protected (string FileName, string Content) BuildExtensionMethod(INamedTypeSymbol dbSetClass, Compilation compilation) - { - string fileName = $"{dbSetClass.Name}.EFExtensions.g.cs"; - - var builder = new StringBuilder($@" -using System; -using System.Linq; - -namespace {dbSetClass.ContainingNamespace} -{{ - public static partial class {dbSetClass.Name}Extensions - {{ -"); - - var boolSymbol = compilation.GetTypeByMetadataName("System.Boolean"); - var byteSymbol = compilation.GetTypeByMetadataName("System.Byte"); - var shortSymbol = compilation.GetTypeByMetadataName("System.Int16"); - var intSymbol = compilation.GetTypeByMetadataName("System.Int32"); - var longSymbol = compilation.GetTypeByMetadataName("System.Int64"); - var floatSymbol = compilation.GetTypeByMetadataName("System.Single"); - var doubleSymbol = compilation.GetTypeByMetadataName("System.Double"); - var decimalSymbol = compilation.GetTypeByMetadataName("System.Decimal"); - var dateTimeSymbol = compilation.GetTypeByMetadataName("System.DateTime"); - - var members = dbSetClass.GetMembers() - .Where((m) => !m.IsStatic && !m.IsAbstract && !m.IsImplicitlyDeclared && m.IsDefinition - && m.Kind is SymbolKind.Property or SymbolKind.Field); - - foreach (var member in members) - { - ITypeSymbol type; - - if (member.Kind is SymbolKind.Property) - { - var property = (IPropertySymbol)member; - type = property.Type; - } - else - { - var field = (IFieldSymbol)member; - type = field.Type; - } - - if (type.Equals(boolSymbol, SymbolEqualityComparer.Default)) - { - builder.AppendLine(CreateMethod("bool", member.Name)); - } - else if (type.Equals(byteSymbol, SymbolEqualityComparer.Default)) - { - builder.AppendLine(CreateMethod("byte", member.Name)); - } - else if (type.Equals(shortSymbol, SymbolEqualityComparer.Default)) - { - builder.AppendLine(CreateMethod("short", member.Name)); - } - else if (type.Equals(intSymbol, SymbolEqualityComparer.Default)) - { - builder.AppendLine(CreateMethod("int", member.Name)); - } - else if (type.Equals(longSymbol, SymbolEqualityComparer.Default)) - { - builder.AppendLine(CreateMethod("long", member.Name)); - } - else if (type.Equals(floatSymbol, SymbolEqualityComparer.Default)) - { - builder.AppendLine(CreateMethod("float", member.Name)); - } - else if (type.Equals(doubleSymbol, SymbolEqualityComparer.Default)) - { - builder.AppendLine(CreateMethod("double", member.Name)); - } - else if (type.Equals(decimalSymbol, SymbolEqualityComparer.Default)) - { - builder.AppendLine(CreateMethod("decimal", member.Name)); - } - else if (type.Equals(dateTimeSymbol, SymbolEqualityComparer.Default)) - { - builder.AppendLine($@" /// - /// Filter the of {dbSetClass.Name} by whether or not the provided is after {member.Name} - /// - /// The that {member.Name} should be before - public static IQueryable<{dbSetClass}> Where{member.Name}IsBefore(this IQueryable<{dbSetClass.Name}> queryable, DateTime value) - {{ - if (queryable == null) - return queryable; - - return queryable.Where(n => n.{member.Name} < value); - }}"); - - builder.AppendLine($@" /// - /// Filter the of {dbSetClass.Name} by whether or not the provided is before {member.Name} - /// - /// The that {member.Name} should be before - public static IQueryable<{dbSetClass}> Where{member.Name}IsAfter(this IQueryable<{dbSetClass.Name}> queryable, DateTime value) - {{ - if (queryable == null) - return queryable; - - return queryable.Where(n => n.{member.Name} > value); - }}"); - - builder.AppendLine($@" /// - /// Filter the of {dbSetClass.Name} by whether or not {member.Name} is between the two provided values. - /// - /// The that should be before {member.Name} - /// The that should be after {member.Name} - public static IQueryable<{dbSetClass}> Where{member.Name}IsBetween(this IQueryable<{dbSetClass.Name}> queryable, DateTime start, DateTime end) - {{ - if (queryable == null) - return queryable; - - return queryable.Where(n => n.{member.Name} > start && n.{member.Name} < end); - }}"); - } - - } - - builder.AppendLine(" }") - .AppendLine("}"); - - return (fileName, builder.ToString()); - - string CreateMethod(string type, string memberName) - { - return $@" /// - /// Filter the of {dbSetClass.Name} by {memberName} - /// - /// The {type} that {memberName} should be equal to - public static IQueryable<{dbSetClass.Name}> By{memberName}(this IQueryable<{dbSetClass.Name}> queryable, {type} value) - {{ - if (queryable == null) - return queryable; - - return queryable.Where(n => n.{memberName} == value); - }}"; - } - } -} - -public record SourceRecord -{ - public SemanticModel Model { get; init; } - public INamedTypeSymbol Symbol { get; init; } - public TypeDeclarationSyntax Syntax { get; init; } -} +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.Text; + +namespace EFRepository.Generator; + +[Generator] +public class ExtensionMethodGenerator : ISourceGenerator +{ + + public void Initialize(GeneratorInitializationContext context) + { + context.RegisterForSyntaxNotifications(() => new SyntaxReceiver()); + } + + public void Execute(GeneratorExecutionContext context) + { + if (context.SyntaxReceiver is not SyntaxReceiver receiver) + return; + + var options = ((CSharpCompilation)context.Compilation).SyntaxTrees[0].Options as CSharpParseOptions; + var compilation = context.Compilation; + + var dbContextSymbol = compilation.GetTypeByMetadataName("Microsoft.EntityFrameworkCore.DbContext"); + + if (dbContextSymbol == null) + dbContextSymbol = compilation.GetTypeByMetadataName("Microsoft.EntityFramework.DbContext"); + + if (dbContextSymbol == null) + return; + + var dbSetSymbol = compilation.GetTypeByMetadataName("Microsoft.EntityFrameworkCore.DbSet`1"); + + if (dbSetSymbol == null) + dbSetSymbol = compilation.GetTypeByMetadataName("Microsoft.EntityFramework.DbSet"); + + if (dbSetSymbol == null) + return; + + var dbContexts = receiver.Classes.Select(c => + { + var sourceModel = compilation.GetSemanticModel(c.SyntaxTree); + var sourceSymbol = sourceModel.GetDeclaredSymbol(c); + + if (sourceModel == null || sourceSymbol == null) + return null; + + return new SourceRecord + { + Model = sourceModel, + Symbol = sourceSymbol, + Syntax = c + }; + }) + .Where(c => c != null) + .Where(c => + c != null && + c.Symbol != null && + c.Symbol.ContainingSymbol.Equals(c.Symbol.ContainingNamespace, SymbolEqualityComparer.Default) && + c.Symbol.BaseType != null && c.Symbol.BaseType.Equals(dbContextSymbol, SymbolEqualityComparer.Default)); + + // TODO: Need to check that this is actually a DbContext class + + var extensions = dbContexts.SelectMany(dbc => ProcessDbContext(dbc, dbSetSymbol, compilation)); + + foreach (var (FileName, Content) in extensions) + { + context.AddSource(FileName, SourceText.From(Content, Encoding.UTF8)); + } + } + + protected IEnumerable<(string FileName, string Content)> ProcessDbContext(SourceRecord record, INamedTypeSymbol dbSetSymbol, Compilation compilation) + { + var dbSets = record.Symbol.GetMembers() + .Where(m => !m.IsStatic && !m.IsAbstract && !m.IsImplicitlyDeclared + && m.Kind is SymbolKind.Property && m.IsDefinition) + .Cast() + .Where(p => !p.IsReadOnly && !p.IsExtern) + .Where(p => + { + return p.Type.OriginalDefinition.Equals(dbSetSymbol, SymbolEqualityComparer.Default); + }); + + var list = new List<(string FileName, string Content)>(); + + foreach (var dbSet in dbSets) + { + var genericType = ((INamedTypeSymbol)dbSet.Type).TypeArguments[0]; + + if (genericType == null) + continue; + + var sourceCode = BuildExtensionMethod((INamedTypeSymbol)genericType, compilation); + + list.Add(sourceCode); + } + + return list; + } + + protected (string FileName, string Content) BuildExtensionMethod(INamedTypeSymbol dbSetClass, Compilation compilation) + { + string fileName = $"{dbSetClass.Name}.EFExtensions.g.cs"; + + var builder = new StringBuilder( +$@"using System; +using System.Linq; +using Microsoft.EntityFrameworkCore; + +namespace {dbSetClass.ContainingNamespace}; + + public static partial class {dbSetClass.Name}Extensions + {{ +"); + + var boolSymbol = compilation.GetTypeByMetadataName("System.Boolean"); + var byteSymbol = compilation.GetTypeByMetadataName("System.Byte"); + var shortSymbol = compilation.GetTypeByMetadataName("System.Int16"); + var intSymbol = compilation.GetTypeByMetadataName("System.Int32"); + var longSymbol = compilation.GetTypeByMetadataName("System.Int64"); + var floatSymbol = compilation.GetTypeByMetadataName("System.Single"); + var doubleSymbol = compilation.GetTypeByMetadataName("System.Double"); + var decimalSymbol = compilation.GetTypeByMetadataName("System.Decimal"); + var stringSymbol = compilation.GetTypeByMetadataName("System.String"); + var dateTimeSymbol = compilation.GetTypeByMetadataName("System.DateTime"); + + var members = dbSetClass.GetMembers() + .Where((m) => !m.IsStatic && !m.IsAbstract && !m.IsImplicitlyDeclared && m.IsDefinition + && m.Kind is SymbolKind.Property or SymbolKind.Field); + + foreach (var member in members) + { + ITypeSymbol type; + bool nullable; + + if (member.Kind is SymbolKind.Property) + { + var property = (IPropertySymbol)member; + type = property.Type; + nullable = property.NullableAnnotation == NullableAnnotation.Annotated; + } + else + { + var field = (IFieldSymbol)member; + type = field.Type; + nullable = field.NullableAnnotation == NullableAnnotation.Annotated; + } + + if (type.Equals(boolSymbol, SymbolEqualityComparer.Default)) + { + builder.AppendLine(CreateMethod("bool", member.Name, nullable)); + } + else if (type.Equals(byteSymbol, SymbolEqualityComparer.Default)) + { + builder.AppendLine(CreateMethod("byte", member.Name, nullable)); + } + else if (type.Equals(shortSymbol, SymbolEqualityComparer.Default)) + { + builder.AppendLine(CreateMethod("short", member.Name, nullable)); + } + else if (type.Equals(intSymbol, SymbolEqualityComparer.Default)) + { + builder.AppendLine(CreateMethod("int", member.Name, nullable)); + } + else if (type.Equals(longSymbol, SymbolEqualityComparer.Default)) + { + builder.AppendLine(CreateMethod("long", member.Name, nullable)); + } + else if (type.Equals(floatSymbol, SymbolEqualityComparer.Default)) + { + builder.AppendLine(CreateMethod("float", member.Name, nullable)); + } + else if (type.Equals(doubleSymbol, SymbolEqualityComparer.Default)) + { + builder.AppendLine(CreateMethod("double", member.Name, nullable)); + } + else if (type.Equals(decimalSymbol, SymbolEqualityComparer.Default)) + { + builder.AppendLine(CreateMethod("decimal", member.Name, nullable)); + } + else if (type.Equals(stringSymbol, SymbolEqualityComparer.Default)) + { + builder.AppendLine(CreateNullMethodFunctions(member.Name)); + + builder.AppendLine($@" + /// + /// Filter the of {dbSetClass.Name} by {member.Name} + /// + /// The {type} which {member.Name} should be equal + public static IQueryable<{dbSetClass.Name}> By{member.Name}(this IQueryable<{dbSetClass.Name}> query, {type}? value) + {{ + if (query == null) + return query; + + if (string.IsNullOrWhiteSpace(value)) + return query; + + return query.Where(n => n.{member.Name} == value); + }}"); + + builder.AppendLine($@" + /// + /// Filter the of {dbSetClass.Name} by {member.Name} contains a value + /// + /// The {type} which {member.Name} should contain + public static IQueryable<{dbSetClass.Name}> By{member.Name}Contains(this IQueryable<{dbSetClass.Name}> query, {type}? value) + {{ + if (query == null) + return query; + + if (string.IsNullOrWhiteSpace(value)) + return query; + + return query.Where(n => n.{member.Name}.Contains(value)); + }}"); + + builder.AppendLine($@" + /// + /// Filter the of {dbSetClass.Name} by {member.Name} starts with a value + /// + /// The {type} which {member.Name} should start with + public static IQueryable<{dbSetClass.Name}> By{member.Name}StartsWith(this IQueryable<{dbSetClass.Name}> query, {type}? value) + {{ + if (query == null) + return query; + + if (string.IsNullOrWhiteSpace(value)) + return query; + + return query.Where(n => n.{member.Name}.StartsWith(value)); + }}"); + + builder.AppendLine($@" + + /// + /// Filter the of {dbSetClass.Name} by {member.Name} ends with a value + /// + /// The {type} which {member.Name} should end with + public static IQueryable<{dbSetClass.Name}> By{member.Name}EndsWith(this IQueryable<{dbSetClass.Name}> query, {type}? value) + {{ + if (query == null) + return query; + + if (string.IsNullOrWhiteSpace(value)) + return query; + + return query.Where(n => n.{member.Name}.EndsWith(value)); + }}"); + + } + else if (type.Equals(dateTimeSymbol, SymbolEqualityComparer.Default)) + { + builder.AppendLine(CreateMethod("DateTime", member.Name, nullable)); + + builder.AppendLine($@" + /// + /// Filter the of {dbSetClass.Name} by whether or not the provided is after {member.Name} + /// + /// The that {member.Name} should be before + public static IQueryable<{dbSetClass}> By{member.Name}IsBefore(this IQueryable<{dbSetClass.Name}> query, DateTime? value) + {{ + if (query == null) + return query; + + if (value == null) + return query; + + return query.Where(n => n.{member.Name} < value); + }}"); + + builder.AppendLine($@" + /// + /// Filter the of {dbSetClass.Name} by whether or not the provided is after {member.Name} + /// + /// The that {member.Name} should be after + public static IQueryable<{dbSetClass}> By{member.Name}IsAfter(this IQueryable<{dbSetClass.Name}> query, DateTime? value) + {{ + if (query == null) + return query; + + if (value == null) + return query; + + return query.Where(n => n.{member.Name} > value); + }}"); + + builder.AppendLine($@" + /// + /// Filter the of {dbSetClass.Name} by whether or not {member.Name} is between the two provided values. + /// + /// The that should be before {member.Name} + /// The that should be after {member.Name} + public static IQueryable<{dbSetClass}> By{member.Name}Between(this IQueryable<{dbSetClass.Name}> query, DateTime? start, DateTime? end) + {{ + if (query == null) + return query; + + if (start != null) + query = query.Where(n => n.{member.Name} > start); + + if (end != null) + query = query.Where(n => n.{member.Name} < end); + + return query; + }}"); + + builder.AppendLine($@" + /// + /// Filter the of {dbSetClass.Name} by whether or not {member.Name} is between the two provided values. + /// + /// The that should the same date as {member.Name}, excluding time + public static IQueryable<{dbSetClass}> By{member.Name}OnDate(this IQueryable<{dbSetClass.Name}> query, DateTime? value) + {{ + if (query == null) + return query; + + if (value != null) + return query.Where(n => n.{member.Name}.Date > value.Value.Date); + else + return query; + }}"); + } + } + + // Close Class + builder.AppendLine("}"); + + return (fileName, builder.ToString()); + + string CreateMethod(string type, string memberName, bool nullable) + { + string result = $@" + /// + /// Filter the of {dbSetClass.Name} by {memberName} + /// + /// The {type} which should equal {memberName} + public static IQueryable<{dbSetClass.Name}> By{memberName}(this IQueryable<{dbSetClass.Name}> query, {type}? value) + {{ + if (query == null) + return query; + + if (value == null) + return query; + + return query.Where(n => n.{memberName} == value); + }}"; + + if (nullable) + { + result += CreateNullMethodFunctions(memberName); + } + + return result; + } + + string CreateNullMethodFunctions(string memberName) + { + return $@" + /// + /// Filter the of {dbSetClass.Name} by {memberName} is null + /// + public static IQueryable<{dbSetClass.Name}> By{memberName}IsNull(this IQueryable<{dbSetClass.Name}> query) + {{ + if (query == null) + return query; + + return query.Where(n => n.{memberName} == null); + }} + + /// + /// Filter the of {dbSetClass.Name} by {memberName} is not null + /// + public static IQueryable<{dbSetClass.Name}> By{memberName}IsNotNull(this IQueryable<{dbSetClass.Name}> query) + {{ + if (query == null) + return query; + + return query.Where(n => n.{memberName} != null); + }}"; + } + } +} + +public record SourceRecord +{ + public SemanticModel Model { get; init; } + public INamedTypeSymbol Symbol { get; init; } + public TypeDeclarationSyntax Syntax { get; init; } +} From eadca5c7433952821929933ff81f2cec6263aa63 Mon Sep 17 00:00:00 2001 From: Nate Zaugg Date: Wed, 12 Jan 2022 12:25:59 -0700 Subject: [PATCH 08/17] PR changes --- .../ExtensionMethodGenerator.cs | 281 +++++++++--------- 1 file changed, 141 insertions(+), 140 deletions(-) diff --git a/src/EFRepository.Generator/ExtensionMethodGenerator.cs b/src/EFRepository.Generator/ExtensionMethodGenerator.cs index 0e9dac1..a11ff68 100644 --- a/src/EFRepository.Generator/ExtensionMethodGenerator.cs +++ b/src/EFRepository.Generator/ExtensionMethodGenerator.cs @@ -50,8 +50,7 @@ public void Execute(GeneratorExecutionContext context) Symbol = sourceSymbol, Syntax = c }; - }) - .Where(c => c != null) + }).Where(c => c != null) .Where(c => c != null && c.Symbol != null && @@ -106,7 +105,8 @@ public void Execute(GeneratorExecutionContext context) using System.Linq; using Microsoft.EntityFrameworkCore; -namespace {dbSetClass.ContainingNamespace}; +namespace {dbSetClass.ContainingNamespace} +{{ public static partial class {dbSetClass.Name}Extensions {{ @@ -182,69 +182,69 @@ public static partial class {dbSetClass.Name}Extensions builder.AppendLine(CreateNullMethodFunctions(member.Name)); builder.AppendLine($@" - /// - /// Filter the of {dbSetClass.Name} by {member.Name} - /// - /// The {type} which {member.Name} should be equal - public static IQueryable<{dbSetClass.Name}> By{member.Name}(this IQueryable<{dbSetClass.Name}> query, {type}? value) - {{ - if (query == null) - return query; + /// + /// Filter the of {dbSetClass.Name} by {member.Name} + /// + /// The string which {member.Name} should be equal + public static IQueryable<{dbSetClass.Name}> By{member.Name}(this IQueryable<{dbSetClass.Name}> query, {type}? value) + {{ + if (query == null) + return query; - if (string.IsNullOrWhiteSpace(value)) - return query; + if (string.IsNullOrWhiteSpace(value)) + return query; - return query.Where(n => n.{member.Name} == value); - }}"); + return query.Where(n => n.{member.Name} == value); + }}"); builder.AppendLine($@" - /// - /// Filter the of {dbSetClass.Name} by {member.Name} contains a value - /// - /// The {type} which {member.Name} should contain - public static IQueryable<{dbSetClass.Name}> By{member.Name}Contains(this IQueryable<{dbSetClass.Name}> query, {type}? value) - {{ - if (query == null) - return query; + /// + /// Filter the of {dbSetClass.Name} by {member.Name} contains a value + /// + /// The string which {member.Name} should contain + public static IQueryable<{dbSetClass.Name}> By{member.Name}Contains(this IQueryable<{dbSetClass.Name}> query, {type}? value) + {{ + if (query == null) + return query; - if (string.IsNullOrWhiteSpace(value)) - return query; + if (string.IsNullOrWhiteSpace(value)) + return query; - return query.Where(n => n.{member.Name}.Contains(value)); - }}"); + return query.Where(n => n.{member.Name}.Contains(value)); + }}"); builder.AppendLine($@" - /// - /// Filter the of {dbSetClass.Name} by {member.Name} starts with a value - /// - /// The {type} which {member.Name} should start with - public static IQueryable<{dbSetClass.Name}> By{member.Name}StartsWith(this IQueryable<{dbSetClass.Name}> query, {type}? value) - {{ - if (query == null) - return query; + /// + /// Filter the of {dbSetClass.Name} by {member.Name} starts with a value + /// + /// The string which {member.Name} should start with + public static IQueryable<{dbSetClass.Name}> By{member.Name}StartsWith(this IQueryable<{dbSetClass.Name}> query, {type}? value) + {{ + if (query == null) + return query; - if (string.IsNullOrWhiteSpace(value)) - return query; + if (string.IsNullOrWhiteSpace(value)) + return query; - return query.Where(n => n.{member.Name}.StartsWith(value)); - }}"); + return query.Where(n => n.{member.Name}.StartsWith(value)); + }}"); builder.AppendLine($@" - /// - /// Filter the of {dbSetClass.Name} by {member.Name} ends with a value - /// - /// The {type} which {member.Name} should end with - public static IQueryable<{dbSetClass.Name}> By{member.Name}EndsWith(this IQueryable<{dbSetClass.Name}> query, {type}? value) - {{ - if (query == null) - return query; + /// + /// Filter the of {dbSetClass.Name} by {member.Name} ends with a value + /// + /// The string which {member.Name} should end with + public static IQueryable<{dbSetClass.Name}> By{member.Name}EndsWith(this IQueryable<{dbSetClass.Name}> query, {type}? value) + {{ + if (query == null) + return query; - if (string.IsNullOrWhiteSpace(value)) - return query; + if (string.IsNullOrWhiteSpace(value)) + return query; - return query.Where(n => n.{member.Name}.EndsWith(value)); - }}"); + return query.Where(n => n.{member.Name}.EndsWith(value)); + }}"); } else if (type.Equals(dateTimeSymbol, SymbolEqualityComparer.Default)) @@ -252,97 +252,98 @@ public static partial class {dbSetClass.Name}Extensions builder.AppendLine(CreateMethod("DateTime", member.Name, nullable)); builder.AppendLine($@" - /// - /// Filter the of {dbSetClass.Name} by whether or not the provided is after {member.Name} - /// - /// The that {member.Name} should be before - public static IQueryable<{dbSetClass}> By{member.Name}IsBefore(this IQueryable<{dbSetClass.Name}> query, DateTime? value) - {{ - if (query == null) - return query; + /// + /// Filter the of {dbSetClass.Name} by whether or not the provided is after {member.Name} + /// + /// The that {member.Name} should be before + public static IQueryable<{dbSetClass}> By{member.Name}IsBefore(this IQueryable<{dbSetClass.Name}> query, DateTime? value) + {{ + if (query == null) + return query; - if (value == null) - return query; + if (value == null) + return query; - return query.Where(n => n.{member.Name} < value); - }}"); + return query.Where(n => n.{member.Name} < value); + }}"); builder.AppendLine($@" - /// - /// Filter the of {dbSetClass.Name} by whether or not the provided is after {member.Name} - /// - /// The that {member.Name} should be after - public static IQueryable<{dbSetClass}> By{member.Name}IsAfter(this IQueryable<{dbSetClass.Name}> query, DateTime? value) - {{ - if (query == null) - return query; + /// + /// Filter the of {dbSetClass.Name} by whether or not the provided is after {member.Name} + /// + /// The that {member.Name} should be after + public static IQueryable<{dbSetClass}> By{member.Name}IsAfter(this IQueryable<{dbSetClass.Name}> query, DateTime? value) + {{ + if (query == null) + return query; - if (value == null) - return query; + if (value == null) + return query; - return query.Where(n => n.{member.Name} > value); - }}"); + return query.Where(n => n.{member.Name} > value); + }}"); builder.AppendLine($@" - /// - /// Filter the of {dbSetClass.Name} by whether or not {member.Name} is between the two provided values. - /// - /// The that should be before {member.Name} - /// The that should be after {member.Name} - public static IQueryable<{dbSetClass}> By{member.Name}Between(this IQueryable<{dbSetClass.Name}> query, DateTime? start, DateTime? end) - {{ - if (query == null) - return query; - - if (start != null) - query = query.Where(n => n.{member.Name} > start); - - if (end != null) - query = query.Where(n => n.{member.Name} < end); + /// + /// Filter the of {dbSetClass.Name} by whether or not {member.Name} is between the two provided values. + /// + /// The that should be before {member.Name} + /// The that should be after {member.Name} + public static IQueryable<{dbSetClass}> By{member.Name}Between(this IQueryable<{dbSetClass.Name}> query, DateTime? start, DateTime? end) + {{ + if (query == null) + return query; + + if (start != null) + query = query.Where(n => n.{member.Name} > start); + + if (end != null) + query = query.Where(n => n.{member.Name} < end); - return query; - }}"); - - builder.AppendLine($@" - /// - /// Filter the of {dbSetClass.Name} by whether or not {member.Name} is between the two provided values. - /// - /// The that should the same date as {member.Name}, excluding time - public static IQueryable<{dbSetClass}> By{member.Name}OnDate(this IQueryable<{dbSetClass.Name}> query, DateTime? value) - {{ - if (query == null) return query; + }}"); - if (value != null) - return query.Where(n => n.{member.Name}.Date > value.Value.Date); - else - return query; - }}"); + builder.AppendLine($@" + /// + /// Filter the of {dbSetClass.Name} by whether or not {member.Name} is between the two provided values. + /// + /// The that should the same date as {member.Name}, excluding time + public static IQueryable<{dbSetClass}> By{member.Name}OnDate(this IQueryable<{dbSetClass.Name}> query, DateTime? value) + {{ + if (query == null) + return query; + + if (value != null) + return query.Where(n => n.{member.Name}.Date == value.Value.Date); + else + return query; + }}"); } } // Close Class - builder.AppendLine("}"); + builder.AppendLine(" }") + .AppendLine("}"); return (fileName, builder.ToString()); string CreateMethod(string type, string memberName, bool nullable) { string result = $@" - /// - /// Filter the of {dbSetClass.Name} by {memberName} - /// - /// The {type} which should equal {memberName} - public static IQueryable<{dbSetClass.Name}> By{memberName}(this IQueryable<{dbSetClass.Name}> query, {type}? value) - {{ - if (query == null) - return query; + /// + /// Filter the of {dbSetClass.Name} by {memberName} + /// + /// The {type} which should equal {memberName} + public static IQueryable<{dbSetClass.Name}> By{memberName}(this IQueryable<{dbSetClass.Name}> query, {type}? value) + {{ + if (query == null) + return query; - if (value == null) - return query; + if (value == null) + return query; - return query.Where(n => n.{memberName} == value); - }}"; + return query.Where(n => n.{memberName} == value); + }}"; if (nullable) { @@ -355,27 +356,27 @@ string CreateMethod(string type, string memberName, bool nullable) string CreateNullMethodFunctions(string memberName) { return $@" - /// - /// Filter the of {dbSetClass.Name} by {memberName} is null - /// - public static IQueryable<{dbSetClass.Name}> By{memberName}IsNull(this IQueryable<{dbSetClass.Name}> query) - {{ - if (query == null) - return query; - - return query.Where(n => n.{memberName} == null); - }} - - /// - /// Filter the of {dbSetClass.Name} by {memberName} is not null - /// - public static IQueryable<{dbSetClass.Name}> By{memberName}IsNotNull(this IQueryable<{dbSetClass.Name}> query) - {{ - if (query == null) - return query; - - return query.Where(n => n.{memberName} != null); - }}"; + /// + /// Filter the of {dbSetClass.Name} by {memberName} is null + /// + public static IQueryable<{dbSetClass.Name}> By{memberName}IsNull(this IQueryable<{dbSetClass.Name}> query) + {{ + if (query == null) + return query; + + return query.Where(n => n.{memberName} == null); + }} + + /// + /// Filter the of {dbSetClass.Name} by {memberName} is not null + /// + public static IQueryable<{dbSetClass.Name}> By{memberName}IsNotNull(this IQueryable<{dbSetClass.Name}> query) + {{ + if (query == null) + return query; + + return query.Where(n => n.{memberName} != null); + }}"; } } } From 8305b9997708216cd1cd0ec0862c410521c64266 Mon Sep 17 00:00:00 2001 From: Nate Zaugg Date: Wed, 12 Jan 2022 13:16:29 -0700 Subject: [PATCH 09/17] More changes for the PR Simplified and organized unit tests. Added Unit Tests for string IsNull and IsNotNull and IsNotNullOrWhiteSpace... --- .../ContextTests.cs | 93 ++++++++++++------- .../Post.cs | 3 +- .../TestingContext.cs | 38 ++++---- .../User.cs | 4 +- .../ExtensionMethodGenerator.cs | 40 ++++++-- 5 files changed, 116 insertions(+), 62 deletions(-) diff --git a/src/EFRepository.Generator.IntegrationTests/ContextTests.cs b/src/EFRepository.Generator.IntegrationTests/ContextTests.cs index 38920b0..c9ffd70 100644 --- a/src/EFRepository.Generator.IntegrationTests/ContextTests.cs +++ b/src/EFRepository.Generator.IntegrationTests/ContextTests.cs @@ -23,48 +23,87 @@ public void UserMethodsGenerated() Created = now.AddHours(-i), IsDeleted = i % 2 == 0, Score = double.Parse($"{i}.{i}{i}{i}") + }).Union(new[] + { + new User + { + Id = 0, + Name = string.Empty, + Address = null, + Phone = " ", + Created = now.AddDays(-5), + IsDeleted = false, + Score = 9.2 + } }); var usersQueryable = users.AsQueryable(); - var user = usersQueryable.ById(1).FirstOrDefault(); + // Int/Long Functions + usersQueryable.ById(1).FirstOrDefault() + .ShouldNotBeNull() + .Id.ShouldBe(1); - user.ShouldNotBeNull(); - user.Id.ShouldBe(1); + // Boolean Functions + usersQueryable.ByIsDeleted(false).FirstOrDefault() + .ShouldNotBeNull() + .IsDeleted.ShouldBeFalse(); - user = usersQueryable.ByIsDeleted(false).FirstOrDefault(); + // Double Functions + usersQueryable.ByScore(3.333).FirstOrDefault() + .ShouldNotBeNull() + .Score.ShouldBe(3.333); - user.ShouldNotBeNull(); - user.IsDeleted.ShouldBeFalse(); + // DateTime Functions + usersQueryable.ByCreatedIsAfter(now.AddHours(-5.5)) + .ShouldNotBeNull() + .Count().ShouldBe(5); - user = usersQueryable.ByScore(3.333).FirstOrDefault(); + usersQueryable.ByCreatedOnDate(now.AddDays(-5)) + .ShouldNotBeNull() + .Count().ShouldBe(1); - user.ShouldNotBeNull(); - user.Score.ShouldBe(3.333); + usersQueryable.ByCreatedIsBefore(now.AddHours(-5.5)) + .ShouldNotBeNull() + .Count().ShouldBe(6); - var filteredUsers = usersQueryable.ByCreatedIsAfter(now.AddHours(-5.5)); + usersQueryable.ByCreatedBetween(start: now.AddHours(-5.5), end: now.AddHours(-2.5)) + .ShouldNotBeNull() + .Count().ShouldBe(3); - filteredUsers.ShouldNotBeNull(); - filteredUsers.Count().ShouldBe(5); + // String functions + usersQueryable.ByAddress("1 Fake St.") + .ShouldNotBeNull() + .Count().ShouldBe(1); - filteredUsers = usersQueryable.ByCreatedOnDate(now); - filteredUsers.ShouldNotBeNull(); + usersQueryable.ByAddressIsNotNull() + .ShouldNotBeNull() + .Count().ShouldBe(10); - filteredUsers = usersQueryable.ByCreatedIsBefore(now.AddHours(-5.5)); + usersQueryable.ByAddressIsNull() + .ShouldNotBeNull() + .Count().ShouldBe(1); - filteredUsers.ShouldNotBeNull(); - filteredUsers.Count().ShouldBe(5); + usersQueryable.ByNameIsNull() + .ShouldNotBeNull() + .Count().ShouldBe(0); - filteredUsers = usersQueryable.ByCreatedBetween(start: now.AddHours(-5.5), end: now.AddHours(-2.5)); + usersQueryable.ByNameIsNullOrWhiteSpace() + .ShouldNotBeNull() + .Count().ShouldBe(1); - filteredUsers.ShouldNotBeNull(); - filteredUsers.Count().ShouldBe(3); + usersQueryable.ByPhoneIsNullOrWhiteSpace() + .ShouldNotBeNull() + .Count().ShouldBe(1); - // String functions - usersQueryable.ByAddress("1 Fake St.") + usersQueryable.ByAddressIsNullOrWhiteSpace() .ShouldNotBeNull() .Count().ShouldBe(1); + usersQueryable.ByAddressIsNotNullOrWhiteSpace() + .ShouldNotBeNull() + .Count().ShouldBe(10); + usersQueryable.ByAddressStartsWith("1") .ShouldNotBeNull() .Count().ShouldBe(2); @@ -76,16 +115,6 @@ public void UserMethodsGenerated() usersQueryable.ByAddressContains("Fake") .ShouldNotBeNull() .Count().ShouldBe(10); - - usersQueryable.ByAddressIsNotNull() - .ShouldNotBeNull() - .Count().ShouldBe(10); - - usersQueryable.ByAddressIsNull() - .ShouldNotBeNull() - .Count().ShouldBe(0); - - } } } \ No newline at end of file diff --git a/src/EFRepository.Generator.IntegrationTests/Post.cs b/src/EFRepository.Generator.IntegrationTests/Post.cs index 422774e..59a9c80 100644 --- a/src/EFRepository.Generator.IntegrationTests/Post.cs +++ b/src/EFRepository.Generator.IntegrationTests/Post.cs @@ -16,6 +16,7 @@ public Post() { Title = string.Empty; Content = string.Empty; - } + } + } } diff --git a/src/EFRepository.Generator.IntegrationTests/TestingContext.cs b/src/EFRepository.Generator.IntegrationTests/TestingContext.cs index 9a25bbf..7c36aa7 100644 --- a/src/EFRepository.Generator.IntegrationTests/TestingContext.cs +++ b/src/EFRepository.Generator.IntegrationTests/TestingContext.cs @@ -1,19 +1,19 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using Microsoft.EntityFrameworkCore; - -namespace EFRepository.Generator.IntegrationTests -{ - public class TestingContext : DbContext - { - public virtual DbSet Users { get; set; } - public virtual DbSet Posts { get; set; } - - public TestingContext(DbContextOptions options) : base(options) - { - } - } -} +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; + +namespace EFRepository.Generator.IntegrationTests +{ + public class TestingContext : DbContext + { + public virtual DbSet Users { get; set; } + public virtual DbSet Posts { get; set; } + + public TestingContext(DbContextOptions options) : base(options) + { + } + } +} diff --git a/src/EFRepository.Generator.IntegrationTests/User.cs b/src/EFRepository.Generator.IntegrationTests/User.cs index f995fa2..2e35427 100644 --- a/src/EFRepository.Generator.IntegrationTests/User.cs +++ b/src/EFRepository.Generator.IntegrationTests/User.cs @@ -10,8 +10,8 @@ public record User { public int Id { get; set; } public string Name { get; set; } - public string Phone { get; set; } - public string Address { get; set; } + public string? Phone { get; set; } + public string? Address { get; set; } public DateTime Created { get; set; } public bool IsDeleted { get; set; } public double Score { get; set; } diff --git a/src/EFRepository.Generator/ExtensionMethodGenerator.cs b/src/EFRepository.Generator/ExtensionMethodGenerator.cs index a11ff68..320017b 100644 --- a/src/EFRepository.Generator/ExtensionMethodGenerator.cs +++ b/src/EFRepository.Generator/ExtensionMethodGenerator.cs @@ -105,6 +105,8 @@ public void Execute(GeneratorExecutionContext context) using System.Linq; using Microsoft.EntityFrameworkCore; +#nullable enable + namespace {dbSetClass.ContainingNamespace} {{ @@ -183,10 +185,33 @@ public static partial class {dbSetClass.Name}Extensions builder.AppendLine($@" /// + /// Filter the of {dbSetClass.Name} by {member.Name} is null + /// + public static IQueryable<{dbSetClass.Name}> By{member.Name}IsNullOrWhiteSpace(this IQueryable<{dbSetClass.Name}> query) + {{ + if (query == null) + return query; + + return query.Where(n => string.IsNullOrWhiteSpace(n.{member.Name})); + }} + + /// + /// Filter the of {dbSetClass.Name} by {member.Name} is not null + /// + public static IQueryable<{dbSetClass.Name}> By{member.Name}IsNotNullOrWhiteSpace(this IQueryable<{dbSetClass.Name}> query) + {{ + if (query == null) + return query; + + return query.Where(n => !string.IsNullOrWhiteSpace(n.{member.Name})); + }}"); + + builder.AppendLine($@" + /// /// Filter the of {dbSetClass.Name} by {member.Name} /// /// The string which {member.Name} should be equal - public static IQueryable<{dbSetClass.Name}> By{member.Name}(this IQueryable<{dbSetClass.Name}> query, {type}? value) + public static IQueryable<{dbSetClass.Name}> By{member.Name}(this IQueryable<{dbSetClass.Name}> query, string? value) {{ if (query == null) return query; @@ -202,7 +227,7 @@ public static partial class {dbSetClass.Name}Extensions /// Filter the of {dbSetClass.Name} by {member.Name} contains a value /// /// The string which {member.Name} should contain - public static IQueryable<{dbSetClass.Name}> By{member.Name}Contains(this IQueryable<{dbSetClass.Name}> query, {type}? value) + public static IQueryable<{dbSetClass.Name}> By{member.Name}Contains(this IQueryable<{dbSetClass.Name}> query, string? value) {{ if (query == null) return query; @@ -210,7 +235,7 @@ public static partial class {dbSetClass.Name}Extensions if (string.IsNullOrWhiteSpace(value)) return query; - return query.Where(n => n.{member.Name}.Contains(value)); + return query.Where(n => n.{member.Name} != null && n.{member.Name}.Contains(value)); }}"); builder.AppendLine($@" @@ -218,7 +243,7 @@ public static partial class {dbSetClass.Name}Extensions /// Filter the of {dbSetClass.Name} by {member.Name} starts with a value /// /// The string which {member.Name} should start with - public static IQueryable<{dbSetClass.Name}> By{member.Name}StartsWith(this IQueryable<{dbSetClass.Name}> query, {type}? value) + public static IQueryable<{dbSetClass.Name}> By{member.Name}StartsWith(this IQueryable<{dbSetClass.Name}> query, string? value) {{ if (query == null) return query; @@ -226,7 +251,7 @@ public static partial class {dbSetClass.Name}Extensions if (string.IsNullOrWhiteSpace(value)) return query; - return query.Where(n => n.{member.Name}.StartsWith(value)); + return query.Where(n => n.{member.Name} != null && n.{member.Name}.StartsWith(value)); }}"); builder.AppendLine($@" @@ -235,7 +260,7 @@ public static partial class {dbSetClass.Name}Extensions /// Filter the of {dbSetClass.Name} by {member.Name} ends with a value /// /// The string which {member.Name} should end with - public static IQueryable<{dbSetClass.Name}> By{member.Name}EndsWith(this IQueryable<{dbSetClass.Name}> query, {type}? value) + public static IQueryable<{dbSetClass.Name}> By{member.Name}EndsWith(this IQueryable<{dbSetClass.Name}> query, string? value) {{ if (query == null) return query; @@ -243,9 +268,8 @@ public static partial class {dbSetClass.Name}Extensions if (string.IsNullOrWhiteSpace(value)) return query; - return query.Where(n => n.{member.Name}.EndsWith(value)); + return query.Where(n => n.{member.Name} != null && n.{member.Name}.EndsWith(value)); }}"); - } else if (type.Equals(dateTimeSymbol, SymbolEqualityComparer.Default)) { From 683418a1d2cfb914e4e0033b1bbb435c772af562 Mon Sep 17 00:00:00 2001 From: Nate Zaugg Date: Wed, 12 Jan 2022 15:14:06 -0700 Subject: [PATCH 10/17] Trying to make both sides nullable --- .../ExtensionMethodGenerator.cs | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/EFRepository.Generator/ExtensionMethodGenerator.cs b/src/EFRepository.Generator/ExtensionMethodGenerator.cs index 320017b..d368df9 100644 --- a/src/EFRepository.Generator/ExtensionMethodGenerator.cs +++ b/src/EFRepository.Generator/ExtensionMethodGenerator.cs @@ -187,7 +187,7 @@ public static partial class {dbSetClass.Name}Extensions /// /// Filter the of {dbSetClass.Name} by {member.Name} is null /// - public static IQueryable<{dbSetClass.Name}> By{member.Name}IsNullOrWhiteSpace(this IQueryable<{dbSetClass.Name}> query) + public static IQueryable<{dbSetClass.Name}>? By{member.Name}IsNullOrWhiteSpace(this IQueryable<{dbSetClass.Name}>? query) {{ if (query == null) return query; @@ -211,7 +211,7 @@ public static partial class {dbSetClass.Name}Extensions /// Filter the of {dbSetClass.Name} by {member.Name} /// /// The string which {member.Name} should be equal - public static IQueryable<{dbSetClass.Name}> By{member.Name}(this IQueryable<{dbSetClass.Name}> query, string? value) + public static IQueryable<{dbSetClass.Name}>? By{member.Name}(this IQueryable<{dbSetClass.Name}>? query, string? value) {{ if (query == null) return query; @@ -227,7 +227,7 @@ public static partial class {dbSetClass.Name}Extensions /// Filter the of {dbSetClass.Name} by {member.Name} contains a value /// /// The string which {member.Name} should contain - public static IQueryable<{dbSetClass.Name}> By{member.Name}Contains(this IQueryable<{dbSetClass.Name}> query, string? value) + public static IQueryable<{dbSetClass.Name}>? By{member.Name}Contains(this IQueryable<{dbSetClass.Name}>? query, string? value) {{ if (query == null) return query; @@ -243,7 +243,7 @@ public static partial class {dbSetClass.Name}Extensions /// Filter the of {dbSetClass.Name} by {member.Name} starts with a value /// /// The string which {member.Name} should start with - public static IQueryable<{dbSetClass.Name}> By{member.Name}StartsWith(this IQueryable<{dbSetClass.Name}> query, string? value) + public static IQueryable<{dbSetClass.Name}>? By{member.Name}StartsWith(this IQueryable<{dbSetClass.Name}>? query, string? value) {{ if (query == null) return query; @@ -260,7 +260,7 @@ public static partial class {dbSetClass.Name}Extensions /// Filter the of {dbSetClass.Name} by {member.Name} ends with a value /// /// The string which {member.Name} should end with - public static IQueryable<{dbSetClass.Name}> By{member.Name}EndsWith(this IQueryable<{dbSetClass.Name}> query, string? value) + public static IQueryable<{dbSetClass.Name}>? By{member.Name}EndsWith(this IQueryable<{dbSetClass.Name}>? query, string? value) {{ if (query == null) return query; @@ -280,7 +280,7 @@ public static partial class {dbSetClass.Name}Extensions /// Filter the of {dbSetClass.Name} by whether or not the provided is after {member.Name} /// /// The that {member.Name} should be before - public static IQueryable<{dbSetClass}> By{member.Name}IsBefore(this IQueryable<{dbSetClass.Name}> query, DateTime? value) + public static IQueryable<{dbSetClass}>? By{member.Name}IsBefore(this IQueryable<{dbSetClass.Name}>? query, DateTime? value) {{ if (query == null) return query; @@ -296,7 +296,7 @@ public static partial class {dbSetClass.Name}Extensions /// Filter the of {dbSetClass.Name} by whether or not the provided is after {member.Name} /// /// The that {member.Name} should be after - public static IQueryable<{dbSetClass}> By{member.Name}IsAfter(this IQueryable<{dbSetClass.Name}> query, DateTime? value) + public static IQueryable<{dbSetClass}>? By{member.Name}IsAfter(this IQueryable<{dbSetClass.Name}>? query, DateTime? value) {{ if (query == null) return query; @@ -313,7 +313,7 @@ public static partial class {dbSetClass.Name}Extensions /// /// The that should be before {member.Name} /// The that should be after {member.Name} - public static IQueryable<{dbSetClass}> By{member.Name}Between(this IQueryable<{dbSetClass.Name}> query, DateTime? start, DateTime? end) + public static IQueryable<{dbSetClass}>? By{member.Name}Between(this IQueryable<{dbSetClass.Name}>? query, DateTime? start, DateTime? end) {{ if (query == null) return query; @@ -332,7 +332,7 @@ public static partial class {dbSetClass.Name}Extensions /// Filter the of {dbSetClass.Name} by whether or not {member.Name} is between the two provided values. /// /// The that should the same date as {member.Name}, excluding time - public static IQueryable<{dbSetClass}> By{member.Name}OnDate(this IQueryable<{dbSetClass.Name}> query, DateTime? value) + public static IQueryable<{dbSetClass}>? By{member.Name}OnDate(this IQueryable<{dbSetClass.Name}>? query, DateTime? value) {{ if (query == null) return query; @@ -383,7 +383,7 @@ string CreateNullMethodFunctions(string memberName) /// /// Filter the of {dbSetClass.Name} by {memberName} is null /// - public static IQueryable<{dbSetClass.Name}> By{memberName}IsNull(this IQueryable<{dbSetClass.Name}> query) + public static IQueryable<{dbSetClass.Name}>? By{memberName}IsNull(this IQueryable<{dbSetClass.Name}>? query) {{ if (query == null) return query; @@ -394,7 +394,7 @@ string CreateNullMethodFunctions(string memberName) /// /// Filter the of {dbSetClass.Name} by {memberName} is not null /// - public static IQueryable<{dbSetClass.Name}> By{memberName}IsNotNull(this IQueryable<{dbSetClass.Name}> query) + public static IQueryable<{dbSetClass.Name}>? By{memberName}IsNotNull(this IQueryable<{dbSetClass.Name}>? query) {{ if (query == null) return query; From cfb6058cc67042e3dd80e4f29de795720222cf93 Mon Sep 17 00:00:00 2001 From: Nate Zaugg Date: Wed, 12 Jan 2022 15:46:57 -0700 Subject: [PATCH 11/17] Fixing nullable warnings that spew out when referencing this package --- .../ContextTests.cs | 482 +++++++++++++++++- .../ExtensionMethodGenerator.cs | 114 ++--- 2 files changed, 504 insertions(+), 92 deletions(-) diff --git a/src/EFRepository.Generator.IntegrationTests/ContextTests.cs b/src/EFRepository.Generator.IntegrationTests/ContextTests.cs index c9ffd70..f36a368 100644 --- a/src/EFRepository.Generator.IntegrationTests/ContextTests.cs +++ b/src/EFRepository.Generator.IntegrationTests/ContextTests.cs @@ -56,65 +56,515 @@ public void UserMethodsGenerated() // DateTime Functions usersQueryable.ByCreatedIsAfter(now.AddHours(-5.5)) - .ShouldNotBeNull() .Count().ShouldBe(5); usersQueryable.ByCreatedOnDate(now.AddDays(-5)) - .ShouldNotBeNull() .Count().ShouldBe(1); usersQueryable.ByCreatedIsBefore(now.AddHours(-5.5)) - .ShouldNotBeNull() .Count().ShouldBe(6); usersQueryable.ByCreatedBetween(start: now.AddHours(-5.5), end: now.AddHours(-2.5)) - .ShouldNotBeNull() .Count().ShouldBe(3); // String functions usersQueryable.ByAddress("1 Fake St.") - .ShouldNotBeNull() + .Count().ShouldBe(1); + + usersQueryable.ByAddress("1 Fake St.") .Count().ShouldBe(1); usersQueryable.ByAddressIsNotNull() - .ShouldNotBeNull() + .ByAddressStartsWith("1") .Count().ShouldBe(10); usersQueryable.ByAddressIsNull() - .ShouldNotBeNull() .Count().ShouldBe(1); usersQueryable.ByNameIsNull() - .ShouldNotBeNull() .Count().ShouldBe(0); usersQueryable.ByNameIsNullOrWhiteSpace() - .ShouldNotBeNull() .Count().ShouldBe(1); usersQueryable.ByPhoneIsNullOrWhiteSpace() - .ShouldNotBeNull() .Count().ShouldBe(1); usersQueryable.ByAddressIsNullOrWhiteSpace() - .ShouldNotBeNull() .Count().ShouldBe(1); usersQueryable.ByAddressIsNotNullOrWhiteSpace() - .ShouldNotBeNull() .Count().ShouldBe(10); usersQueryable.ByAddressStartsWith("1") - .ShouldNotBeNull() .Count().ShouldBe(2); usersQueryable.ByAddressEndsWith("St.") - .ShouldNotBeNull() .Count().ShouldBe(10); usersQueryable.ByAddressContains("Fake") - .ShouldNotBeNull() .Count().ShouldBe(10); + + // Testing chained functions + usersQueryable.ByAddress("1 Fake St.") + .ByAddressIsNotNull() + .ByNameIsNotNull() + .ByPhoneContains("801") + .ByPhone("201-111-0221") + .Count().ShouldBe(0); + + } + } +} + + +/* + + +namespace EFRepository.Generator.IntegrationTests +{ + + public static partial class UserExtensions + { + + /// + /// Filter the of User by Id + /// + /// The int which should equal Id + public static IQueryable? ById(this IQueryable? query, int? value) + { + if (query == null) + return query; + + if (value == null) + return query; + + return query.Where(n => n.Id == value); + } + + /// + /// Filter the of User by Name is null + /// + public static IQueryable? ByNameIsNull(this IQueryable? query) + { + if (query == null) + return query; + + return query.Where(n => n.Name == null); + } + + /// + /// Filter the of User by Name is not null + /// + public static IQueryable? ByNameIsNotNull(this IQueryable? query) + { + if (query == null) + return query; + + return query.Where(n => n.Name != null); + } + + /// + /// Filter the of User by Name is null + /// + public static IQueryable? ByNameIsNullOrWhiteSpace(this IQueryable? query) + { + if (query == null) + return query; + + return query.Where(n => string.IsNullOrWhiteSpace(n.Name)); + } + + /// + /// Filter the of User by Name is not null + /// + public static IQueryable? ByNameIsNotNullOrWhiteSpace(this IQueryable? query) + { + if (query == null) + return query; + + return query.Where(n => !string.IsNullOrWhiteSpace(n.Name)); + } + + /// + /// Filter the of User by Name + /// + /// The string which Name should be equal + public static IQueryable? ByName(this IQueryable query, string? value) + { + if (query == null) + return query; + + if (string.IsNullOrWhiteSpace(value)) + return query; + + return query.Where(n => n.Name == value); + } + + /// + /// Filter the of User by Name contains a value + /// + /// The string which Name should contain + public static IQueryable? ByNameContains(this IQueryable query, string? value) + { + if (query == null) + return query; + + if (string.IsNullOrWhiteSpace(value)) + return query; + + return query.Where(n => n.Name != null && n.Name.Contains(value)); + } + + /// + /// Filter the of User by Name starts with a value + /// + /// The string which Name should start with + public static IQueryable? ByNameStartsWith(this IQueryable query, string? value) + { + if (query == null) + return query; + + if (string.IsNullOrWhiteSpace(value)) + return query; + + return query.Where(n => n.Name != null && n.Name.StartsWith(value)); + } + + + /// + /// Filter the of User by Name ends with a value + /// + /// The string which Name should end with + public static IQueryable? ByNameEndsWith(this IQueryable query, string? value) + { + if (query == null) + return query; + + if (string.IsNullOrWhiteSpace(value)) + return query; + + return query.Where(n => n.Name != null && n.Name.EndsWith(value)); + } + + /// + /// Filter the of User by Phone is null + /// + public static IQueryable? ByPhoneIsNull(this IQueryable query) + { + if (query == null) + return query; + + return query.Where(n => n.Phone == null); + } + + /// + /// Filter the of User by Phone is not null + /// + public static IQueryable? ByPhoneIsNotNull(this IQueryable query) + { + if (query == null) + return query; + + return query.Where(n => n.Phone != null); + } + + /// + /// Filter the of User by Phone is null + /// + public static IQueryable ByPhoneIsNullOrWhiteSpace(this IQueryable query) + { + if (query == null) + return query; + + return query.Where(n => string.IsNullOrWhiteSpace(n.Phone)); + } + + /// + /// Filter the of User by Phone is not null + /// + public static IQueryable ByPhoneIsNotNullOrWhiteSpace(this IQueryable query) + { + if (query == null) + return query; + + return query.Where(n => !string.IsNullOrWhiteSpace(n.Phone)); + } + + /// + /// Filter the of User by Phone + /// + /// The string which Phone should be equal + public static IQueryable ByPhone(this IQueryable query, string? value) + { + if (query == null) + return query; + + if (string.IsNullOrWhiteSpace(value)) + return query; + + return query.Where(n => n.Phone == value); + } + + /// + /// Filter the of User by Phone contains a value + /// + /// The string which Phone should contain + public static IQueryable? ByPhoneContains(this IQueryable query, string? value) + { + if (query == null) + return query; + + if (string.IsNullOrWhiteSpace(value)) + return query; + + return query.Where(n => n.Phone != null && n.Phone.Contains(value)); + } + + /// + /// Filter the of User by Phone starts with a value + /// + /// The string which Phone should start with + public static IQueryable? ByPhoneStartsWith(this IQueryable query, string? value) + { + if (query == null) + return query; + + if (string.IsNullOrWhiteSpace(value)) + return query; + + return query.Where(n => n.Phone != null && n.Phone.StartsWith(value)); + } + + + /// + /// Filter the of User by Phone ends with a value + /// + /// The string which Phone should end with + public static IQueryable? ByPhoneEndsWith(this IQueryable query, string? value) + { + if (query == null) + return query; + + if (string.IsNullOrWhiteSpace(value)) + return query; + + return query.Where(n => n.Phone != null && n.Phone.EndsWith(value)); + } + + /// + /// Filter the of User by Address is null + /// + public static IQueryable? ByAddressIsNull(this IQueryable query) + { + if (query == null) + return query; + + return query.Where(n => n.Address == null); + } + + /// + /// Filter the of User by Address is not null + /// + public static IQueryable? ByAddressIsNotNull(this IQueryable query) + { + if (query == null) + return query; + + return query.Where(n => n.Address != null); + } + + /// + /// Filter the of User by Address is null + /// + public static IQueryable ByAddressIsNullOrWhiteSpace(this IQueryable query) + { + if (query == null) + return query; + + return query.Where(n => string.IsNullOrWhiteSpace(n.Address)); + } + + /// + /// Filter the of User by Address is not null + /// + public static IQueryable ByAddressIsNotNullOrWhiteSpace(this IQueryable query) + { + if (query == null) + return query; + + return query.Where(n => !string.IsNullOrWhiteSpace(n.Address)); + } + + /// + /// Filter the of User by Address + /// + /// The string which Address should be equal + public static IQueryable ByAddress(this IQueryable query, string? value) + { + if (query == null) + return query; + + if (string.IsNullOrWhiteSpace(value)) + return query; + + return query.Where(n => n.Address == value); + } + + /// + /// Filter the of User by Address contains a value + /// + /// The string which Address should contain + public static IQueryable? ByAddressContains(this IQueryable query, string? value) + { + if (query == null) + return query; + + if (string.IsNullOrWhiteSpace(value)) + return query; + + return query.Where(n => n.Address != null && n.Address.Contains(value)); + } + + /// + /// Filter the of User by Address starts with a value + /// + /// The string which Address should start with + public static IQueryable? ByAddressStartsWith(this IQueryable query, string? value) + { + if (query == null) + return query; + + if (string.IsNullOrWhiteSpace(value)) + return query; + + return query.Where(n => n.Address != null && n.Address.StartsWith(value)); + } + + + /// + /// Filter the of User by Address ends with a value + /// + /// The string which Address should end with + public static IQueryable? ByAddressEndsWith(this IQueryable query, string? value) + { + if (query == null) + return query; + + if (string.IsNullOrWhiteSpace(value)) + return query; + + return query.Where(n => n.Address != null && n.Address.EndsWith(value)); + } + + /// + /// Filter the of User by Created + /// + /// The DateTime which should equal Created + public static IQueryable ByCreated(this IQueryable query, DateTime? value) + { + if (query == null) + return query; + + if (value == null) + return query; + + return query.Where(n => n.Created == value); + } + + /// + /// Filter the of User by whether or not the provided is after Created + /// + /// The that Created should be before + public static IQueryable? ByCreatedIsBefore(this IQueryable query, DateTime? value) + { + if (query == null) + return query; + + if (value == null) + return query; + + return query.Where(n => n.Created < value); + } + + /// + /// Filter the of User by whether or not the provided is after Created + /// + /// The that Created should be after + public static IQueryable? ByCreatedIsAfter(this IQueryable query, DateTime? value) + { + if (query == null) + return query; + + if (value == null) + return query; + + return query.Where(n => n.Created > value); + } + + /// + /// Filter the of User by whether or not Created is between the two provided values. + /// + /// The that should be before Created + /// The that should be after Created + public static IQueryable? ByCreatedBetween(this IQueryable query, DateTime? start, DateTime? end) + { + if (query == null) + return query; + + if (start != null) + query = query.Where(n => n.Created > start); + + if (end != null) + query = query.Where(n => n.Created < end); + + return query; + } + + /// + /// Filter the of User by whether or not Created is between the two provided values. + /// + /// The that should the same date as Created, excluding time + public static IQueryable? ByCreatedOnDate(this IQueryable query, DateTime? value) + { + if (query == null) + return query; + + if (value != null) + return query.Where(n => n.Created.Date == value.Value.Date); + else + return query; + } + + /// + /// Filter the of User by IsDeleted + /// + /// The bool which should equal IsDeleted + public static IQueryable ByIsDeleted(this IQueryable query, bool? value) + { + if (query == null) + return query; + + if (value == null) + return query; + + return query.Where(n => n.IsDeleted == value); + } + + /// + /// Filter the of User by Score + /// + /// The double which should equal Score + public static IQueryable ByScore(this IQueryable query, double? value) + { + if (query == null) + return query; + + if (value == null) + return query; + + return query.Where(n => n.Score == value); } } -} \ No newline at end of file +} +*/ diff --git a/src/EFRepository.Generator/ExtensionMethodGenerator.cs b/src/EFRepository.Generator/ExtensionMethodGenerator.cs index d368df9..2862317 100644 --- a/src/EFRepository.Generator/ExtensionMethodGenerator.cs +++ b/src/EFRepository.Generator/ExtensionMethodGenerator.cs @@ -44,12 +44,7 @@ public void Execute(GeneratorExecutionContext context) if (sourceModel == null || sourceSymbol == null) return null; - return new SourceRecord - { - Model = sourceModel, - Symbol = sourceSymbol, - Syntax = c - }; + return new SourceRecord(sourceModel, sourceSymbol, c); }).Where(c => c != null) .Where(c => c != null && @@ -67,33 +62,32 @@ public void Execute(GeneratorExecutionContext context) } } - protected IEnumerable<(string FileName, string Content)> ProcessDbContext(SourceRecord record, INamedTypeSymbol dbSetSymbol, Compilation compilation) + protected IEnumerable<(string FileName, string Content)> ProcessDbContext(SourceRecord? record, INamedTypeSymbol dbSetSymbol, Compilation compilation) { - var dbSets = record.Symbol.GetMembers() - .Where(m => !m.IsStatic && !m.IsAbstract && !m.IsImplicitlyDeclared - && m.Kind is SymbolKind.Property && m.IsDefinition) - .Cast() - .Where(p => !p.IsReadOnly && !p.IsExtern) - .Where(p => + if (record != null) { - return p.Type.OriginalDefinition.Equals(dbSetSymbol, SymbolEqualityComparer.Default); - }); - - var list = new List<(string FileName, string Content)>(); + var dbSets = record.Symbol.GetMembers() + .Where(m => !m.IsStatic && !m.IsAbstract && !m.IsImplicitlyDeclared + && m.Kind is SymbolKind.Property && m.IsDefinition) + .Cast() + .Where(p => !p.IsReadOnly && !p.IsExtern) + .Where(p => + { + return p.Type.OriginalDefinition.Equals(dbSetSymbol, SymbolEqualityComparer.Default); + }); - foreach (var dbSet in dbSets) - { - var genericType = ((INamedTypeSymbol)dbSet.Type).TypeArguments[0]; + foreach (var dbSet in dbSets) + { + var genericType = ((INamedTypeSymbol)dbSet.Type).TypeArguments[0]; - if (genericType == null) - continue; + if (genericType == null) + continue; - var sourceCode = BuildExtensionMethod((INamedTypeSymbol)genericType, compilation); + var sourceCode = BuildExtensionMethod((INamedTypeSymbol)genericType, compilation); - list.Add(sourceCode); + yield return sourceCode; + } } - - return list; } protected (string FileName, string Content) BuildExtensionMethod(INamedTypeSymbol dbSetClass, Compilation compilation) @@ -187,11 +181,8 @@ public static partial class {dbSetClass.Name}Extensions /// /// Filter the of {dbSetClass.Name} by {member.Name} is null /// - public static IQueryable<{dbSetClass.Name}>? By{member.Name}IsNullOrWhiteSpace(this IQueryable<{dbSetClass.Name}>? query) + public static IQueryable<{dbSetClass.Name}> By{member.Name}IsNullOrWhiteSpace(this IQueryable<{dbSetClass.Name}> query) {{ - if (query == null) - return query; - return query.Where(n => string.IsNullOrWhiteSpace(n.{member.Name})); }} @@ -200,9 +191,6 @@ public static partial class {dbSetClass.Name}Extensions /// public static IQueryable<{dbSetClass.Name}> By{member.Name}IsNotNullOrWhiteSpace(this IQueryable<{dbSetClass.Name}> query) {{ - if (query == null) - return query; - return query.Where(n => !string.IsNullOrWhiteSpace(n.{member.Name})); }}"); @@ -211,11 +199,8 @@ public static partial class {dbSetClass.Name}Extensions /// Filter the of {dbSetClass.Name} by {member.Name} /// /// The string which {member.Name} should be equal - public static IQueryable<{dbSetClass.Name}>? By{member.Name}(this IQueryable<{dbSetClass.Name}>? query, string? value) + public static IQueryable<{dbSetClass.Name}> By{member.Name}(this IQueryable<{dbSetClass.Name}> query, string? value) {{ - if (query == null) - return query; - if (string.IsNullOrWhiteSpace(value)) return query; @@ -227,11 +212,8 @@ public static partial class {dbSetClass.Name}Extensions /// Filter the of {dbSetClass.Name} by {member.Name} contains a value /// /// The string which {member.Name} should contain - public static IQueryable<{dbSetClass.Name}>? By{member.Name}Contains(this IQueryable<{dbSetClass.Name}>? query, string? value) + public static IQueryable<{dbSetClass.Name}> By{member.Name}Contains(this IQueryable<{dbSetClass.Name}> query, string? value) {{ - if (query == null) - return query; - if (string.IsNullOrWhiteSpace(value)) return query; @@ -243,11 +225,8 @@ public static partial class {dbSetClass.Name}Extensions /// Filter the of {dbSetClass.Name} by {member.Name} starts with a value /// /// The string which {member.Name} should start with - public static IQueryable<{dbSetClass.Name}>? By{member.Name}StartsWith(this IQueryable<{dbSetClass.Name}>? query, string? value) + public static IQueryable<{dbSetClass.Name}> By{member.Name}StartsWith(this IQueryable<{dbSetClass.Name}> query, string? value) {{ - if (query == null) - return query; - if (string.IsNullOrWhiteSpace(value)) return query; @@ -260,11 +239,8 @@ public static partial class {dbSetClass.Name}Extensions /// Filter the of {dbSetClass.Name} by {member.Name} ends with a value /// /// The string which {member.Name} should end with - public static IQueryable<{dbSetClass.Name}>? By{member.Name}EndsWith(this IQueryable<{dbSetClass.Name}>? query, string? value) + public static IQueryable<{dbSetClass.Name}> By{member.Name}EndsWith(this IQueryable<{dbSetClass.Name}> query, string? value) {{ - if (query == null) - return query; - if (string.IsNullOrWhiteSpace(value)) return query; @@ -280,11 +256,8 @@ public static partial class {dbSetClass.Name}Extensions /// Filter the of {dbSetClass.Name} by whether or not the provided is after {member.Name} /// /// The that {member.Name} should be before - public static IQueryable<{dbSetClass}>? By{member.Name}IsBefore(this IQueryable<{dbSetClass.Name}>? query, DateTime? value) + public static IQueryable<{dbSetClass}> By{member.Name}IsBefore(this IQueryable<{dbSetClass.Name}> query, DateTime? value) {{ - if (query == null) - return query; - if (value == null) return query; @@ -296,11 +269,8 @@ public static partial class {dbSetClass.Name}Extensions /// Filter the of {dbSetClass.Name} by whether or not the provided is after {member.Name} /// /// The that {member.Name} should be after - public static IQueryable<{dbSetClass}>? By{member.Name}IsAfter(this IQueryable<{dbSetClass.Name}>? query, DateTime? value) + public static IQueryable<{dbSetClass}> By{member.Name}IsAfter(this IQueryable<{dbSetClass.Name}> query, DateTime? value) {{ - if (query == null) - return query; - if (value == null) return query; @@ -313,11 +283,8 @@ public static partial class {dbSetClass.Name}Extensions /// /// The that should be before {member.Name} /// The that should be after {member.Name} - public static IQueryable<{dbSetClass}>? By{member.Name}Between(this IQueryable<{dbSetClass.Name}>? query, DateTime? start, DateTime? end) + public static IQueryable<{dbSetClass}> By{member.Name}Between(this IQueryable<{dbSetClass.Name}> query, DateTime? start, DateTime? end) {{ - if (query == null) - return query; - if (start != null) query = query.Where(n => n.{member.Name} > start); @@ -332,11 +299,8 @@ public static partial class {dbSetClass.Name}Extensions /// Filter the of {dbSetClass.Name} by whether or not {member.Name} is between the two provided values. /// /// The that should the same date as {member.Name}, excluding time - public static IQueryable<{dbSetClass}>? By{member.Name}OnDate(this IQueryable<{dbSetClass.Name}>? query, DateTime? value) + public static IQueryable<{dbSetClass}> By{member.Name}OnDate(this IQueryable<{dbSetClass.Name}> query, DateTime? value) {{ - if (query == null) - return query; - if (value != null) return query.Where(n => n.{member.Name}.Date == value.Value.Date); else @@ -360,9 +324,6 @@ string CreateMethod(string type, string memberName, bool nullable) /// The {type} which should equal {memberName} public static IQueryable<{dbSetClass.Name}> By{memberName}(this IQueryable<{dbSetClass.Name}> query, {type}? value) {{ - if (query == null) - return query; - if (value == null) return query; @@ -383,22 +344,16 @@ string CreateNullMethodFunctions(string memberName) /// /// Filter the of {dbSetClass.Name} by {memberName} is null /// - public static IQueryable<{dbSetClass.Name}>? By{memberName}IsNull(this IQueryable<{dbSetClass.Name}>? query) + public static IQueryable<{dbSetClass.Name}> By{memberName}IsNull(this IQueryable<{dbSetClass.Name}> query) {{ - if (query == null) - return query; - return query.Where(n => n.{memberName} == null); }} /// /// Filter the of {dbSetClass.Name} by {memberName} is not null /// - public static IQueryable<{dbSetClass.Name}>? By{memberName}IsNotNull(this IQueryable<{dbSetClass.Name}>? query) + public static IQueryable<{dbSetClass.Name}> By{memberName}IsNotNull(this IQueryable<{dbSetClass.Name}> query) {{ - if (query == null) - return query; - return query.Where(n => n.{memberName} != null); }}"; } @@ -410,4 +365,11 @@ public record SourceRecord public SemanticModel Model { get; init; } public INamedTypeSymbol Symbol { get; init; } public TypeDeclarationSyntax Syntax { get; init; } + + public SourceRecord(SemanticModel model, INamedTypeSymbol symbol, TypeDeclarationSyntax syntax) + { + Model = model; + Symbol = symbol; + Syntax = syntax; + } } From bc110818c22112e6a5b9a516e30a3e42149a8f0c Mon Sep 17 00:00:00 2001 From: Nate Zaugg Date: Wed, 12 Jan 2022 15:50:08 -0700 Subject: [PATCH 12/17] Removed junk! --- .../ContextTests.cs | 454 +----------------- 1 file changed, 1 insertion(+), 453 deletions(-) diff --git a/src/EFRepository.Generator.IntegrationTests/ContextTests.cs b/src/EFRepository.Generator.IntegrationTests/ContextTests.cs index f36a368..3202cf1 100644 --- a/src/EFRepository.Generator.IntegrationTests/ContextTests.cs +++ b/src/EFRepository.Generator.IntegrationTests/ContextTests.cs @@ -115,456 +115,4 @@ public void UserMethodsGenerated() } } -} - - -/* - - -namespace EFRepository.Generator.IntegrationTests -{ - - public static partial class UserExtensions - { - - /// - /// Filter the of User by Id - /// - /// The int which should equal Id - public static IQueryable? ById(this IQueryable? query, int? value) - { - if (query == null) - return query; - - if (value == null) - return query; - - return query.Where(n => n.Id == value); - } - - /// - /// Filter the of User by Name is null - /// - public static IQueryable? ByNameIsNull(this IQueryable? query) - { - if (query == null) - return query; - - return query.Where(n => n.Name == null); - } - - /// - /// Filter the of User by Name is not null - /// - public static IQueryable? ByNameIsNotNull(this IQueryable? query) - { - if (query == null) - return query; - - return query.Where(n => n.Name != null); - } - - /// - /// Filter the of User by Name is null - /// - public static IQueryable? ByNameIsNullOrWhiteSpace(this IQueryable? query) - { - if (query == null) - return query; - - return query.Where(n => string.IsNullOrWhiteSpace(n.Name)); - } - - /// - /// Filter the of User by Name is not null - /// - public static IQueryable? ByNameIsNotNullOrWhiteSpace(this IQueryable? query) - { - if (query == null) - return query; - - return query.Where(n => !string.IsNullOrWhiteSpace(n.Name)); - } - - /// - /// Filter the of User by Name - /// - /// The string which Name should be equal - public static IQueryable? ByName(this IQueryable query, string? value) - { - if (query == null) - return query; - - if (string.IsNullOrWhiteSpace(value)) - return query; - - return query.Where(n => n.Name == value); - } - - /// - /// Filter the of User by Name contains a value - /// - /// The string which Name should contain - public static IQueryable? ByNameContains(this IQueryable query, string? value) - { - if (query == null) - return query; - - if (string.IsNullOrWhiteSpace(value)) - return query; - - return query.Where(n => n.Name != null && n.Name.Contains(value)); - } - - /// - /// Filter the of User by Name starts with a value - /// - /// The string which Name should start with - public static IQueryable? ByNameStartsWith(this IQueryable query, string? value) - { - if (query == null) - return query; - - if (string.IsNullOrWhiteSpace(value)) - return query; - - return query.Where(n => n.Name != null && n.Name.StartsWith(value)); - } - - - /// - /// Filter the of User by Name ends with a value - /// - /// The string which Name should end with - public static IQueryable? ByNameEndsWith(this IQueryable query, string? value) - { - if (query == null) - return query; - - if (string.IsNullOrWhiteSpace(value)) - return query; - - return query.Where(n => n.Name != null && n.Name.EndsWith(value)); - } - - /// - /// Filter the of User by Phone is null - /// - public static IQueryable? ByPhoneIsNull(this IQueryable query) - { - if (query == null) - return query; - - return query.Where(n => n.Phone == null); - } - - /// - /// Filter the of User by Phone is not null - /// - public static IQueryable? ByPhoneIsNotNull(this IQueryable query) - { - if (query == null) - return query; - - return query.Where(n => n.Phone != null); - } - - /// - /// Filter the of User by Phone is null - /// - public static IQueryable ByPhoneIsNullOrWhiteSpace(this IQueryable query) - { - if (query == null) - return query; - - return query.Where(n => string.IsNullOrWhiteSpace(n.Phone)); - } - - /// - /// Filter the of User by Phone is not null - /// - public static IQueryable ByPhoneIsNotNullOrWhiteSpace(this IQueryable query) - { - if (query == null) - return query; - - return query.Where(n => !string.IsNullOrWhiteSpace(n.Phone)); - } - - /// - /// Filter the of User by Phone - /// - /// The string which Phone should be equal - public static IQueryable ByPhone(this IQueryable query, string? value) - { - if (query == null) - return query; - - if (string.IsNullOrWhiteSpace(value)) - return query; - - return query.Where(n => n.Phone == value); - } - - /// - /// Filter the of User by Phone contains a value - /// - /// The string which Phone should contain - public static IQueryable? ByPhoneContains(this IQueryable query, string? value) - { - if (query == null) - return query; - - if (string.IsNullOrWhiteSpace(value)) - return query; - - return query.Where(n => n.Phone != null && n.Phone.Contains(value)); - } - - /// - /// Filter the of User by Phone starts with a value - /// - /// The string which Phone should start with - public static IQueryable? ByPhoneStartsWith(this IQueryable query, string? value) - { - if (query == null) - return query; - - if (string.IsNullOrWhiteSpace(value)) - return query; - - return query.Where(n => n.Phone != null && n.Phone.StartsWith(value)); - } - - - /// - /// Filter the of User by Phone ends with a value - /// - /// The string which Phone should end with - public static IQueryable? ByPhoneEndsWith(this IQueryable query, string? value) - { - if (query == null) - return query; - - if (string.IsNullOrWhiteSpace(value)) - return query; - - return query.Where(n => n.Phone != null && n.Phone.EndsWith(value)); - } - - /// - /// Filter the of User by Address is null - /// - public static IQueryable? ByAddressIsNull(this IQueryable query) - { - if (query == null) - return query; - - return query.Where(n => n.Address == null); - } - - /// - /// Filter the of User by Address is not null - /// - public static IQueryable? ByAddressIsNotNull(this IQueryable query) - { - if (query == null) - return query; - - return query.Where(n => n.Address != null); - } - - /// - /// Filter the of User by Address is null - /// - public static IQueryable ByAddressIsNullOrWhiteSpace(this IQueryable query) - { - if (query == null) - return query; - - return query.Where(n => string.IsNullOrWhiteSpace(n.Address)); - } - - /// - /// Filter the of User by Address is not null - /// - public static IQueryable ByAddressIsNotNullOrWhiteSpace(this IQueryable query) - { - if (query == null) - return query; - - return query.Where(n => !string.IsNullOrWhiteSpace(n.Address)); - } - - /// - /// Filter the of User by Address - /// - /// The string which Address should be equal - public static IQueryable ByAddress(this IQueryable query, string? value) - { - if (query == null) - return query; - - if (string.IsNullOrWhiteSpace(value)) - return query; - - return query.Where(n => n.Address == value); - } - - /// - /// Filter the of User by Address contains a value - /// - /// The string which Address should contain - public static IQueryable? ByAddressContains(this IQueryable query, string? value) - { - if (query == null) - return query; - - if (string.IsNullOrWhiteSpace(value)) - return query; - - return query.Where(n => n.Address != null && n.Address.Contains(value)); - } - - /// - /// Filter the of User by Address starts with a value - /// - /// The string which Address should start with - public static IQueryable? ByAddressStartsWith(this IQueryable query, string? value) - { - if (query == null) - return query; - - if (string.IsNullOrWhiteSpace(value)) - return query; - - return query.Where(n => n.Address != null && n.Address.StartsWith(value)); - } - - - /// - /// Filter the of User by Address ends with a value - /// - /// The string which Address should end with - public static IQueryable? ByAddressEndsWith(this IQueryable query, string? value) - { - if (query == null) - return query; - - if (string.IsNullOrWhiteSpace(value)) - return query; - - return query.Where(n => n.Address != null && n.Address.EndsWith(value)); - } - - /// - /// Filter the of User by Created - /// - /// The DateTime which should equal Created - public static IQueryable ByCreated(this IQueryable query, DateTime? value) - { - if (query == null) - return query; - - if (value == null) - return query; - - return query.Where(n => n.Created == value); - } - - /// - /// Filter the of User by whether or not the provided is after Created - /// - /// The that Created should be before - public static IQueryable? ByCreatedIsBefore(this IQueryable query, DateTime? value) - { - if (query == null) - return query; - - if (value == null) - return query; - - return query.Where(n => n.Created < value); - } - - /// - /// Filter the of User by whether or not the provided is after Created - /// - /// The that Created should be after - public static IQueryable? ByCreatedIsAfter(this IQueryable query, DateTime? value) - { - if (query == null) - return query; - - if (value == null) - return query; - - return query.Where(n => n.Created > value); - } - - /// - /// Filter the of User by whether or not Created is between the two provided values. - /// - /// The that should be before Created - /// The that should be after Created - public static IQueryable? ByCreatedBetween(this IQueryable query, DateTime? start, DateTime? end) - { - if (query == null) - return query; - - if (start != null) - query = query.Where(n => n.Created > start); - - if (end != null) - query = query.Where(n => n.Created < end); - - return query; - } - - /// - /// Filter the of User by whether or not Created is between the two provided values. - /// - /// The that should the same date as Created, excluding time - public static IQueryable? ByCreatedOnDate(this IQueryable query, DateTime? value) - { - if (query == null) - return query; - - if (value != null) - return query.Where(n => n.Created.Date == value.Value.Date); - else - return query; - } - - /// - /// Filter the of User by IsDeleted - /// - /// The bool which should equal IsDeleted - public static IQueryable ByIsDeleted(this IQueryable query, bool? value) - { - if (query == null) - return query; - - if (value == null) - return query; - - return query.Where(n => n.IsDeleted == value); - } - - /// - /// Filter the of User by Score - /// - /// The double which should equal Score - public static IQueryable ByScore(this IQueryable query, double? value) - { - if (query == null) - return query; - - if (value == null) - return query; - - return query.Where(n => n.Score == value); - } - } -} -*/ +} \ No newline at end of file From 8e14c4f9d082a5a924c637e022efc92daed30289 Mon Sep 17 00:00:00 2001 From: Nate Zaugg Date: Wed, 12 Jan 2022 21:51:11 -0700 Subject: [PATCH 13/17] * Adding support for DateTimeOffset * Adding NullReferenceException being thrown if query is null * Added Support for Nullable value types * Enhanced the README.md file --- FutureGoals.md | 14 + README.md | 260 +++++++++++------- .../ContextTests.cs | 23 +- .../User.cs | 67 +++-- .../GeneratorTests.cs | 2 +- .../TestData/AllClasses.txt | 129 +++++---- .../ExtensionMethodGenerator.cs | 177 ++++++++++-- src/EFRepository.sln | 6 + src/EFRepository/IRepository.cs | 13 +- src/EFRepository/Repository.cs | 9 +- src/UnitTests/App.config | 9 +- src/UnitTests/UnitTests.csproj | 4 +- 12 files changed, 470 insertions(+), 243 deletions(-) create mode 100644 FutureGoals.md diff --git a/FutureGoals.md b/FutureGoals.md new file mode 100644 index 0000000..55a6ee6 --- /dev/null +++ b/FutureGoals.md @@ -0,0 +1,14 @@ +## Future Goals + - Better handling of client-provided ID's + - What is the expected behavior for AddOrUpdate on a new Client side generated ID? + - Add an attribute? + - Better support for Transactions + - Test to see if we're inside of a transaction? + - If the transaction is already in progress, then throw on BeginTrans + - Commit or Rollback or throw on dispose? + - Concurrency + - AddOrUpdate with a New object that uses an existing object as a relation + - If you're not using lazy loading, a simple recursive add + - If you are using lazy loading, then it's your responsibility? +- Soft Deletes +- Better support for Eager, Explicit, and Lazy Loading \ No newline at end of file diff --git a/README.md b/README.md index 4df3af0..9f8e429 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,170 @@ # EFRepository -Use a LINQ-Enabled version of the Repository Pattern for Entity Framework +Use a LINQ-Enabled version of the Repository Pattern for Entity Framework. + +## Features Include + * Compatible with .NET Framework and .NET Core Frameworks + * Source Generators automatically create extension methods that allow for a very readable fluent syntax. The generated methods start with `By` E.g. +```C# +var query = Repo.Query() + .ByUsernameStartsWith("Bill") + .ByIsDeleted(false) + .ByRegistrationDateIsBefore(startDate) + .AsNoTracking(); +``` + * Decoupling Data Context from Code for Better Testability via the `IRepository` interface + * C# 10 `nullable` compatible code is generated + * Automatic Update / Insert detection with `.AddOrUpdate` + * Force Adding of New Object with `.AddNew(object)` + * FineOne to find objects by key. Will even work with composite keys + * Events for objects being added or modified or deleted + * `async` and synchronous methods provided where possible. Chaining to LINQ's async methods is strongly advised (like `.CountAsync()`). + + Note: If a value is passed into a `By` but is null, it will be ignored. If you want to get the the value where the field is null, then use the `By...IsNull()` or `By...IsNotNull()`. E.g.: + + ```C$ +var query = Repo.Query() + .ByUsernameStartsWith(null); // No users filtered here. + ``` + +This is helpful if you want to create a "search" where you have some of the values but always have the option to have some of the values. + +It is often necessary to separate the server-side LINQ statements from the client side. In those cases use the `.AsEnumerable()` function. This will convert the IQueryable to in IEnumerable and everything after that statement will happen after the data is retrieved and there will be no attempt to convert those expressions into the query. A good use-case for this is the mapping of an collection of objects returned from the query to a different type. + +## Setting Up +Setting up is easy! Add the `Mindfire.EFRepository` nuget package to your project, then follow the steps below. + +### Step 1 - Dependency Registration +In the example below the +```C# +// This isn't really necessary, but it's a really good idea. If you have a type that you may not use or if +// you have a type that needs a factory (to use with a using statement, for example), then this pattern +// might be right up your alley +services.AddTransient(typeof(Lazy<>)); +services.AddTransient(typeof(Func<>)); + +// Now register your DataContext +services.AddScoped(); + +// Now register IRepository +services.AddScoped(); + +// Register your other services that depend on IRepository +services.AddScoped(); +``` + +### Step 2 - Use In Your Project +This example shows a service that depends on an instance of `IRepository`. This service could then be injected into something like a Controller for API. It is a good idea to map from your EF database objects to domain objects. This controller uses AutoMapper's `IMapper` interface to map between domain objects and data objects. +```C# +public class OrderService : IOrderService +{ + protected IMapper Mapper { get; } + protected IRepository Repo { get; } + + public OrderService(IMapper mapper, IRepository repository) + { + Mapper = mapper; + Repo = repository; + } + + public async Task GetOrder(int orderId) + { + var order = await Repo.Query() + .ByOrderId(orderId) + .FirstOrDefaultAsync(); + + return Mapper.Map(order); + } + + public async Task AddOrder(Order order) + { + var mapped = Mapper.Map(order); + mapped.Created = DateTimeOffset.Now; + + Repository.AddOrUpdate(mapped); + await Repository.SaveAsync(); + + return await GetOrder(mapped.OrderId); + } + ... +} +``` + +### Step 3 - Unit Testing +Here is a quick example of how to unit test a service that has a dependency on some data from the `IRepository` interface. The example below uses `XUnit` with `Shouldly` and `FakeItEasy`. + +You'll need to add a reference to the project with your `DbContext` in it. +```C# +... +using EFRepository; +using FakeItEasy; +using Shouldly; +... +public sealed class OrderServiceTests +{ + [Theory, InlineData(1)] + public async Task OrderQueryTest(int orderId) + { + // Arrange + var repo = A.Fake(); + A.CallTo(() => repo.Query()) + .Returns(GetFakeOrderData().AsQueryable()); + var orderService = new OrderService(repo); + + // Act + var target = await orderService.GetOrderById(orderId); + + // Assert + A.CallTo(() => repo.Query()).MustHaveHappened(); + target + .ShouldNotBeNull() + .OrderId.ShouldBe(orderId); + } + + private IEnumerable GetFakeOrderData() => new[] + { + new Data.Order + { + OrderId = 1, + Created = DateTime.Now, + Email = "homer@compuserv.net" + } + }; +} +``` ## Interface +Here is the full interface for `IRepository`. Beyond what is listed in the interface, the source generators create the following prototypes for each of the following types: + +### Numeric Types (byte, short, int, long, single, double, decimal) and bool + * By{Name}({type}? value) + * By{Name}GreaterThan({type}? value) + * By{Name}GreaterThanOrEqual({type}? value) + * By{Name}LessThan({type}? value) + * By{Name}LessThanOrEqual({type}? value) + +### DateTime or DateTimeOffset + * By{Name}(DateTime? value) + * By{Name}IsBefore(DateTime? value) + * By{Name}IsAfter(DateTime? value) + * By{Name}IsBetween(DateTime? start, DateTime? end) + * By{Name}OnDate(DateTime? value) - Same day, ignore the time + +### String + * By{Name}(string? value) + * By{Name}IsNullOrWhiteSpace() + * By{Name}IsNotNullOrWhiteSpace() + * By{Name}Contains(string? value) + * By{Name}StartsWith(string? value) + * By{Name}EndsWith(string? value) + +### Any type that is nullable (including `string`) + * By{Name}IsNull() + * By{Name}IsNotNull() ```C# /// -/// Interface for interacting with data storage through the repository pattern +/// Interface for interacting with data storage through a queryable repository pattern /// -/// public interface IRepository : IDisposable { @@ -24,13 +181,6 @@ public interface IRepository : IDisposable /// Queriable Entity IQueryable Query() where TEntity : class, new(); - /// - /// Join another entity - /// - /// - /// - IQueryable Join() where TEntity : class, new(); - /// /// Find an entity based on key(s) /// @@ -91,97 +241,11 @@ public interface IRepository : IDisposable /// Number of affected entities int Save(); - /// - /// Save pending changes for the collection async - /// - /// Number of affected entities - Task SaveAsync(); - /// /// Save pending changes for the collection async with cancellation /// - /// Cancelation Token + /// Cancellation Token /// Number of affected entities Task SaveAsync(CancellationToken cancellationToken = default); - - /// - /// Begins a transaction at the specified isolation level - /// - /// Will throw an excpetion if there is already a transaction in progress - /// The desired transaction isolation level - void StartTransaction(IsolationLevel isolation); - - /// - /// Begins a transaction at the specified isolation level - /// - /// Will throw an excpetion if there is already a transaction in progress - /// The desired transaction isolation level - /// Optional cancelation token - void StartTransactionAsync(IsolationLevel isolation, CancellationToken cancellationToken = default); - - /// - /// Begins a transaction at the ReadCommitted isolation level - /// - /// Will throw an excpetion if there is already a transaction in progress - void StartTransaction(); - - /// - /// Begins a transaction at the ReadCommitted isolation level - /// - /// Will throw an excpetion if there is already a transaction in progress - /// Optional cancelation token - void StartTransactionAsync(CancellationToken cancellationToken = default); - - /// - /// Commits an active transaction - /// - /// Will throw an exception if there is not a transaction already in progress - void CommitTransaction(); - - /// - /// Commits an active transaction - /// - /// Will throw an exception if there is not a transaction already in progress - /// Optional cancelation token - void CommitTransactionAsync(CancellationToken cancellationToken = default); - - /// - /// Rolls back the changes for a given transaction - /// - /// Will throw an exception if there is not a transaction already in progress - void RollbackTransaction(); - - /// - /// Rolls back the changes for a given transaction - /// - /// Will throw an exception if there is not a transaction already in progress - /// Optional cancelation token - void RollbackTransactionAsync(CancellationToken cancellationToken = default); - - void EnlistTransaction(IDbTransaction transaction); - - IDbTransaction GetCurrentTransaction(); } -``` - -## Future Goals - - Better handling of client-provided ID's - - What is the expected behavior for AddOrUpdate on a new Client side generated ID? - - Add an attribute? - - Better handling of joining of other tables into the query - - One to One - - One to Many - - Many to One - - Many to Many - - Better transactional support -- espeically for EF Core - - Better handling of Child Objects - - Better support for Transactions - - Test to see if we're inside of a transaction? - - If the transaction is already in progress, then throw on BeginTrans - - Commit or Rollback or throw on dispose? - - Concurrency - - AddOrUpdate with a New object that uses an existing object as a relation - - If you're not using lazy loading, a simple recursive add - - If you are using lazy loading, then it's your responsibility? -- Soft Deletes -- Better support for Eager, Explicit, and Lazy Loading +``` \ No newline at end of file diff --git a/src/EFRepository.Generator.IntegrationTests/ContextTests.cs b/src/EFRepository.Generator.IntegrationTests/ContextTests.cs index 3202cf1..13e42d3 100644 --- a/src/EFRepository.Generator.IntegrationTests/ContextTests.cs +++ b/src/EFRepository.Generator.IntegrationTests/ContextTests.cs @@ -44,6 +44,18 @@ public void UserMethodsGenerated() .ShouldNotBeNull() .Id.ShouldBe(1); + usersQueryable.ByIdGreaterThan(5) + .Count().ShouldBe(5); + + usersQueryable.ByIdGreaterThanOrEqual(5) + .Count().ShouldBe(6); + + usersQueryable.ByIdLessThan(5) + .Count().ShouldBe(5); + + usersQueryable.ByIdLessThanOrEqual(5) + .Count().ShouldBe(6); + // Boolean Functions usersQueryable.ByIsDeleted(false).FirstOrDefault() .ShouldNotBeNull() @@ -67,16 +79,16 @@ public void UserMethodsGenerated() usersQueryable.ByCreatedBetween(start: now.AddHours(-5.5), end: now.AddHours(-2.5)) .Count().ShouldBe(3); - // String functions - usersQueryable.ByAddress("1 Fake St.") - .Count().ShouldBe(1); + usersQueryable.ByRegistrationDateOnDate(now.AddDays(-5)) + .Count().ShouldBe(0); + // String functions usersQueryable.ByAddress("1 Fake St.") .Count().ShouldBe(1); usersQueryable.ByAddressIsNotNull() .ByAddressStartsWith("1") - .Count().ShouldBe(10); + .Count().ShouldBe(2); usersQueryable.ByAddressIsNull() .Count().ShouldBe(1); @@ -110,7 +122,8 @@ public void UserMethodsGenerated() .ByAddressIsNotNull() .ByNameIsNotNull() .ByPhoneContains("801") - .ByPhone("201-111-0221") + .ByScoreGreaterThan(1) + .ByScoreLessThan(100) .Count().ShouldBe(0); } diff --git a/src/EFRepository.Generator.IntegrationTests/User.cs b/src/EFRepository.Generator.IntegrationTests/User.cs index 2e35427..7232753 100644 --- a/src/EFRepository.Generator.IntegrationTests/User.cs +++ b/src/EFRepository.Generator.IntegrationTests/User.cs @@ -1,28 +1,39 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace EFRepository.Generator.IntegrationTests -{ - public record User - { - public int Id { get; set; } - public string Name { get; set; } - public string? Phone { get; set; } - public string? Address { get; set; } - public DateTime Created { get; set; } - public bool IsDeleted { get; set; } - public double Score { get; set; } - - public virtual ICollection Posts { get; set; } - - public User() - { - Name = string.Empty; - Phone = string.Empty; - Address = string.Empty; - } - } -} +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace EFRepository.Generator.IntegrationTests +{ + public record User + { + public int Id { get; set; } + public string Name { get; set; } + public string? Phone { get; set; } + public string? Address { get; set; } + public DateTime Created { get; set; } + public DateTimeOffset RegistrationDate { get; set; } + public DateTimeOffset? TokenExpirationDate { get; set; } + public bool IsDeleted { get; set; } + public bool? IsModified { get; set; } + public double Score { get; set; } + public double? MaxScore { get; set; } + public Nullable MinScore { get; set; } + + public virtual ICollection Posts { get; set; } + + public User() + { + Name = string.Empty; + Phone = string.Empty; + Address = string.Empty; + + RegistrationDate = DateTimeOffset.MinValue; + + IsModified = null; + + Posts = new List(); + } + } +} diff --git a/src/EFRepository.Generator.Tests/GeneratorTests.cs b/src/EFRepository.Generator.Tests/GeneratorTests.cs index 110d275..f67de03 100644 --- a/src/EFRepository.Generator.Tests/GeneratorTests.cs +++ b/src/EFRepository.Generator.Tests/GeneratorTests.cs @@ -25,7 +25,7 @@ public void ExtensionsWereGenerated() diagnostics.IsDefaultOrEmpty.ShouldBeTrue(); var outputDiag = outputCompilation.GetDiagnostics(); - var allClasses = outputCompilation.SyntaxTrees.Where(st => st.FilePath.EndsWith(".EFExtensions.g.cs")); + var allClasses = outputCompilation.SyntaxTrees.Where(st => st.FilePath.EndsWith(".EFRepoExtensions.g.cs")); allClasses.ShouldNotBeNull("No classes were generated"); diff --git a/src/EFRepository.Generator.Tests/TestData/AllClasses.txt b/src/EFRepository.Generator.Tests/TestData/AllClasses.txt index 89c04f1..ba590e4 100644 --- a/src/EFRepository.Generator.Tests/TestData/AllClasses.txt +++ b/src/EFRepository.Generator.Tests/TestData/AllClasses.txt @@ -1,62 +1,69 @@ -using System; -using Microsoft.EntityFrameworkCore; - -namespace EFRepository.Tests.Classes; - -public class TestContext : DbContext -{ - public virtual DbSet? Orders { get; set; } - public virtual DbSet? OrderItems { get; set; } - public virtual DbSet? Payments { get; set; } - - public TestContext(DbContextOptions options) : base(options) - { } - - protected override void OnModelCreating(ModelBuilder modelBuilder) - { - base.OnModelCreating(modelBuilder); - - modelBuilder.Entity(entity => - { - - }); - - modelBuilder.Entity(entity => - { - - }); - - modelBuilder.Entity(entity => - { - - }); - } -} - -public record Order -{ - public int Id { get; set; } - public DateTime Created { get; set; } - public decimal Amount => Items.Sum(i => i.Price); - public string CurrencyCode { get; set; } - - public virtual ICollection Items { get; set; } - - public Order() - { - CurrencyCode = string.Empty; - } -} - -public record OrderItem -{ - public int Id { get; set; } - public decimal Price { get; set; } -} - -public record Payment -{ - public int Id { get; set; } - public decimal Amount { get; set; } - public DateTime Created { get; set; } +using System; +using Microsoft.EntityFrameworkCore; + +namespace EFRepository.Tests.Classes; + +public class TestContext : DbContext +{ + public virtual DbSet? Orders { get; set; } + public virtual DbSet? OrderItems { get; set; } + public virtual DbSet? Payments { get; set; } + + public TestContext(DbContextOptions options) : base(options) + { } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + + modelBuilder.Entity(entity => + { + + }); + + modelBuilder.Entity(entity => + { + + }); + + modelBuilder.Entity(entity => + { + + }); + } +} + +public record Order +{ + public int Id { get; set; } + public DateTime Created { get; set; } + public decimal Amount => Items.Sum(i => i.Price); + public string CurrencyCode { get; set; } + public DateTime? Modified { get; set; } + public Nullable RefID { get; set; } + public string? OrderOrigination { get; set; } + public DateTimeOffset RegistrationDate { get; set; } + public DateTimeOffset? TokenExpirationDate { get; set; } + + public virtual ICollection Items { get; set; } + + public Order() + { + CurrencyCode = string.Empty; + } +} + +public record OrderItem +{ + public int Id { get; set; } + public decimal Price { get; set; } + public long? CartPosition { get; set; } +} + +public record Payment +{ + public int Id { get; set; } + public decimal Amount { get; set; } + public DateTime Created { get; set; } + public Nullable PaymentProcessedDate { get; set; } } \ No newline at end of file diff --git a/src/EFRepository.Generator/ExtensionMethodGenerator.cs b/src/EFRepository.Generator/ExtensionMethodGenerator.cs index 2862317..dc03021 100644 --- a/src/EFRepository.Generator/ExtensionMethodGenerator.cs +++ b/src/EFRepository.Generator/ExtensionMethodGenerator.cs @@ -1,3 +1,4 @@ +using System.Diagnostics; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.Text; @@ -92,10 +93,11 @@ public void Execute(GeneratorExecutionContext context) protected (string FileName, string Content) BuildExtensionMethod(INamedTypeSymbol dbSetClass, Compilation compilation) { - string fileName = $"{dbSetClass.Name}.EFExtensions.g.cs"; + string fileName = $"{dbSetClass.Name}.EFRepoExtensions.g.cs"; var builder = new StringBuilder( -$@"using System; +$@"// This file has been auto generated on {DateTime.Now.ToShortDateString()} at {DateTime.Now.ToLongTimeString()} +using System; using System.Linq; using Microsoft.EntityFrameworkCore; @@ -107,7 +109,6 @@ namespace {dbSetClass.ContainingNamespace} public static partial class {dbSetClass.Name}Extensions {{ "); - var boolSymbol = compilation.GetTypeByMetadataName("System.Boolean"); var byteSymbol = compilation.GetTypeByMetadataName("System.Byte"); var shortSymbol = compilation.GetTypeByMetadataName("System.Int16"); @@ -118,6 +119,10 @@ public static partial class {dbSetClass.Name}Extensions var decimalSymbol = compilation.GetTypeByMetadataName("System.Decimal"); var stringSymbol = compilation.GetTypeByMetadataName("System.String"); var dateTimeSymbol = compilation.GetTypeByMetadataName("System.DateTime"); + var dateTimeOffsetSymbol = compilation.GetTypeByMetadataName("System.DateTimeOffset"); + + var valueTypes = new[] { boolSymbol, byteSymbol, shortSymbol, intSymbol, longSymbol, + floatSymbol, doubleSymbol, decimalSymbol, dateTimeSymbol, dateTimeOffsetSymbol }; var members = dbSetClass.GetMembers() .Where((m) => !m.IsStatic && !m.IsAbstract && !m.IsImplicitlyDeclared && m.IsDefinition @@ -126,21 +131,38 @@ public static partial class {dbSetClass.Name}Extensions foreach (var member in members) { ITypeSymbol type; + string typeString; bool nullable; if (member.Kind is SymbolKind.Property) { var property = (IPropertySymbol)member; type = property.Type; + typeString = property.Type.ToDisplayString(); nullable = property.NullableAnnotation == NullableAnnotation.Annotated; } else { var field = (IFieldSymbol)member; type = field.Type; + typeString = field.Type.ToDisplayString(); nullable = field.NullableAnnotation == NullableAnnotation.Annotated; } + if (typeString.EndsWith("?") && typeString != "string?") + { + typeString = typeString.TrimEnd('?'); + var mappedType = valueTypes.SingleOrDefault(n => n != null && n.ToDisplayString() == typeString); + + if (mappedType == null) + { + Trace.Write($"Unable to find mapping from type {typeString}? to a non-nullable type"); + continue; + } + else + type = mappedType; + } + if (type.Equals(boolSymbol, SymbolEqualityComparer.Default)) { builder.AppendLine(CreateMethod("bool", member.Name, nullable)); @@ -179,18 +201,24 @@ public static partial class {dbSetClass.Name}Extensions builder.AppendLine($@" /// - /// Filter the of {dbSetClass.Name} by {member.Name} is null + /// Filter the of {dbSetClass.Name} by {member.Name} is null or whitespace /// + /// thows if query is null public static IQueryable<{dbSetClass.Name}> By{member.Name}IsNullOrWhiteSpace(this IQueryable<{dbSetClass.Name}> query) {{ + if (query == null) throw new ArgumentNullException(nameof(query)); + return query.Where(n => string.IsNullOrWhiteSpace(n.{member.Name})); }} /// - /// Filter the of {dbSetClass.Name} by {member.Name} is not null + /// Filter the of {dbSetClass.Name} by {member.Name} is not null or whitespace /// + /// thows if query is null public static IQueryable<{dbSetClass.Name}> By{member.Name}IsNotNullOrWhiteSpace(this IQueryable<{dbSetClass.Name}> query) {{ + if (query == null) throw new ArgumentNullException(nameof(query)); + return query.Where(n => !string.IsNullOrWhiteSpace(n.{member.Name})); }}"); @@ -199,8 +227,11 @@ public static partial class {dbSetClass.Name}Extensions /// Filter the of {dbSetClass.Name} by {member.Name} /// /// The string which {member.Name} should be equal + /// thows if query is null public static IQueryable<{dbSetClass.Name}> By{member.Name}(this IQueryable<{dbSetClass.Name}> query, string? value) {{ + if (query == null) throw new ArgumentNullException(nameof(query)); + if (string.IsNullOrWhiteSpace(value)) return query; @@ -212,8 +243,11 @@ public static partial class {dbSetClass.Name}Extensions /// Filter the of {dbSetClass.Name} by {member.Name} contains a value /// /// The string which {member.Name} should contain + /// thows if query is null public static IQueryable<{dbSetClass.Name}> By{member.Name}Contains(this IQueryable<{dbSetClass.Name}> query, string? value) {{ + if (query == null) throw new ArgumentNullException(nameof(query)); + if (string.IsNullOrWhiteSpace(value)) return query; @@ -225,8 +259,11 @@ public static partial class {dbSetClass.Name}Extensions /// Filter the of {dbSetClass.Name} by {member.Name} starts with a value /// /// The string which {member.Name} should start with + /// thows if query is null public static IQueryable<{dbSetClass.Name}> By{member.Name}StartsWith(this IQueryable<{dbSetClass.Name}> query, string? value) {{ + if (query == null) throw new ArgumentNullException(nameof(query)); + if (string.IsNullOrWhiteSpace(value)) return query; @@ -239,25 +276,35 @@ public static partial class {dbSetClass.Name}Extensions /// Filter the of {dbSetClass.Name} by {member.Name} ends with a value /// /// The string which {member.Name} should end with + /// thows if query is null public static IQueryable<{dbSetClass.Name}> By{member.Name}EndsWith(this IQueryable<{dbSetClass.Name}> query, string? value) {{ + if (query == null) throw new ArgumentNullException(nameof(query)); + if (string.IsNullOrWhiteSpace(value)) return query; return query.Where(n => n.{member.Name} != null && n.{member.Name}.EndsWith(value)); }}"); } - else if (type.Equals(dateTimeSymbol, SymbolEqualityComparer.Default)) + else if (type.Equals(dateTimeSymbol, SymbolEqualityComparer.Default) || + type.Equals(dateTimeOffsetSymbol, SymbolEqualityComparer.Default)) { - builder.AppendLine(CreateMethod("DateTime", member.Name, nullable)); + string dateType = type.Equals(dateTimeSymbol, SymbolEqualityComparer.Default) ? + "DateTime" : "DateTimeOffset"; + + builder.AppendLine(CreateMethod(dateType, member.Name, nullable)); builder.AppendLine($@" /// - /// Filter the of {dbSetClass.Name} by whether or not the provided is after {member.Name} + /// Filter the of {dbSetClass.Name} by whether or not the provided is after {member.Name} /// - /// The that {member.Name} should be before - public static IQueryable<{dbSetClass}> By{member.Name}IsBefore(this IQueryable<{dbSetClass.Name}> query, DateTime? value) + /// The that {member.Name} should be before + /// thows if query is null + public static IQueryable<{dbSetClass}> By{member.Name}IsBefore(this IQueryable<{dbSetClass.Name}> query, {dateType}? value) {{ + if (query == null) throw new ArgumentNullException(nameof(query)); + if (value == null) return query; @@ -266,11 +313,14 @@ public static partial class {dbSetClass.Name}Extensions builder.AppendLine($@" /// - /// Filter the of {dbSetClass.Name} by whether or not the provided is after {member.Name} + /// Filter the of {dbSetClass.Name} by whether or not the provided is after {member.Name} /// - /// The that {member.Name} should be after - public static IQueryable<{dbSetClass}> By{member.Name}IsAfter(this IQueryable<{dbSetClass.Name}> query, DateTime? value) + /// The that {member.Name} should be after + /// thows if query is null + public static IQueryable<{dbSetClass}> By{member.Name}IsAfter(this IQueryable<{dbSetClass.Name}> query, {dateType}? value) {{ + if (query == null) throw new ArgumentNullException(nameof(query)); + if (value == null) return query; @@ -281,10 +331,13 @@ public static partial class {dbSetClass.Name}Extensions /// /// Filter the of {dbSetClass.Name} by whether or not {member.Name} is between the two provided values. /// - /// The that should be before {member.Name} - /// The that should be after {member.Name} - public static IQueryable<{dbSetClass}> By{member.Name}Between(this IQueryable<{dbSetClass.Name}> query, DateTime? start, DateTime? end) + /// The that should be before {member.Name} + /// The that should be after {member.Name} + /// thows if query is null + public static IQueryable<{dbSetClass}> By{member.Name}Between(this IQueryable<{dbSetClass.Name}> query, {dateType}? start, {dateType}? end) {{ + if (query == null) throw new ArgumentNullException(nameof(query)); + if (start != null) query = query.Where(n => n.{member.Name} > start); @@ -298,9 +351,12 @@ public static partial class {dbSetClass.Name}Extensions /// /// Filter the of {dbSetClass.Name} by whether or not {member.Name} is between the two provided values. /// - /// The that should the same date as {member.Name}, excluding time - public static IQueryable<{dbSetClass}> By{member.Name}OnDate(this IQueryable<{dbSetClass.Name}> query, DateTime? value) + /// The that should the same date as {member.Name}, excluding time + /// thows if query is null + public static IQueryable<{dbSetClass}> By{member.Name}OnDate(this IQueryable<{dbSetClass.Name}> query, {dateType}? value) {{ + if (query == null) throw new ArgumentNullException(nameof(query)); + if (value != null) return query.Where(n => n.{member.Name}.Date == value.Value.Date); else @@ -309,7 +365,7 @@ public static partial class {dbSetClass.Name}Extensions } } - // Close Class + // Close Class & Namespace builder.AppendLine(" }") .AppendLine("}"); @@ -317,25 +373,94 @@ public static partial class {dbSetClass.Name}Extensions string CreateMethod(string type, string memberName, bool nullable) { - string result = $@" + StringBuilder result = new(); + result.AppendLine($@" /// /// Filter the of {dbSetClass.Name} by {memberName} /// /// The {type} which should equal {memberName} + /// thows if query is null public static IQueryable<{dbSetClass.Name}> By{memberName}(this IQueryable<{dbSetClass.Name}> query, {type}? value) {{ + if (query == null) throw new ArgumentNullException(nameof(query)); + if (value == null) return query; return query.Where(n => n.{memberName} == value); - }}"; + }}"); + + // This is for numeric types only + if (!type.Contains("bool") && !type.Contains("DateTime") && !type.Contains("string")) + { + result.AppendLine($@" + /// + /// Filter the of {dbSetClass.Name} by {memberName} greater than value + /// + /// The {type} which should be greater than {memberName} + /// thows if query is null + public static IQueryable<{dbSetClass.Name}> By{memberName}GreaterThan(this IQueryable<{dbSetClass.Name}> query, {type}? value) + {{ + if (query == null) throw new ArgumentNullException(nameof(query)); + + if (value == null) + return query; + + return query.Where(n => n.{memberName} > value); + }} + + /// + /// Filter the of {dbSetClass.Name} by {memberName} greater than or equal value + /// + /// The {type} which should be greater than {memberName} + /// thows if query is null + public static IQueryable<{dbSetClass.Name}> By{memberName}GreaterThanOrEqual(this IQueryable<{dbSetClass.Name}> query, {type}? value) + {{ + if (query == null) throw new ArgumentNullException(nameof(query)); + + if (value == null) + return query; + + return query.Where(n => n.{memberName} >= value); + }} + + /// + /// Filter the of {dbSetClass.Name} by {memberName} less than value + /// + /// The {type} which should be less than {memberName} + /// thows if query is null + public static IQueryable<{dbSetClass.Name}> By{memberName}LessThan(this IQueryable<{dbSetClass.Name}> query, {type}? value) + {{ + if (query == null) throw new ArgumentNullException(nameof(query)); + + if (value == null) + return query; + + return query.Where(n => n.{memberName} < value); + }} + + /// + /// Filter the of {dbSetClass.Name} by {memberName} less than or qual to value + /// + /// The {type} which should be less than or equal to {memberName} + /// thows if query is null + public static IQueryable<{dbSetClass.Name}> By{memberName}LessThanOrEqual(this IQueryable<{dbSetClass.Name}> query, {type}? value) + {{ + if (query == null) throw new ArgumentNullException(nameof(query)); + + if (value == null) + return query; + + return query.Where(n => n.{memberName} <= value); + }}"); + } if (nullable) { - result += CreateNullMethodFunctions(memberName); + result.AppendLine(CreateNullMethodFunctions(memberName)); } - return result; + return result.ToString(); } string CreateNullMethodFunctions(string memberName) @@ -344,16 +469,22 @@ string CreateNullMethodFunctions(string memberName) /// /// Filter the of {dbSetClass.Name} by {memberName} is null /// + /// thows if query is null public static IQueryable<{dbSetClass.Name}> By{memberName}IsNull(this IQueryable<{dbSetClass.Name}> query) {{ + if (query == null) throw new ArgumentNullException(nameof(query)); + return query.Where(n => n.{memberName} == null); }} /// /// Filter the of {dbSetClass.Name} by {memberName} is not null /// + /// thows if query is null public static IQueryable<{dbSetClass.Name}> By{memberName}IsNotNull(this IQueryable<{dbSetClass.Name}> query) {{ + if (query == null) throw new ArgumentNullException(nameof(query)); + return query.Where(n => n.{memberName} != null); }}"; } diff --git a/src/EFRepository.sln b/src/EFRepository.sln index 3a603d3..6fe16ce 100644 --- a/src/EFRepository.sln +++ b/src/EFRepository.sln @@ -15,6 +15,12 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EFRepository.Generator.Test EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EFRepository", "EFRepository\EFRepository.csproj", "{5B80FD35-149B-4DA0-A4AE-51E47E38F9E6}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Files", "Solution Files", "{DDA38264-799F-4E75-8E30-41BCAB9B9878}" + ProjectSection(SolutionItems) = preProject + ..\FutureGoals.md = ..\FutureGoals.md + ..\README.md = ..\README.md + EndProjectSection +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU diff --git a/src/EFRepository/IRepository.cs b/src/EFRepository/IRepository.cs index 0069b15..5207163 100644 --- a/src/EFRepository/IRepository.cs +++ b/src/EFRepository/IRepository.cs @@ -8,12 +8,10 @@ namespace EFRepository { /// - /// Interface for interacting with data storage through the repository pattern + /// Interface for interacting with data storage through a queryable repository pattern /// - /// public interface IRepository : IDisposable { - /// Event that fires when an item is added event Action ItemAdding; @@ -23,7 +21,6 @@ public interface IRepository : IDisposable /// Event that fires when an item is deleted event Action ItemDeleting; - /// Queriable Entity IQueryable Query() where TEntity : class, new(); @@ -87,16 +84,10 @@ public interface IRepository : IDisposable /// Number of affected entities int Save(); - /// - /// Save pending changes for the collection async - /// - /// Number of affected entities - Task SaveAsync(); - /// /// Save pending changes for the collection async with cancellation /// - /// Cancelation Token + /// Cancellation Token /// Number of affected entities Task SaveAsync(CancellationToken cancellationToken = default); } diff --git a/src/EFRepository/Repository.cs b/src/EFRepository/Repository.cs index 670edf1..5972743 100644 --- a/src/EFRepository/Repository.cs +++ b/src/EFRepository/Repository.cs @@ -119,14 +119,7 @@ public virtual int Save() return DataContext.SaveChanges(); } - public virtual Task SaveAsync() - { - CheckDetectChanges(); - - return DataContext.SaveChangesAsync(); - } - - public virtual Task SaveAsync(CancellationToken cancellationToken) + public virtual Task SaveAsync(CancellationToken cancellationToken = default) { CheckDetectChanges(); diff --git a/src/UnitTests/App.config b/src/UnitTests/App.config index db5ac23..217ee9d 100644 --- a/src/UnitTests/App.config +++ b/src/UnitTests/App.config @@ -12,16 +12,13 @@ - + - - + + diff --git a/src/UnitTests/UnitTests.csproj b/src/UnitTests/UnitTests.csproj index 8e6052a..97e9fbd 100644 --- a/src/UnitTests/UnitTests.csproj +++ b/src/UnitTests/UnitTests.csproj @@ -9,8 +9,8 @@ Properties UnitTests UnitTests - v4.5.2 - win + v4.7.2 + win 512 {3AC096D0-A1C2-E12C-1390-A8335801FDAB};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} 15.0 From cda049a1bfd5d2e8a31bdc868b1012d6eb74aba7 Mon Sep 17 00:00:00 2001 From: nzaugg Date: Sun, 16 Jan 2022 15:57:05 -0700 Subject: [PATCH 14/17] Update README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 9f8e429..17e490d 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ var query = Repo.Query() * C# 10 `nullable` compatible code is generated * Automatic Update / Insert detection with `.AddOrUpdate` * Force Adding of New Object with `.AddNew(object)` - * FineOne to find objects by key. Will even work with composite keys + * FindOne to find objects by key. Will even work with composite keys * Events for objects being added or modified or deleted * `async` and synchronous methods provided where possible. Chaining to LINQ's async methods is strongly advised (like `.CountAsync()`). @@ -248,4 +248,4 @@ public interface IRepository : IDisposable /// Number of affected entities Task SaveAsync(CancellationToken cancellationToken = default); } -``` \ No newline at end of file +``` From 3b21c7046a2a65a2daf70104c480972c2bea21d7 Mon Sep 17 00:00:00 2001 From: Nate Zaugg Date: Sun, 19 Jun 2022 14:05:13 -0600 Subject: [PATCH 15/17] Fixes for dates --- .../GeneratorTests.cs | 109 +++++++++--------- .../TestData/AllClasses.txt | 5 + .../ExtensionMethodGenerator.cs | 33 +++++- src/EFRepository.sln | 12 +- src/EFRepository/EFRepository.csproj | 15 ++- 5 files changed, 107 insertions(+), 67 deletions(-) diff --git a/src/EFRepository.Generator.Tests/GeneratorTests.cs b/src/EFRepository.Generator.Tests/GeneratorTests.cs index f67de03..e99faf7 100644 --- a/src/EFRepository.Generator.Tests/GeneratorTests.cs +++ b/src/EFRepository.Generator.Tests/GeneratorTests.cs @@ -1,52 +1,57 @@ -using System; -using System.Reflection; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp; -using Microsoft.EntityFrameworkCore; -using Shouldly; -using Xunit; - -namespace EFRepository.Generator.Tests -{ - public class GeneratorTests - { - [Fact] - public void ExtensionsWereGenerated() - { - string? file = File.ReadAllText(@"./TestData/AllClasses.txt"); - var compilation = CreateCompilation(file); - var generator = new ExtensionMethodGenerator(); - var driver = CSharpGeneratorDriver.Create(generator); - - var name = typeof(DbSet<>); - - driver.RunGeneratorsAndUpdateCompilation(compilation, out var outputCompilation, out var diagnostics); - - diagnostics.IsDefaultOrEmpty.ShouldBeTrue(); - var outputDiag = outputCompilation.GetDiagnostics(); - - var allClasses = outputCompilation.SyntaxTrees.Where(st => st.FilePath.EndsWith(".EFRepoExtensions.g.cs")); - - allClasses.ShouldNotBeNull("No classes were generated"); - - allClasses.Count().ShouldBe(3, "Not all classes were generated."); - - - } - - protected static Compilation CreateCompilation(string source) - => CSharpCompilation.Create("compilation", - new[] { CSharpSyntaxTree.ParseText(source) }, - new[] - { - MetadataReference.CreateFromFile(typeof(DateTime).GetTypeInfo().Assembly.Location), - MetadataReference.CreateFromFile(typeof(Binder).GetTypeInfo().Assembly.Location), - MetadataReference.CreateFromFile(typeof(Attribute).GetTypeInfo().Assembly.Location), - MetadataReference.CreateFromFile(typeof(System.ComponentModel.INotifyPropertyChanged).GetTypeInfo().Assembly.Location), - MetadataReference.CreateFromFile(typeof(MulticastDelegate).GetTypeInfo().Assembly.Location), - MetadataReference.CreateFromFile(typeof(DbContext).GetTypeInfo().Assembly.Location), - MetadataReference.CreateFromFile(typeof(DbSet<>).GetTypeInfo().Assembly.Location), - }, - new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)); - } -} +using System; +using System.Reflection; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.EntityFrameworkCore; +using Shouldly; +using Xunit; + +namespace EFRepository.Generator.Tests +{ + public class GeneratorTests + { + [Fact] + public void ExtensionsWereGenerated() + { + string? file = File.ReadAllText(@"./TestData/AllClasses.txt"); + var compilation = CreateCompilation(file); + var generator = new ExtensionMethodGenerator(); + var driver = CSharpGeneratorDriver.Create(generator); + + var name = typeof(DbSet<>); + + driver.RunGeneratorsAndUpdateCompilation(compilation, out var outputCompilation, out var diagnostics); + + diagnostics.IsDefaultOrEmpty.ShouldBeTrue(); + var outputDiag = outputCompilation.GetDiagnostics(); + + var allClasses = outputCompilation.SyntaxTrees.Where(st => st.FilePath.EndsWith(".EFRepoExtensions.g.cs")); + + allClasses.ShouldNotBeNull("No classes were generated"); + + allClasses.Count().ShouldBe(3, "Not all classes were generated."); + + + } + + protected static Compilation CreateCompilation(string source) + { + return CSharpCompilation.Create("compilation", + new[] { CSharpSyntaxTree.ParseText(source) }, + new[] { + MetadataReference.CreateFromFile(typeof(object).GetTypeInfo().Assembly.Location), + MetadataReference.CreateFromFile(typeof(DateTime).GetTypeInfo().Assembly.Location), + MetadataReference.CreateFromFile(typeof(DateTimeOffset).GetTypeInfo().Assembly.Location), + MetadataReference.CreateFromFile(typeof(DateOnly).GetTypeInfo().Assembly.Location), + MetadataReference.CreateFromFile(typeof(TimeOnly).GetTypeInfo().Assembly.Location), + MetadataReference.CreateFromFile(typeof(Binder).GetTypeInfo().Assembly.Location), + MetadataReference.CreateFromFile(typeof(Attribute).GetTypeInfo().Assembly.Location), + MetadataReference.CreateFromFile(typeof(System.ComponentModel.INotifyPropertyChanged).GetTypeInfo().Assembly.Location), + MetadataReference.CreateFromFile(typeof(MulticastDelegate).GetTypeInfo().Assembly.Location), + MetadataReference.CreateFromFile(typeof(DbContext).GetTypeInfo().Assembly.Location), + MetadataReference.CreateFromFile(typeof(DbSet<>).GetTypeInfo().Assembly.Location), + }, + new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)); + } + } +} diff --git a/src/EFRepository.Generator.Tests/TestData/AllClasses.txt b/src/EFRepository.Generator.Tests/TestData/AllClasses.txt index ba590e4..048746a 100644 --- a/src/EFRepository.Generator.Tests/TestData/AllClasses.txt +++ b/src/EFRepository.Generator.Tests/TestData/AllClasses.txt @@ -44,6 +44,11 @@ public record Order public string? OrderOrigination { get; set; } public DateTimeOffset RegistrationDate { get; set; } public DateTimeOffset? TokenExpirationDate { get; set; } + public DateOnly ActiveDate { get; set; } + public DateOnly? ExpiryDate { get; set; } + public TimeOnly ValidTime { get; set; } + public TimeOnly? ExpiryTime { get; set; } + public virtual ICollection Items { get; set; } diff --git a/src/EFRepository.Generator/ExtensionMethodGenerator.cs b/src/EFRepository.Generator/ExtensionMethodGenerator.cs index dc03021..7331b39 100644 --- a/src/EFRepository.Generator/ExtensionMethodGenerator.cs +++ b/src/EFRepository.Generator/ExtensionMethodGenerator.cs @@ -120,9 +120,12 @@ public static partial class {dbSetClass.Name}Extensions var stringSymbol = compilation.GetTypeByMetadataName("System.String"); var dateTimeSymbol = compilation.GetTypeByMetadataName("System.DateTime"); var dateTimeOffsetSymbol = compilation.GetTypeByMetadataName("System.DateTimeOffset"); + var dateOnlySymbol = compilation.GetTypeByMetadataName("System.DateOnly"); + var timeOnlySymbol = compilation.GetTypeByMetadataName("System.TimeOnly"); var valueTypes = new[] { boolSymbol, byteSymbol, shortSymbol, intSymbol, longSymbol, - floatSymbol, doubleSymbol, decimalSymbol, dateTimeSymbol, dateTimeOffsetSymbol }; + floatSymbol, doubleSymbol, decimalSymbol, dateTimeSymbol, dateTimeOffsetSymbol, + dateOnlySymbol, timeOnlySymbol }; var members = dbSetClass.GetMembers() .Where((m) => !m.IsStatic && !m.IsAbstract && !m.IsImplicitlyDeclared && m.IsDefinition @@ -288,13 +291,27 @@ public static partial class {dbSetClass.Name}Extensions }}"); } else if (type.Equals(dateTimeSymbol, SymbolEqualityComparer.Default) || - type.Equals(dateTimeOffsetSymbol, SymbolEqualityComparer.Default)) + type.Equals(dateTimeOffsetSymbol, SymbolEqualityComparer.Default) || + type.Equals(dateOnlySymbol, SymbolEqualityComparer.Default) || + type.Equals(timeOnlySymbol, SymbolEqualityComparer.Default)) { - string dateType = type.Equals(dateTimeSymbol, SymbolEqualityComparer.Default) ? - "DateTime" : "DateTimeOffset"; + string dateType; + + if (type.Equals(dateTimeSymbol, SymbolEqualityComparer.Default)) + dateType = "DateTime"; + else if (type.Equals(dateTimeOffsetSymbol, SymbolEqualityComparer.Default)) + dateType = "DateTimeOffset"; + else if (type.Equals(dateOnlySymbol, SymbolEqualityComparer.Default)) + dateType = "DateOnly"; + else if (type.Equals(timeOnlySymbol, SymbolEqualityComparer.Default)) + dateType = "TimeOnly"; + else + continue; + // By (is) builder.AppendLine(CreateMethod(dateType, member.Name, nullable)); + // IsBefore builder.AppendLine($@" /// /// Filter the of {dbSetClass.Name} by whether or not the provided is after {member.Name} @@ -311,6 +328,7 @@ public static partial class {dbSetClass.Name}Extensions return query.Where(n => n.{member.Name} < value); }}"); + // IsAfter builder.AppendLine($@" /// /// Filter the of {dbSetClass.Name} by whether or not the provided is after {member.Name} @@ -327,6 +345,7 @@ public static partial class {dbSetClass.Name}Extensions return query.Where(n => n.{member.Name} > value); }}"); + // Between builder.AppendLine($@" /// /// Filter the of {dbSetClass.Name} by whether or not {member.Name} is between the two provided values. @@ -347,7 +366,9 @@ public static partial class {dbSetClass.Name}Extensions return query; }}"); - builder.AppendLine($@" + // OnDate (Skip for DateOnly and TimeOnly types) + if (dateType != "DateOnly" && dateType != "TimeOnly") + builder.AppendLine($@" /// /// Filter the of {dbSetClass.Name} by whether or not {member.Name} is between the two provided values. /// @@ -358,7 +379,7 @@ public static partial class {dbSetClass.Name}Extensions if (query == null) throw new ArgumentNullException(nameof(query)); if (value != null) - return query.Where(n => n.{member.Name}.Date == value.Value.Date); + return query.Where(n => n.{member.Name}{(nullable ? ".GetValueOrDefault()" : "")}.Date == value.Value.Date); else return query; }}"); diff --git a/src/EFRepository.sln b/src/EFRepository.sln index 6fe16ce..73cc4f1 100644 --- a/src/EFRepository.sln +++ b/src/EFRepository.sln @@ -3,8 +3,6 @@ Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 17 VisualStudioVersion = 17.0.31912.275 MinimumVisualStudioVersion = 10.0.40219.1 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UnitTests", "UnitTests\UnitTests.csproj", "{0C6142A2-DC38-4E93-95D3-1B2C78F7FA2A}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EFRepository.Generator", "EFRepository.Generator\EFRepository.Generator.csproj", "{C8968136-BD14-4CE0-B603-46375C1316C8}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Generator", "Generator", "{9D3C7B8A-ABCA-470F-BE2B-8755A86D71F0}" @@ -21,16 +19,14 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Files", "Solution ..\README.md = ..\README.md EndProjectSection EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "UnitTestsCore", "UnitTestsCore\UnitTestsCore.csproj", "{71B3CA71-E549-41B0-91C0-E71DA3FA2F6C}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU Release|Any CPU = Release|Any CPU EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution - {0C6142A2-DC38-4E93-95D3-1B2C78F7FA2A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {0C6142A2-DC38-4E93-95D3-1B2C78F7FA2A}.Debug|Any CPU.Build.0 = Debug|Any CPU - {0C6142A2-DC38-4E93-95D3-1B2C78F7FA2A}.Release|Any CPU.ActiveCfg = Release|Any CPU - {0C6142A2-DC38-4E93-95D3-1B2C78F7FA2A}.Release|Any CPU.Build.0 = Release|Any CPU {C8968136-BD14-4CE0-B603-46375C1316C8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {C8968136-BD14-4CE0-B603-46375C1316C8}.Debug|Any CPU.Build.0 = Debug|Any CPU {C8968136-BD14-4CE0-B603-46375C1316C8}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -47,6 +43,10 @@ Global {5B80FD35-149B-4DA0-A4AE-51E47E38F9E6}.Debug|Any CPU.Build.0 = Debug|Any CPU {5B80FD35-149B-4DA0-A4AE-51E47E38F9E6}.Release|Any CPU.ActiveCfg = Release|Any CPU {5B80FD35-149B-4DA0-A4AE-51E47E38F9E6}.Release|Any CPU.Build.0 = Release|Any CPU + {71B3CA71-E549-41B0-91C0-E71DA3FA2F6C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {71B3CA71-E549-41B0-91C0-E71DA3FA2F6C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {71B3CA71-E549-41B0-91C0-E71DA3FA2F6C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {71B3CA71-E549-41B0-91C0-E71DA3FA2F6C}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/src/EFRepository/EFRepository.csproj b/src/EFRepository/EFRepository.csproj index be609f2..15bac71 100644 --- a/src/EFRepository/EFRepository.csproj +++ b/src/EFRepository/EFRepository.csproj @@ -7,11 +7,11 @@ Mindfire.EFRepository - 2.0.0 + 2.3.5 Mindfire Technology Mindfire EFRepository EFRepository allows you to use a LINQ-Enabled version of the Repository Pattern for Entity Framework - 2021 + 2022 https://github.com/MindfireTechnology/EFRepository https://github.com/MindfireTechnology/EFRepository EF, EntityFramework, Entity Framework, Core, NetStandard, Repository Pattern @@ -20,11 +20,12 @@ Updated to include a source generator that creates helper methods for filtering True Nate Zaugg, Dan Beus MIT - True + False Logo.png true snupkg true + README.md @@ -97,6 +98,14 @@ Updated to include a source generator that creates helper methods for filtering + + + + + + True + \ + True \ From da3b7a9d53856920b78afa9adc56b2da91888788 Mon Sep 17 00:00:00 2001 From: Nate Zaugg Date: Thu, 20 Feb 2025 22:33:54 -0700 Subject: [PATCH 16/17] Adding support for newer frameworks --- .../EFRepository.Generator.csproj | 10 +++---- src/EFRepository/EFRepository.csproj | 29 +++++++++++++++++-- src/UnitTestsCore/UnitTestsCore.csproj | 2 +- 3 files changed, 31 insertions(+), 10 deletions(-) diff --git a/src/EFRepository.Generator/EFRepository.Generator.csproj b/src/EFRepository.Generator/EFRepository.Generator.csproj index b725600..6d25459 100644 --- a/src/EFRepository.Generator/EFRepository.Generator.csproj +++ b/src/EFRepository.Generator/EFRepository.Generator.csproj @@ -31,12 +31,6 @@ Logo.png - - - - - - all @@ -53,6 +47,10 @@ + + + + diff --git a/src/EFRepository/EFRepository.csproj b/src/EFRepository/EFRepository.csproj index 15bac71..e30afda 100644 --- a/src/EFRepository/EFRepository.csproj +++ b/src/EFRepository/EFRepository.csproj @@ -1,13 +1,13 @@ - net6.0;net5.0;netstandard2.0;netstandard2.1;netcoreapp3.0;net452 + net9.0;net8.0;net7.0;net6.0;net5.0;netstandard2.0;netstandard2.1;netcoreapp3.0;net452 10.0 Mindfire.EFRepository - 2.3.5 + 2.5.0 Mindfire Technology Mindfire EFRepository EFRepository allows you to use a LINQ-Enabled version of the Repository Pattern for Entity Framework @@ -82,6 +82,27 @@ Updated to include a source generator that creates helper methods for filtering + + + 7.0.0 + + + + + + + 8.0.0 + + + + + + + 9.0.0 + + + + @@ -94,7 +115,7 @@ Updated to include a source generator that creates helper methods for filtering - + @@ -115,6 +136,8 @@ Updated to include a source generator that creates helper methods for filtering + +