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 d8fb5c4..17e490d 100644 --- a/README.md +++ b/README.md @@ -1,100 +1,251 @@ # 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)` + * 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()`). + + 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 a queryable 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 + /// Find an entity based on key(s) /// - /// - 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 - - 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 + /// 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 with cancellation + /// + /// Cancellation Token + /// Number of affected entities + Task SaveAsync(CancellationToken cancellationToken = default); +} +``` diff --git a/Resources/Logo.png b/Resources/Logo.png new file mode 100644 index 0000000..09ff714 Binary files /dev/null and b/Resources/Logo.png differ 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/releases/Mindfire.EFRepository.1.0.0-alpha.nupkg b/releases/Mindfire.EFRepository.1.0.0-alpha.nupkg deleted file mode 100644 index b971675..0000000 Binary files a/releases/Mindfire.EFRepository.1.0.0-alpha.nupkg and /dev/null differ diff --git a/releases/Mindfire.EFRepository.1.0.1.nupkg b/releases/Mindfire.EFRepository.1.0.1.nupkg deleted file mode 100644 index 5183607..0000000 Binary files a/releases/Mindfire.EFRepository.1.0.1.nupkg and /dev/null differ diff --git a/releases/Mindfire.EFRepository.2.0.0-alpha.symbols/Mindfire.EFRepository.nuspec b/releases/Mindfire.EFRepository.2.0.0-alpha.symbols/Mindfire.EFRepository.nuspec deleted file mode 100644 index e8bb27f..0000000 --- a/releases/Mindfire.EFRepository.2.0.0-alpha.symbols/Mindfire.EFRepository.nuspec +++ /dev/null @@ -1,33 +0,0 @@ - - - - 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 ac61513..7faee3d 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,17 +99,17 @@ 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_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_methods = false:warning +csharp_style_expression_bodied_constructors = false: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 @@ -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-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-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-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-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.IntegrationTests/ContextTests.cs b/src/EFRepository.Generator.IntegrationTests/ContextTests.cs new file mode 100644 index 0000000..13e42d3 --- /dev/null +++ b/src/EFRepository.Generator.IntegrationTests/ContextTests.cs @@ -0,0 +1,131 @@ +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}") + }).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(); + + // Int/Long Functions + usersQueryable.ById(1).FirstOrDefault() + .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() + .IsDeleted.ShouldBeFalse(); + + // Double Functions + usersQueryable.ByScore(3.333).FirstOrDefault() + .ShouldNotBeNull() + .Score.ShouldBe(3.333); + + // DateTime Functions + usersQueryable.ByCreatedIsAfter(now.AddHours(-5.5)) + .Count().ShouldBe(5); + + usersQueryable.ByCreatedOnDate(now.AddDays(-5)) + .Count().ShouldBe(1); + + usersQueryable.ByCreatedIsBefore(now.AddHours(-5.5)) + .Count().ShouldBe(6); + + usersQueryable.ByCreatedBetween(start: now.AddHours(-5.5), end: now.AddHours(-2.5)) + .Count().ShouldBe(3); + + usersQueryable.ByRegistrationDateOnDate(now.AddDays(-5)) + .Count().ShouldBe(0); + + // String functions + usersQueryable.ByAddress("1 Fake St.") + .Count().ShouldBe(1); + + usersQueryable.ByAddressIsNotNull() + .ByAddressStartsWith("1") + .Count().ShouldBe(2); + + usersQueryable.ByAddressIsNull() + .Count().ShouldBe(1); + + usersQueryable.ByNameIsNull() + .Count().ShouldBe(0); + + usersQueryable.ByNameIsNullOrWhiteSpace() + .Count().ShouldBe(1); + + usersQueryable.ByPhoneIsNullOrWhiteSpace() + .Count().ShouldBe(1); + + usersQueryable.ByAddressIsNullOrWhiteSpace() + .Count().ShouldBe(1); + + usersQueryable.ByAddressIsNotNullOrWhiteSpace() + .Count().ShouldBe(10); + + usersQueryable.ByAddressStartsWith("1") + .Count().ShouldBe(2); + + usersQueryable.ByAddressEndsWith("St.") + .Count().ShouldBe(10); + + usersQueryable.ByAddressContains("Fake") + .Count().ShouldBe(10); + + // Testing chained functions + usersQueryable.ByAddress("1 Fake St.") + .ByAddressIsNotNull() + .ByNameIsNotNull() + .ByPhoneContains("801") + .ByScoreGreaterThan(1) + .ByScoreLessThan(100) + .Count().ShouldBe(0); + + } + } +} \ 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..59a9c80 --- /dev/null +++ b/src/EFRepository.Generator.IntegrationTests/Post.cs @@ -0,0 +1,22 @@ +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..7c36aa7 --- /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..7232753 --- /dev/null +++ b/src/EFRepository.Generator.IntegrationTests/User.cs @@ -0,0 +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 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/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..e99faf7 --- /dev/null +++ b/src/EFRepository.Generator.Tests/GeneratorTests.cs @@ -0,0 +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) + { + 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 new file mode 100644 index 0000000..048746a --- /dev/null +++ b/src/EFRepository.Generator.Tests/TestData/AllClasses.txt @@ -0,0 +1,74 @@ +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 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; } + + 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/EFRepository.Generator.csproj b/src/EFRepository.Generator/EFRepository.Generator.csproj new file mode 100644 index 0000000..6d25459 --- /dev/null +++ b/src/EFRepository.Generator/EFRepository.Generator.csproj @@ -0,0 +1,61 @@ + + + + netstandard2.0 + enable + enable + 10.0 + + false + PackageReference + + 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..7331b39 --- /dev/null +++ b/src/EFRepository.Generator/ExtensionMethodGenerator.cs @@ -0,0 +1,527 @@ +using System.Diagnostics; +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(sourceModel, sourceSymbol, 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) + { + if (record != null) + { + 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]; + + if (genericType == null) + continue; + + var sourceCode = BuildExtensionMethod((INamedTypeSymbol)genericType, compilation); + + yield return sourceCode; + } + } + } + + protected (string FileName, string Content) BuildExtensionMethod(INamedTypeSymbol dbSetClass, Compilation compilation) + { + string fileName = $"{dbSetClass.Name}.EFRepoExtensions.g.cs"; + + var builder = new StringBuilder( +$@"// This file has been auto generated on {DateTime.Now.ToShortDateString()} at {DateTime.Now.ToLongTimeString()} +using System; +using System.Linq; +using Microsoft.EntityFrameworkCore; + +#nullable enable + +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 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, + dateOnlySymbol, timeOnlySymbol }; + + 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; + 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)); + } + 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} 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 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})); + }}"); + + builder.AppendLine($@" + /// + /// 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; + + return query.Where(n => n.{member.Name} == value); + }}"); + + builder.AppendLine($@" + /// + /// 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; + + return query.Where(n => n.{member.Name} != null && n.{member.Name}.Contains(value)); + }}"); + + builder.AppendLine($@" + /// + /// 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; + + return query.Where(n => n.{member.Name} != null && n.{member.Name}.StartsWith(value)); + }}"); + + builder.AppendLine($@" + + /// + /// 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) || + type.Equals(dateTimeOffsetSymbol, SymbolEqualityComparer.Default) || + type.Equals(dateOnlySymbol, SymbolEqualityComparer.Default) || + type.Equals(timeOnlySymbol, SymbolEqualityComparer.Default)) + { + 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} + /// + /// 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; + + 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} + /// + /// 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; + + 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. + /// + /// 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); + + if (end != null) + query = query.Where(n => n.{member.Name} < end); + + return query; + }}"); + + // 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. + /// + /// 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}{(nullable ? ".GetValueOrDefault()" : "")}.Date == value.Value.Date); + else + return query; + }}"); + } + } + + // Close Class & Namespace + builder.AppendLine(" }") + .AppendLine("}"); + + return (fileName, builder.ToString()); + + string CreateMethod(string type, string memberName, bool nullable) + { + 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.AppendLine(CreateNullMethodFunctions(memberName)); + } + + return result.ToString(); + } + + string CreateNullMethodFunctions(string memberName) + { + return $@" + /// + /// 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); + }}"; + } + } +} + +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; + } +} 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..8019b73 --- /dev/null +++ b/src/EFRepository.Generator/tools/install.ps1 @@ -0,0 +1,49 @@ +param($installPath, $toolsPath, $package, $project) + +$analyzersPaths = Join-Path (Join-Path (Split-Path -Path $toolsPath -Parent) "analyzers" ) * -Resolve + +foreach($analyzersPath in $analyzersPaths) +{ + # 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 new file mode 100644 index 0000000..d49eb31 --- /dev/null +++ b/src/EFRepository.Generator/tools/uninstall.ps1 @@ -0,0 +1,56 @@ +param($installPath, $toolsPath, $package, $project) + +$analyzersPaths = Join-Path (Join-Path (Split-Path -Path $toolsPath -Parent) "analyzers" ) * -Resolve + +foreach($analyzersPath in $analyzersPaths) +{ + # 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.sln b/src/EFRepository.sln index 1c6292b..73cc4f1 100644 --- a/src/EFRepository.sln +++ b/src/EFRepository.sln @@ -1,24 +1,25 @@  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}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EFRepository.Generator", "EFRepository.Generator\EFRepository.Generator.csproj", "{C8968136-BD14-4CE0-B603-46375C1316C8}" 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("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Generator", "Generator", "{9D3C7B8A-ABCA-470F-BE2B-8755A86D71F0}" 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.IntegrationTests", "EFRepository.Generator.IntegrationTests\EFRepository.Generator.IntegrationTests.csproj", "{FD1A33DE-FDA2-491D-A66F-9A247D810DA0}" EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{FF0B9269-0D2A-40E9-9143-EDE411AA80E4}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EFRepository.Generator.Tests", "EFRepository.Generator.Tests\EFRepository.Generator.Tests.csproj", "{C6A4D0BC-6D44-47FA-BE67-496FE156F783}" +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 -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "UnitTestsCore", "UnitTestsCore\UnitTestsCore.csproj", "{3BEE89EA-5183-4A5E-A0CB-54D3A0708243}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EFRepository", "EFRepository\EFRepository.csproj", "{9D130311-BF55-4C25-8411-3E5117B30AAA}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UnitTests", "UnitTests\UnitTests.csproj", "{0C6142A2-DC38-4E93-95D3-1B2C78F7FA2A}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "UnitTestsCore", "UnitTestsCore\UnitTestsCore.csproj", "{71B3CA71-E549-41B0-91C0-E71DA3FA2F6C}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -26,34 +27,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 + {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 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..ce37dcb 100644 --- a/src/EFRepository/EFRepository.csproj +++ b/src/EFRepository/EFRepository.csproj @@ -1,81 +1,156 @@ - - - - - - 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 + + + + net10.0;net9.0;net8.0;net7.0;net6.0;net5.0;netstandard2.0;netstandard2.1;netcoreapp3.0;net452 + 10.0 + + + + Mindfire.EFRepository + 2.5.5 + Mindfire Technology + Mindfire EFRepository + EFRepository allows you to use a LINQ-Enabled version of the Repository Pattern for Entity Framework + 2025 + https://github.com/MindfireTechnology/EFRepository + https://github.com/MindfireTechnology/EFRepository + EF, EntityFramework, Entity Framework, Core, NetStandard, Repository Pattern + 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 + False + Logo.png + true + snupkg + true + README.md + + + + 5 + + + + 5 + + + + + 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 + + + + + + + 7.0.0 + + + + + + + 8.0.0 + + + + + + + 9.0.0 + + + + + + + 10.0.0 + + + + + + + + + + + + + + + + + + + + + + + + + + + True + \ + + + True + \ + + + + + + + + + + + diff --git a/src/EFRepository/IRepository.cs b/src/EFRepository/IRepository.cs index f3c11b2..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 + public interface IRepository : IDisposable { - /// Event that fires when an item is added event Action ItemAdding; @@ -23,17 +21,9 @@ public interface IRepository : IDisposable /// Event that fires when an item is deleted event Action ItemDeleting; - /// Queriable Entity IQueryable Query() where TEntity : class, new(); - /// - /// Join another entity - /// - /// - /// - IQueryable Join() where TEntity : class, new(); - /// /// Find an entity based on key(s) /// @@ -94,75 +84,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(); } } 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..c5e71d6 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 using System.Data.Entity.Infrastructure; using System.Data.Entity; #else @@ -39,12 +39,6 @@ public Repository(DbContext context, bool ownsDataContext = true) return DataContext.Set(); } - public virtual IQueryable Join() where TEntity : class, new() - { - throw new NotImplementedException(); - } - - public virtual TEntity FindOne(params object[] keys) where TEntity : class, new() { return DataContext.Set().Find(keys); @@ -87,7 +81,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); } @@ -119,20 +113,13 @@ public Repository(DbContext context, bool ownsDataContext = true) } public virtual int Save() - { - CheckDetectChanges(); - - return DataContext.SaveChanges(); - } - - public virtual Task SaveAsync() { CheckDetectChanges(); - return DataContext.SaveChangesAsync(); + return DataContext.SaveChanges(); } - public virtual Task SaveAsync(CancellationToken cancellationToken) + public virtual Task SaveAsync(CancellationToken cancellationToken = default) { CheckDetectChanges(); @@ -141,7 +128,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) @@ -181,7 +168,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]); @@ -190,7 +177,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() @@ -230,70 +217,19 @@ protected PropertyInfo[] GetKeyProperties() { object value = keyField.GetValue(entity); - // TODO: Check for "AutoGenerated" attribute on this field - object defaultValue = Activator.CreateInstance(keyField.PropertyType); + // TODO: Check for "AutoGenerated" attribute on this field? + // String is a special case and we cannot create it using Activator + if (keyField.PropertyType == typeof(string)) + return string.IsNullOrWhiteSpace((string)value); + + object defaultValue = Activator.CreateInstance(keyField.PropertyType); if (value.Equals(defaultValue)) return true; } 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 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 e28dcbf..0000000 Binary files a/src/EFRepository/logo.png and /dev/null differ 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 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 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 diff --git a/src/UnitTestsCore/UnitTestsCore.csproj b/src/UnitTestsCore/UnitTestsCore.csproj index 0973ddc..9b8bf20 100644 --- a/src/UnitTestsCore/UnitTestsCore.csproj +++ b/src/UnitTestsCore/UnitTestsCore.csproj @@ -22,7 +22,7 @@ - +