diff --git a/src/Easify.Ef.Testing.UnitTests/DbContextExtensionsTests.cs b/src/Easify.Ef.Testing.UnitTests/DbContextExtensionsTests.cs
index f44697f..b523d40 100644
--- a/src/Easify.Ef.Testing.UnitTests/DbContextExtensionsTests.cs
+++ b/src/Easify.Ef.Testing.UnitTests/DbContextExtensionsTests.cs
@@ -15,6 +15,8 @@
// along with this program. If not, see .
//
+using Easify.Ef.UnitOfWork;
+
namespace Easify.Ef.Testing.UnitTests;
public class DbContextExtensionsTests
diff --git a/src/Easify.Ef.Testing.UnitTests/Models/SampleDbContext.cs b/src/Easify.Ef.Testing.UnitTests/Models/SampleDbContext.cs
index 610a429..72227f7 100644
--- a/src/Easify.Ef.Testing.UnitTests/Models/SampleDbContext.cs
+++ b/src/Easify.Ef.Testing.UnitTests/Models/SampleDbContext.cs
@@ -15,6 +15,8 @@
// along with this program. If not, see .
//
+using Easify.Http;
+
namespace Easify.Ef.Testing.UnitTests.Models;
public class SampleDbContext : DbContextBase
diff --git a/src/Easify.Ef.Testing.UnitTests/Usings.cs b/src/Easify.Ef.Testing.UnitTests/Usings.cs
index d40c248..f12dfdb 100644
--- a/src/Easify.Ef.Testing.UnitTests/Usings.cs
+++ b/src/Easify.Ef.Testing.UnitTests/Usings.cs
@@ -1,10 +1,7 @@
global using Easify.Ef.Testing.UnitTests.Models;
global using Easify.Ef.Testing.Extensions;
-global using Easify.Http;
global using Microsoft.EntityFrameworkCore;
global using Microsoft.Extensions.DependencyInjection;
-
-global using EfCore.UnitOfWork;
global using FluentAssertions;
global using Xunit;
diff --git a/src/Easify.Ef.Testing/Extensions/EntityListExtensions.cs b/src/Easify.Ef.Testing/Extensions/EntityListExtensions.cs
index 048bfcc..eb33910 100644
--- a/src/Easify.Ef.Testing/Extensions/EntityListExtensions.cs
+++ b/src/Easify.Ef.Testing/Extensions/EntityListExtensions.cs
@@ -15,6 +15,8 @@
// along with this program. If not, see .
//
+using Easify.Ef.UnitOfWork;
+
namespace Easify.Ef.Testing.Extensions;
public static class EntityListExtensions
diff --git a/src/Easify.Ef.Testing/Usings.cs b/src/Easify.Ef.Testing/Usings.cs
index 3dfddac..115040c 100644
--- a/src/Easify.Ef.Testing/Usings.cs
+++ b/src/Easify.Ef.Testing/Usings.cs
@@ -7,5 +7,5 @@
global using Microsoft.Extensions.DependencyInjection;
global using Microsoft.Extensions.DependencyInjection.Extensions;
-global using EfCore.UnitOfWork;
+global using Easify.Ef.UnitOfWork;
global using AutoMapper;
diff --git a/src/Easify.Ef.UnitTests/UnitOfWork/Entities/City.cs b/src/Easify.Ef.UnitTests/UnitOfWork/Entities/City.cs
new file mode 100644
index 0000000..0c55b47
--- /dev/null
+++ b/src/Easify.Ef.UnitTests/UnitOfWork/Entities/City.cs
@@ -0,0 +1,13 @@
+using System.Collections.Generic;
+
+namespace Easify.Ef.UnitOfWork.UnitTests.Entities
+{
+ public class City
+ {
+ public int Id { get; set; }
+ public string Name { get; set; }
+ public int CountryId { get; set; }
+ public Country Country { get; set; }
+ public List Towns { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/src/Easify.Ef.UnitTests/UnitOfWork/Entities/Country.cs b/src/Easify.Ef.UnitTests/UnitOfWork/Entities/Country.cs
new file mode 100644
index 0000000..d2a25df
--- /dev/null
+++ b/src/Easify.Ef.UnitTests/UnitOfWork/Entities/Country.cs
@@ -0,0 +1,13 @@
+using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
+
+namespace Easify.Ef.UnitOfWork.UnitTests.Entities
+{
+ public class Country
+ {
+ [Key]
+ public int Id { get; set; }
+ public string Name { get; set; }
+ public List Cities { get; set; }
+ }
+}
diff --git a/src/Easify.Ef.UnitTests/UnitOfWork/Entities/Customer.cs b/src/Easify.Ef.UnitTests/UnitOfWork/Entities/Customer.cs
new file mode 100644
index 0000000..e438eae
--- /dev/null
+++ b/src/Easify.Ef.UnitTests/UnitOfWork/Entities/Customer.cs
@@ -0,0 +1,9 @@
+namespace Easify.Ef.UnitOfWork.UnitTests.Entities
+{
+ public class Customer
+ {
+ public int Id { get; set; }
+ public string Name { get; set; }
+ public int Age { get; set; }
+ }
+}
diff --git a/src/Easify.Ef.UnitTests/UnitOfWork/Entities/Town.cs b/src/Easify.Ef.UnitTests/UnitOfWork/Entities/Town.cs
new file mode 100644
index 0000000..23cee39
--- /dev/null
+++ b/src/Easify.Ef.UnitTests/UnitOfWork/Entities/Town.cs
@@ -0,0 +1,10 @@
+namespace Easify.Ef.UnitOfWork.UnitTests.Entities
+{
+ public class Town
+ {
+ public int Id { get; set; }
+ public string Name { get; set; }
+ public int CityId { get; set; }
+ public City City { get; set; }
+ }
+}
diff --git a/src/Easify.Ef.UnitTests/UnitOfWork/InMemoryDbContext.cs b/src/Easify.Ef.UnitTests/UnitOfWork/InMemoryDbContext.cs
new file mode 100644
index 0000000..0017759
--- /dev/null
+++ b/src/Easify.Ef.UnitTests/UnitOfWork/InMemoryDbContext.cs
@@ -0,0 +1,27 @@
+using System;
+using Easify.Ef.UnitOfWork.UnitTests.Entities;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Diagnostics;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace Easify.Ef.UnitOfWork.UnitTests
+{
+ public class InMemoryDbContext : DbContext
+ {
+ public DbSet Countries { get; set; }
+ public DbSet Customers { get; set; }
+
+ protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
+ {
+ var name = $"{nameof(InMemoryDbContext)}_{Guid.NewGuid()}";
+
+ var serviceProvider = new ServiceCollection()
+ .AddEntityFrameworkInMemoryDatabase()
+ .BuildServiceProvider();
+
+ optionsBuilder.UseInMemoryDatabase(name)
+ .ConfigureWarnings(warnings => warnings.Ignore(InMemoryEventId.TransactionIgnoredWarning))
+ .UseInternalServiceProvider(serviceProvider);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Easify.Ef.UnitTests/UnitOfWork/RepositoryFixture.cs b/src/Easify.Ef.UnitTests/UnitOfWork/RepositoryFixture.cs
new file mode 100644
index 0000000..d9d9fa1
--- /dev/null
+++ b/src/Easify.Ef.UnitTests/UnitOfWork/RepositoryFixture.cs
@@ -0,0 +1,50 @@
+using Easify.Ef.UnitOfWork.UnitTests.Entities;
+
+namespace Easify.Ef.UnitOfWork.UnitTests
+{
+ public class RepositoryFixture
+ {
+ private static IEnumerable TestCountries => new List
+ {
+ new Country {Id = 1, Name = "A"},
+ new Country {Id = 2, Name = "B"}
+ };
+
+ private static IEnumerable TestCities => new List
+ {
+ new City { Id = 1, Name = "A", CountryId = 1},
+ new City { Id = 2, Name = "B", CountryId = 2},
+ new City { Id = 3, Name = "C", CountryId = 1},
+ new City { Id = 4, Name = "D", CountryId = 2},
+ new City { Id = 5, Name = "E", CountryId = 1},
+ new City { Id = 6, Name = "F", CountryId = 2},
+ };
+
+ private static IEnumerable TestTowns => new List
+ {
+ new Town { Id = 1, Name="A", CityId = 1 },
+ new Town { Id = 2, Name="B", CityId = 2 },
+ new Town { Id = 3, Name="C", CityId = 3 },
+ new Town { Id = 4, Name="D", CityId = 4 },
+ new Town { Id = 5, Name="E", CityId = 5 },
+ new Town { Id = 6, Name="F", CityId = 6 },
+ };
+
+ public InMemoryDbContext DbContext()
+ {
+ var dbContext = new InMemoryDbContext();
+
+ dbContext.AddRange(TestCountries);
+ dbContext.AddRange(TestCities);
+ dbContext.AddRange(TestTowns);
+
+ dbContext.SaveChanges();
+
+ return dbContext;
+ }
+
+ public IUnitOfWork CreateUnitOfWork() => new UnitOfWork(DbContext());
+
+ public IRepository CreateRepository() where T : class => new Repository(DbContext());
+ }
+}
\ No newline at end of file
diff --git a/src/Easify.Ef.UnitTests/UnitOfWork/RepositoryTests.cs b/src/Easify.Ef.UnitTests/UnitOfWork/RepositoryTests.cs
new file mode 100644
index 0000000..25c0ffd
--- /dev/null
+++ b/src/Easify.Ef.UnitTests/UnitOfWork/RepositoryTests.cs
@@ -0,0 +1,171 @@
+using System.Threading.Tasks;
+using Easify.Ef.UnitOfWork.UnitTests.Entities;
+using Easify.Ef.UnitOfWork.Extensions;
+using FluentAssertions;
+using Microsoft.EntityFrameworkCore;
+using Xunit;
+
+namespace Easify.Ef.UnitOfWork.UnitTests
+{
+ public class RepositoryTests : IClassFixture
+ {
+ private readonly RepositoryFixture _fixture;
+
+ public RepositoryTests(RepositoryFixture fixture)
+ {
+ _fixture = fixture;
+ }
+
+ [Fact]
+ public void Should_GetList_ResolveTheRightSetOfDataFromContext()
+ {
+ // Arrange
+ var repository = _fixture.CreateRepository();
+
+ // Act
+ var actual = repository.GetList(t => t.Name == "C", q => q.Include(t => t.Country).PagedBy(0, 1));
+
+ // Assert
+
+ actual.Should().HaveCount(1)
+ .And.Contain(m => m.Country != null && m.CountryId != 0 && m.Country.Name == "A" && m.Country.Id == 1);
+ }
+
+ [Fact]
+ public async Task Should_GetListAsync_ResolveTheRightSetOfDataFromContext()
+ {
+ // Arrange
+ var repository = _fixture.CreateRepository();
+
+ // Act
+ var actual =
+ await repository.GetListAsync(t => t.Name == "C", q => q.Include(t => t.Country).PagedBy(0, 1));
+
+ // Assert
+
+ actual.Should().HaveCount(1)
+ .And.Contain(m => m.Country != null && m.CountryId != 0 && m.Country.Name == "A" && m.Country.Id == 1);
+ }
+
+ [Fact]
+ public void Should_GetProjectedList_WithProjection_ResolveTheSmallerSetOfDataFromContext()
+ {
+ // Arrange
+ var repository = _fixture.CreateRepository();
+
+ // Act
+ var actual = repository.GetProjectedList(t => t.Name == "C", m => m.Name, q => q.Include(t => t.Country));
+
+ // Assert
+ actual.Should().HaveCount(1)
+ .And.Contain(m => m == "C");
+ }
+
+ [Fact]
+ public async Task Should_GetProjectedListAsync_WithProjection_ResolveTheSmallerSetOfDataFromContext()
+ {
+ // Arrange
+ var repository = _fixture.CreateRepository();
+
+ // Act
+ var actual =
+ await repository.GetProjectedListAsync(t => t.Name == "C", m => m.Name);
+ // await repository.GetProjectedListAsync(t => t.Name == "C", m => m.Name, q => q.Include(t => t.Country));
+
+ // Assert
+ actual.Should().HaveCount(1)
+ .And.Contain(m => m == "C");
+ }
+
+ [Fact]
+ public void Should_GetFirstOrDefault_ResolveTheRightSetOfDataFromContext()
+ {
+ // Arrange
+ var repository = _fixture.CreateRepository();
+
+ // Act
+ var actual = repository.GetFirstOrDefault(t => t.Name == "C", q => q.Include(t => t.Country));
+
+ // Assert
+ actual.Should().NotBeNull();
+ actual.Name.Should().Be("C");
+ actual.CountryId.Should().Be(1);
+ actual.Country.Should().BeEquivalentTo(new Country {Name = "A", Id = 1}, m => m.Excluding(t => t.Cities));
+ }
+
+ [Fact]
+ public async Task Should_GetFirstOrDefaultAsync_ResolveTheRightSetOfDataFromContext()
+ {
+ // Arrange
+ var repository = _fixture.CreateRepository();
+
+ // Act
+ var actual = await repository.GetFirstOrDefaultAsync(t => t.Name == "C", q => q.Include(t => t.Country));
+
+ // Assert
+ actual.Should().NotBeNull();
+ actual.Name.Should().Be("C");
+ actual.CountryId.Should().Be(1);
+ actual.Country.Should().BeEquivalentTo(new Country {Name = "A", Id = 1}, m => m.Excluding(t => t.Cities));
+ }
+
+ [Fact]
+ public async Task Should_GetFirstOrDefaultAsync_ReturnsMultipleLevelOfHierarchyWhenItsIncludedInQuery()
+ {
+ // Arrange
+ var repository = _fixture.CreateRepository();
+
+ // Act
+ var actual = await repository.GetFirstOrDefaultAsync(t => t.Name == "A",
+ country => country.Include(c => c.Cities).ThenInclude(city => city.Towns));
+
+ // Assert
+ actual.Should().NotBeNull();
+ actual.Cities.Should().HaveCount(3).And.Contain(m => m.Towns.Count == 1);
+ }
+
+ [Fact]
+ public void Should_Find_ReturnsCertainEntityByIdentifier()
+ {
+ // Arrange
+ var repository = _fixture.CreateRepository();
+
+ // Act
+ var actual = repository.Find(1);
+
+ // Assert
+ actual.Should().NotBeNull();
+ actual.Name.Should().Be("A");
+ }
+
+ [Fact]
+ public async Task Should_FindAsync_ReturnsCertainEntityByIdentifier()
+ {
+ // Arrange
+ var repository = _fixture.CreateRepository();
+
+ // Act
+ var actual = await repository.FindAsync(1);
+
+ // Assert
+ actual.Should().NotBeNull();
+ actual.Name.Should().Be("A");
+ }
+
+ [Theory]
+ [InlineData("A", 1)]
+ [InlineData("B", 1)]
+ [InlineData("C", 0)]
+ public void Should_Count_ReturnsTheNumberOfRecordsAccordingly(string phrase, int expectedCount)
+ {
+ // Arrange
+ var repository = _fixture.CreateRepository();
+
+ // Act
+ var actual = repository.Count(m => m.Name == phrase);
+
+ // Assert
+ actual.Should().Be(expectedCount);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Easify.Ef.UnitTests/UnitOfWork/UnitOfWorkTests.cs b/src/Easify.Ef.UnitTests/UnitOfWork/UnitOfWorkTests.cs
new file mode 100644
index 0000000..ac447c6
--- /dev/null
+++ b/src/Easify.Ef.UnitTests/UnitOfWork/UnitOfWorkTests.cs
@@ -0,0 +1,84 @@
+using Easify.Ef.UnitOfWork.UnitTests.Entities;
+
+namespace Easify.Ef.UnitOfWork.UnitTests
+{
+ public class UnitOfWorkTests : IClassFixture
+ {
+ private readonly RepositoryFixture _fixture;
+
+ public UnitOfWorkTests(RepositoryFixture fixture) => _fixture = fixture;
+
+ [Fact]
+ public void Should_SaveChanges_StoreTheDataCorrectly()
+ {
+ // Arrange
+ var uow = _fixture.CreateUnitOfWork();
+
+ // Act
+ var repository = uow.GetRepository();
+ repository.Insert(new Country() {Id = 100, Name = "Country 100"});
+ repository.Insert(new Country() {Id = 101, Name = "Country 101"});
+ repository.Insert(new Country() {Id = 102, Name = "Country 102"});
+ uow.SaveChanges();
+
+ var actual = repository.GetList();
+
+ // Assert
+
+ actual.Should().HaveCount(5);
+ }
+
+ [Fact]
+ public async Task Should_SaveChangesAsync_StoreTheDataCorrectly()
+ {
+ // Arrange
+ var uow = _fixture.CreateUnitOfWork();
+
+ // Act
+ var repository = uow.GetRepository();
+ repository.Insert(new Country() {Id = 100, Name = "Country 100"});
+ repository.Insert(new Country() {Id = 101, Name = "Country 101"});
+ repository.Insert(new Country() {Id = 102, Name = "Country 102"});
+ await uow.SaveChangesAsync();
+
+ var actual = await repository.GetListAsync();
+
+ // Assert
+ actual.Should().HaveCount(5);
+ }
+
+ [Fact]
+ public void Should_SaveChanges_WhenMultipleOperation_ThenChangeTheSateOfTheDataSourceAccordingly()
+ {
+ // Arrange
+ var uow = _fixture.CreateUnitOfWork();
+ var repository = uow.GetRepository();
+
+ // Act
+ var list = repository.GetList().ToList();
+ var entityForUpdate = list.First();
+ entityForUpdate.Name = $"Country {entityForUpdate.Id}";
+
+ repository.Insert(new List
+ {
+ new Country {Id = 100, Name = "Country 100"},
+ new Country {Id = 101, Name = "Country 101"},
+ new Country {Id = 102, Name = "Country 102"}
+ });
+
+ repository.Update(entityForUpdate);
+ repository.Delete(list.Last().Id);
+
+ uow.SaveChanges();
+
+ var actual = repository.GetList();
+
+ // Assert
+ actual.Should().HaveCount(4)
+ .And.Contain(m => m.Id == 1 && m.Name == "Country 1")
+ .And.Contain(m => m.Id == 100 && m.Name == "Country 100")
+ .And.Contain(m => m.Id == 101 && m.Name == "Country 101")
+ .And.Contain(m => m.Id == 102 && m.Name == "Country 102");
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Easify.Ef.UnitTests/Usings.cs b/src/Easify.Ef.UnitTests/Usings.cs
index 09454b6..e02b3d5 100644
--- a/src/Easify.Ef.UnitTests/Usings.cs
+++ b/src/Easify.Ef.UnitTests/Usings.cs
@@ -7,7 +7,7 @@
global using Microsoft.EntityFrameworkCore;
-global using EfCore.UnitOfWork;
+global using Easify.Ef.UnitOfWork;
global using FluentAssertions;
global using NSubstitute;
global using Xunit;
diff --git a/src/Easify.Ef.sln b/src/Easify.Ef.sln
index 509645b..dde4f06 100644
--- a/src/Easify.Ef.sln
+++ b/src/Easify.Ef.sln
@@ -1,15 +1,15 @@
Microsoft Visual Studio Solution File, Format Version 12.00
-# Visual Studio 15
-VisualStudioVersion = 15.0.26730.16
+# Visual Studio Version 17
+VisualStudioVersion = 17.4.33122.133
MinimumVisualStudioVersion = 15.0.26124.0
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Easify.Ef", "Easify.Ef\Easify.Ef.csproj", "{172C7EEB-9663-4B4C-94A9-295F4961C285}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Easify.Ef.UnitTests", "Easify.Ef.UnitTests\Easify.Ef.UnitTests.csproj", "{EE8ACCA4-BFEB-4A17-80ED-86C7CF3BC781}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Easify.Ef.UnitTests", "Easify.Ef.UnitTests\Easify.Ef.UnitTests.csproj", "{EE8ACCA4-BFEB-4A17-80ED-86C7CF3BC781}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Easify.Ef.Testing", "Easify.Ef.Testing\Easify.Ef.Testing.csproj", "{8258D7DC-CE00-46F6-B7E2-C54FA9259AE5}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Easify.Ef.Testing", "Easify.Ef.Testing\Easify.Ef.Testing.csproj", "{8258D7DC-CE00-46F6-B7E2-C54FA9259AE5}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Easify.Ef.Testing.UnitTests", "Easify.Ef.Testing.UnitTests\Easify.Ef.Testing.UnitTests.csproj", "{D4B8183D-0B90-470C-BE92-89EC3B99C246}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Easify.Ef.Testing.UnitTests", "Easify.Ef.Testing.UnitTests\Easify.Ef.Testing.UnitTests.csproj", "{D4B8183D-0B90-470C-BE92-89EC3B99C246}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
diff --git a/src/Easify.Ef/Easify.Ef.csproj b/src/Easify.Ef/Easify.Ef.csproj
index 79026ee..90329bd 100644
--- a/src/Easify.Ef/Easify.Ef.csproj
+++ b/src/Easify.Ef/Easify.Ef.csproj
@@ -13,8 +13,10 @@
-
+
+
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
diff --git a/src/Easify.Ef/UnitOfWork/Extensions/PagingOptions.cs b/src/Easify.Ef/UnitOfWork/Extensions/PagingOptions.cs
new file mode 100644
index 0000000..26ca7b6
--- /dev/null
+++ b/src/Easify.Ef/UnitOfWork/Extensions/PagingOptions.cs
@@ -0,0 +1,25 @@
+using System;
+
+namespace Easify.Ef.UnitOfWork.Extensions
+{
+ public sealed class PagingOptions
+ {
+ private const int DefaultPageSize = 20;
+
+ public PagingOptions(int pageIndex, int pageSize = DefaultPageSize)
+ {
+ if (pageSize <= 0) throw new ArgumentOutOfRangeException(nameof(pageSize));
+ if (pageIndex <= 0) throw new ArgumentOutOfRangeException(nameof(pageIndex));
+
+ PageIndex = pageIndex;
+ PageSize = pageSize;
+ }
+
+ public PagingOptions() : this(0, DefaultPageSize)
+ {
+ }
+
+ public int PageIndex { get; }
+ public int PageSize { get; }
+ }
+}
\ No newline at end of file
diff --git a/src/Easify.Ef/UnitOfWork/Extensions/QueryableExtensions.cs b/src/Easify.Ef/UnitOfWork/Extensions/QueryableExtensions.cs
new file mode 100644
index 0000000..458212b
--- /dev/null
+++ b/src/Easify.Ef/UnitOfWork/Extensions/QueryableExtensions.cs
@@ -0,0 +1,33 @@
+using System;
+using System.Linq;
+
+namespace Easify.Ef.UnitOfWork.Extensions
+{
+ public static class QueryableExtensions
+ {
+ public static IQueryable PagedBy(this IQueryable queryable, PagingOptions options)
+ {
+ if (queryable == null) throw new ArgumentNullException(nameof(queryable));
+ if (options == null) throw new ArgumentNullException(nameof(options));
+
+ return queryable.PagedBy(options.PageIndex, options.PageSize);
+ }
+
+ public static IQueryable PagedBy(this IQueryable queryable, int pageIndex, int pageSize)
+ {
+ if (queryable == null) throw new ArgumentNullException(nameof(queryable));
+ if (pageIndex < 0) throw new ArgumentOutOfRangeException(nameof(pageIndex));
+ if (pageSize <= 0) throw new ArgumentOutOfRangeException(nameof(pageSize));
+
+ return queryable.Skip(pageIndex * pageSize).Take(pageSize);
+ }
+
+ public static IQueryable ProjectTo(this IQueryable queryable, Func projection)
+ {
+ if (queryable == null) throw new ArgumentNullException(nameof(queryable));
+ if (projection == null) throw new ArgumentNullException(nameof(projection));
+
+ return queryable.Select(m => projection(m));
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Easify.Ef/UnitOfWork/IRepository.cs b/src/Easify.Ef/UnitOfWork/IRepository.cs
new file mode 100644
index 0000000..deea63c
--- /dev/null
+++ b/src/Easify.Ef/UnitOfWork/IRepository.cs
@@ -0,0 +1,63 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Linq.Expressions;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.EntityFrameworkCore.ChangeTracking;
+
+namespace Easify.Ef.UnitOfWork
+{
+ public interface IRepository where TEntity : class
+ {
+ IQueryable FromSql(string sql, params object[] parameters);
+ TEntity Find(params object[] keyValues);
+ ValueTask FindAsync(params object[] keyValues);
+ ValueTask FindAsync(object[] keyValues, CancellationToken cancellationToken);
+ int Count(Expression> predicate = null);
+ void Insert(TEntity entity);
+ void Insert(params TEntity[] entities);
+ void Insert(IEnumerable entities);
+ ValueTask> InsertAsync(TEntity entity, CancellationToken cancellationToken = default);
+ Task InsertAsync(params TEntity[] entities);
+ Task InsertAsync(IEnumerable entities, CancellationToken cancellationToken = default);
+ void Update(TEntity entity);
+ void Update(params TEntity[] entities);
+ void Update(IEnumerable entities);
+ void Delete(object id);
+ void Delete(TEntity entity);
+ void Delete(params TEntity[] entities);
+ void Delete(IEnumerable entities);
+
+ Task> GetListAsync(Expression> predicate,
+ Func, IQueryable> extendBy = null);
+
+ Task> GetListAsync(Func, IQueryable> extendBy = null);
+
+ IEnumerable GetList(Expression> predicate,
+ Func, IQueryable> extendBy = null);
+
+ IEnumerable GetList(Func, IQueryable> extendBy = null);
+
+ Task GetFirstOrDefaultAsync(Expression> predicate,
+ Func, IQueryable> extendBy = null);
+
+ TEntity GetFirstOrDefault(Expression> predicate,
+ Func, IQueryable> extendBy = null);
+
+ Task> GetProjectedListAsync(Expression> predicate,
+ Func projectedBy,
+ Func, IQueryable> extendBy = null);
+
+ Task> GetProjectedListAsync(
+ Func projectedBy,
+ Func, IQueryable> extendBy = null);
+
+ IEnumerable GetProjectedList(Expression> predicate,
+ Func projectedBy,
+ Func, IQueryable> extendBy = null);
+
+ IEnumerable GetProjectedList(Func projectedBy,
+ Func, IQueryable> extendBy = null);
+ }
+}
\ No newline at end of file
diff --git a/src/Easify.Ef/UnitOfWork/IRepositoryFactory.cs b/src/Easify.Ef/UnitOfWork/IRepositoryFactory.cs
new file mode 100644
index 0000000..5efb8fa
--- /dev/null
+++ b/src/Easify.Ef/UnitOfWork/IRepositoryFactory.cs
@@ -0,0 +1,7 @@
+namespace Easify.Ef.UnitOfWork
+{
+ public interface IRepositoryFactory
+ {
+ IRepository GetRepository(bool hasCustomRepository = false) where TEntity : class;
+ }
+}
\ No newline at end of file
diff --git a/src/Easify.Ef/UnitOfWork/IUnitOfWork.cs b/src/Easify.Ef/UnitOfWork/IUnitOfWork.cs
new file mode 100644
index 0000000..2d85a67
--- /dev/null
+++ b/src/Easify.Ef/UnitOfWork/IUnitOfWork.cs
@@ -0,0 +1,17 @@
+using System;
+using System.Linq;
+using System.Threading.Tasks;
+using Microsoft.EntityFrameworkCore.ChangeTracking;
+
+namespace Easify.Ef.UnitOfWork
+{
+ public interface IUnitOfWork : IDisposable
+ {
+ IRepository GetRepository(bool hasCustomRepository = false) where TEntity : class;
+ int SaveChanges(bool ensureAutoHistory = false);
+ Task SaveChangesAsync(bool ensureAutoHistory = false);
+ int ExecuteSqlCommand(string sql, params object[] parameters);
+ IQueryable FromSql(string sql, params object[] parameters) where TEntity : class;
+ void TrackGraph(object rootEntity, Action callback);
+ }
+}
\ No newline at end of file
diff --git a/src/Easify.Ef/UnitOfWork/IUnitOfWorkOfT.cs b/src/Easify.Ef/UnitOfWork/IUnitOfWorkOfT.cs
new file mode 100644
index 0000000..f5809c8
--- /dev/null
+++ b/src/Easify.Ef/UnitOfWork/IUnitOfWorkOfT.cs
@@ -0,0 +1,10 @@
+using System.Threading.Tasks;
+using Microsoft.EntityFrameworkCore;
+
+namespace Easify.Ef.UnitOfWork
+{
+ public interface IUnitOfWork : IUnitOfWork where TContext : DbContext {
+ TContext DbContext { get; }
+ Task SaveChangesAsync(bool ensureAutoHistory = false, params IUnitOfWork[] unitOfWorks);
+ }
+}
diff --git a/src/Easify.Ef/UnitOfWork/Repository.cs b/src/Easify.Ef/UnitOfWork/Repository.cs
new file mode 100644
index 0000000..30038db
--- /dev/null
+++ b/src/Easify.Ef/UnitOfWork/Repository.cs
@@ -0,0 +1,136 @@
+// Copyright (c) Arch team. All rights reserved.
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Linq.Expressions;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.ChangeTracking;
+
+namespace Easify.Ef.UnitOfWork
+{
+ public sealed class Repository : IRepository where TEntity : class
+ {
+ private readonly DbContext _dbContext;
+ private readonly DbSet _dbSet;
+
+ public Repository(DbContext dbContext)
+ {
+ _dbContext = dbContext ?? throw new ArgumentNullException(nameof(dbContext));
+ _dbSet = _dbContext.Set();
+ }
+
+ public async Task> GetListAsync(Expression> predicate,
+ Func, IQueryable> extendBy = null) =>
+ await GetListInternal(predicate, extendBy).ToListAsync();
+
+ public async Task> GetProjectedListAsync(Expression> predicate,
+ Func projectedBy,
+ Func, IQueryable> extendBy = null)
+ {
+ var results = await GetListInternal(predicate, extendBy).ToListAsync();
+ return results.Select(projectedBy).ToList();
+ }
+
+ public async Task> GetListAsync(Func, IQueryable> extendBy = null) =>
+ await GetListInternal(p => true, extendBy).ToListAsync();
+
+ public async Task> GetProjectedListAsync(
+ Func projectedBy,
+ Func, IQueryable> extendBy = null)
+ {
+ var results = await GetListInternal(p => true, extendBy).ToListAsync();
+ return results.Select(projectedBy).ToList();
+ }
+
+ public IEnumerable GetList(Expression> predicate,
+ Func, IQueryable> extendBy = null) =>
+ GetListInternal(predicate, extendBy).ToList();
+
+ public IEnumerable GetProjectedList(Expression> predicate,
+ Func projectedBy,
+ Func, IQueryable> extendBy = null) =>
+ GetListInternal(predicate, extendBy).AsEnumerable().Select(projectedBy).ToList();
+
+ public IEnumerable GetList(Func, IQueryable> extendBy = null) =>
+ GetListInternal(p => true, extendBy).ToList();
+
+ public IEnumerable GetProjectedList(Func projectedBy,
+ Func, IQueryable> extendBy = null) =>
+ GetListInternal(p => true, extendBy).AsEnumerable().Select(projectedBy).ToList();
+
+ public async Task GetFirstOrDefaultAsync(Expression> predicate,
+ Func, IQueryable> extendBy = null) =>
+ await GetListInternal(predicate, extendBy).FirstOrDefaultAsync();
+
+ public TEntity GetFirstOrDefault(Expression> predicate,
+ Func, IQueryable> extendBy = null) =>
+ GetListInternal(predicate, extendBy).FirstOrDefault();
+
+ private IQueryable GetListInternal(Expression> predicate,
+ Func, IQueryable> extend = null)
+ {
+ if (predicate == null) throw new ArgumentNullException(nameof(predicate));
+
+ var queryable = _dbSet.Where(predicate);
+ if (extend != null)
+ queryable = extend(queryable);
+
+ return queryable;
+ }
+ private IQueryable GetListProjectedByInternal(Expression> predicate,
+ Func projectedBy,
+ Func, IQueryable> extend = null)
+ {
+ if (predicate == null) throw new ArgumentNullException(nameof(predicate));
+
+ var queryable = GetListInternal(predicate, extend);
+ return queryable.Select(i => projectedBy(i));
+ }
+
+ public IQueryable FromSql(string sql, params object[] parameters) => _dbSet.FromSqlRaw(sql, parameters);
+
+ public TEntity Find(params object[] keyValues) => _dbSet.Find(keyValues);
+
+ public ValueTask FindAsync(params object[] keyValues) => _dbSet.FindAsync(keyValues);
+
+ public ValueTask FindAsync(object[] keyValues, CancellationToken cancellationToken) => _dbSet.FindAsync(keyValues, cancellationToken);
+
+ public int Count(Expression> predicate = null) => predicate == null ? _dbSet.Count() : _dbSet.Count(predicate);
+
+ public void Insert(TEntity entity) => _dbSet.Add(entity);
+
+ public void Insert(params TEntity[] entities) => _dbSet.AddRange(entities);
+
+ public void Insert(IEnumerable entities) => _dbSet.AddRange(entities);
+
+ public ValueTask> InsertAsync(TEntity entity, CancellationToken cancellationToken = default) => _dbSet.AddAsync(entity, cancellationToken);
+
+ public Task InsertAsync(params TEntity[] entities) => _dbSet.AddRangeAsync(entities);
+
+ public Task InsertAsync(IEnumerable entities, CancellationToken cancellationToken = default) => _dbSet.AddRangeAsync(entities, cancellationToken);
+
+ public void Update(TEntity entity) => _dbSet.Update(entity);
+
+ public void Update(params TEntity[] entities) => _dbSet.UpdateRange(entities);
+
+ public void Update(IEnumerable entities) => _dbSet.UpdateRange(entities);
+
+ public void Delete(TEntity entity) => _dbSet.Remove(entity);
+
+ public void Delete(object id)
+ {
+ var entity = _dbSet.Find(id);
+ if (entity != null)
+ {
+ Delete(entity);
+ }
+ }
+
+ public void Delete(params TEntity[] entities) => _dbSet.RemoveRange(entities);
+
+ public void Delete(IEnumerable entities) => _dbSet.RemoveRange(entities);
+ }
+}
diff --git a/src/Easify.Ef/UnitOfWork/UnitOfWork.cs b/src/Easify.Ef/UnitOfWork/UnitOfWork.cs
new file mode 100644
index 0000000..7b81823
--- /dev/null
+++ b/src/Easify.Ef/UnitOfWork/UnitOfWork.cs
@@ -0,0 +1,102 @@
+// Copyright (c) Arch team. All rights reserved.
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+using System.Transactions;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.ChangeTracking;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+
+namespace Easify.Ef.UnitOfWork
+{
+ public sealed class UnitOfWork : IRepositoryFactory, IUnitOfWork where TContext : DbContext
+ {
+ private bool _disposed;
+ private Dictionary _repositories;
+
+ public UnitOfWork(TContext context) => DbContext = context ?? throw new ArgumentNullException(nameof(context));
+
+ public TContext DbContext { get; }
+
+ public IRepository GetRepository(bool hasCustomRepository = false) where TEntity : class
+ {
+ _repositories ??= new Dictionary();
+
+ // TODO: Find a better way of resolving the repository
+ if (hasCustomRepository)
+ {
+ var repository = DbContext.GetService>();
+ if (repository != null)
+ return repository;
+ }
+
+ // TODO: Needs to be thread-safe
+ var type = typeof(TEntity);
+ if (!_repositories.ContainsKey(type))
+ _repositories[type] = new Repository(DbContext);
+
+ return (IRepository)_repositories[type];
+ }
+
+ public int ExecuteSqlCommand(string sql, params object[] parameters) => DbContext.Database.ExecuteSqlRaw(sql, parameters);
+
+ public IQueryable FromSql(string sql, params object[] parameters) where TEntity : class => DbContext.Set().FromSqlRaw(sql, parameters);
+
+ public int SaveChanges(bool ensureAutoHistory = false)
+ {
+ if (ensureAutoHistory)
+ DbContext.EnsureAutoHistory();
+
+ return DbContext.SaveChanges();
+ }
+
+ public async Task SaveChangesAsync(bool ensureAutoHistory = false)
+ {
+ if (ensureAutoHistory)
+ DbContext.EnsureAutoHistory();
+
+ return await DbContext.SaveChangesAsync();
+ }
+
+ public async Task SaveChangesAsync(bool ensureAutoHistory = false, params IUnitOfWork[] unitOfWorks)
+ {
+ using (var ts = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled))
+ {
+ var tasks = unitOfWorks.Select(async unitOfWork => await unitOfWork.SaveChangesAsync(ensureAutoHistory)).ToList();
+ var results = await Task.WhenAll(tasks);
+
+ var count = results.Sum();
+ count += await SaveChangesAsync(ensureAutoHistory);
+
+ ts.Complete();
+
+ return count;
+ }
+ }
+
+ public void Dispose()
+ {
+ Dispose(true);
+
+ GC.SuppressFinalize(this);
+ }
+
+ private void Dispose(bool disposing)
+ {
+ if (!_disposed)
+ {
+ if (disposing)
+ {
+ _repositories?.Clear();
+ DbContext.Dispose();
+ }
+ }
+
+ _disposed = true;
+ }
+
+ public void TrackGraph(object rootEntity, Action callback) => DbContext.ChangeTracker.TrackGraph(rootEntity, callback);
+ }
+}
diff --git a/src/Easify.Ef/UnitOfWork/UnitOfWorkServiceCollectionExtensions.cs b/src/Easify.Ef/UnitOfWork/UnitOfWorkServiceCollectionExtensions.cs
new file mode 100644
index 0000000..8eff559
--- /dev/null
+++ b/src/Easify.Ef/UnitOfWork/UnitOfWorkServiceCollectionExtensions.cs
@@ -0,0 +1,62 @@
+using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace Easify.Ef.UnitOfWork
+{
+ public static class UnitOfWorkServiceCollectionExtensions
+ {
+ public static IServiceCollection AddUnitOfWork(this IServiceCollection services) where TContext : DbContext
+ {
+ services.AddScoped>();
+ services.AddScoped>();
+ services.AddScoped, UnitOfWork>();
+
+ return services;
+ }
+
+ public static IServiceCollection AddUnitOfWork(this IServiceCollection services)
+ where TContext1 : DbContext
+ where TContext2 : DbContext
+ {
+ services.AddScoped, UnitOfWork>();
+ services.AddScoped, UnitOfWork>();
+
+ return services;
+ }
+
+ public static IServiceCollection AddUnitOfWork(this IServiceCollection services)
+ where TContext1 : DbContext
+ where TContext2 : DbContext
+ where TContext3 : DbContext
+ {
+ services.AddScoped, UnitOfWork>();
+ services.AddScoped, UnitOfWork>();
+ services.AddScoped, UnitOfWork>();
+
+ return services;
+ }
+
+ public static IServiceCollection AddUnitOfWork(this IServiceCollection services)
+ where TContext1 : DbContext
+ where TContext2 : DbContext
+ where TContext3 : DbContext
+ where TContext4 : DbContext
+ {
+ services.AddScoped, UnitOfWork>();
+ services.AddScoped, UnitOfWork>();
+ services.AddScoped, UnitOfWork>();
+ services.AddScoped, UnitOfWork>();
+
+ return services;
+ }
+
+ public static IServiceCollection AddCustomRepository(this IServiceCollection services)
+ where TEntity : class
+ where TRepository : class, IRepository
+ {
+ services.AddScoped, TRepository>();
+
+ return services;
+ }
+ }
+}
diff --git a/src/Easify.Ef/Usings.cs b/src/Easify.Ef/Usings.cs
index 8cd5aee..a6ea6f1 100644
--- a/src/Easify.Ef/Usings.cs
+++ b/src/Easify.Ef/Usings.cs
@@ -8,4 +8,4 @@
global using Microsoft.EntityFrameworkCore.Infrastructure;
global using Microsoft.Extensions.DependencyInjection;
-global using EfCore.UnitOfWork;
+global using Easify.Ef.UnitOfWork;