├── samples
└── UnitOfWork.Host
│ ├── appsettings.json
│ ├── Models
│ ├── CustomBlogRepository.cs
│ └── BlogggingContext.cs
│ ├── web.config
│ ├── Program.cs
│ ├── Properties
│ └── launchSettings.json
│ ├── UnitOfWork.Host.csproj
│ ├── Startup.cs
│ └── Controllers
│ └── ValuesController.cs
├── test
└── UnitOfWork.Tests
│ ├── Entities
│ ├── Customer.cs
│ ├── Town.cs
│ ├── Country.cs
│ └── City.cs
│ ├── InMemoryContext.cs
│ ├── UnitOfWork.Tests.csproj
│ ├── IQueryablePageListExtensionsTests.cs
│ ├── TestGetFirstOrDefaultAsync.cs
│ └── IRepositoryGetPagedListTest.cs
├── NuGet.config
├── .github
└── workflows
│ └── dotnetcore.yml
├── src
└── UnitOfWork
│ ├── IRepositoryFactory.cs
│ ├── IUnitOfWorkOfT.cs
│ ├── UnitOfWork.csproj
│ ├── Collections
│ ├── IPagedList.cs
│ ├── IQueryablePageListExtensions.cs
│ ├── IEnumerablePagedListExtensions.cs
│ └── PagedList.cs
│ ├── IUnitOfWork.cs
│ ├── UnitOfWorkServiceCollectionExtensions.cs
│ ├── UnitOfWork.cs
│ ├── IRepository.cs
│ └── Repository.cs
├── LICENSE
├── .gitattributes
├── .editorconfig
├── UnitOfWork.sln
├── README.md
└── .gitignore
/samples/UnitOfWork.Host/appsettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "Logging": {
3 | "IncludeScopes": false,
4 | "LogLevel": {
5 | "Default": "Debug",
6 | "System": "Information",
7 | "Microsoft": "Information"
8 | }
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/test/UnitOfWork.Tests/Entities/Customer.cs:
--------------------------------------------------------------------------------
1 | namespace Arch.EntityFrameworkCore.UnitOfWork.Tests.Entities
2 | {
3 | public class Customer
4 | {
5 | public int Id { get; set; }
6 | public string Name { get; set; }
7 | public int Age { get; set; }
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/NuGet.config:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/samples/UnitOfWork.Host/Models/CustomBlogRepository.cs:
--------------------------------------------------------------------------------
1 | namespace Arch.EntityFrameworkCore.UnitOfWork.Host.Models
2 | {
3 | public class CustomBlogRepository : Repository, IRepository
4 | {
5 | public CustomBlogRepository(BloggingContext dbContext) : base(dbContext)
6 | {
7 |
8 | }
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/test/UnitOfWork.Tests/Entities/Town.cs:
--------------------------------------------------------------------------------
1 | namespace Arch.EntityFrameworkCore.UnitOfWork.Tests.Entities
2 | {
3 | public class Town
4 | {
5 | public int Id { get; set; }
6 | public string Name { get; set; }
7 |
8 | public int CityId { get; set; }
9 |
10 | public City City { get; set; }
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/test/UnitOfWork.Tests/Entities/Country.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 |
3 | namespace Arch.EntityFrameworkCore.UnitOfWork.Tests.Entities
4 | {
5 | public class Country
6 | {
7 | public int Id { get; set; }
8 | public string Name { get; set; }
9 |
10 | public List Cities { get; set; }
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/test/UnitOfWork.Tests/Entities/City.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 | using System.ComponentModel.DataAnnotations.Schema;
3 |
4 | namespace Arch.EntityFrameworkCore.UnitOfWork.Tests.Entities
5 | {
6 | public class City
7 | {
8 | public int Id { get; set; }
9 | public string Name { get; set; }
10 |
11 | public int CountryId { get; set; }
12 |
13 | public Country Country { get; set; }
14 |
15 | public List Towns { get; set; }
16 | }
17 | }
--------------------------------------------------------------------------------
/.github/workflows/dotnetcore.yml:
--------------------------------------------------------------------------------
1 | name: Unit Of Work Nuget Package Deploy
2 |
3 | on:
4 | push:
5 | branches: [ master ]
6 | pull_request:
7 | branches: [ master ]
8 |
9 | jobs:
10 | build:
11 |
12 | runs-on: ubuntu-latest
13 |
14 | steps:
15 | - uses: actions/checkout@v2
16 | - name: Setup .NET Core
17 | uses: actions/setup-dotnet@v1
18 | with:
19 | dotnet-version: 3.1.102
20 | - name: Install dependencies
21 | run: dotnet restore
22 | - name: Build
23 | run: dotnet build --configuration Release
24 |
--------------------------------------------------------------------------------
/test/UnitOfWork.Tests/InMemoryContext.cs:
--------------------------------------------------------------------------------
1 | using Arch.EntityFrameworkCore.UnitOfWork.Tests.Entities;
2 | using Microsoft.EntityFrameworkCore;
3 |
4 | namespace Arch.EntityFrameworkCore.UnitOfWork.Tests
5 | {
6 | public class InMemoryContext : DbContext
7 | {
8 | public DbSet Countries { get; set; }
9 | public DbSet Customers { get; set; }
10 |
11 | protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
12 | {
13 | optionsBuilder.UseInMemoryDatabase("test");
14 | }
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/samples/UnitOfWork.Host/web.config:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/samples/UnitOfWork.Host/Program.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.IO;
4 | using System.Linq;
5 | using System.Threading.Tasks;
6 | using Microsoft.AspNetCore.Hosting;
7 | using Microsoft.AspNetCore.Builder;
8 |
9 | namespace Arch.EntityFrameworkCore.UnitOfWork.Host
10 | {
11 | public class Program
12 | {
13 | public static void Main(string[] args)
14 | {
15 | var host = new WebHostBuilder()
16 | .UseKestrel()
17 | .UseContentRoot(Directory.GetCurrentDirectory())
18 | .UseIISIntegration()
19 | .UseStartup()
20 | .Build();
21 |
22 | host.Run();
23 | }
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/samples/UnitOfWork.Host/Properties/launchSettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "iisSettings": {
3 | "windowsAuthentication": false,
4 | "anonymousAuthentication": true,
5 | "iisExpress": {
6 | "applicationUrl": "http://localhost:2310/",
7 | "sslPort": 0
8 | }
9 | },
10 | "profiles": {
11 | "IIS Express": {
12 | "commandName": "IISExpress",
13 | "launchBrowser": true,
14 | "launchUrl": "api/values",
15 | "environmentVariables": {
16 | "ASPNETCORE_ENVIRONMENT": "Development"
17 | }
18 | },
19 | "Host": {
20 | "commandName": "Project",
21 | "launchBrowser": true,
22 | "launchUrl": "http://localhost:5000/api/values",
23 | "environmentVariables": {
24 | "ASPNETCORE_ENVIRONMENT": "Development"
25 | }
26 | }
27 | }
28 | }
--------------------------------------------------------------------------------
/test/UnitOfWork.Tests/UnitOfWork.Tests.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | netcoreapp3.1
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 | all
13 | runtime; build; native; contentfiles; analyzers
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/src/UnitOfWork/IRepositoryFactory.cs:
--------------------------------------------------------------------------------
1 | //-----------------------------------------------------------------------
2 | //
3 | // Copyright (c) Arch team. All rights reserved.
4 | //
5 | //-----------------------------------------------------------------------
6 |
7 | namespace Arch.EntityFrameworkCore.UnitOfWork
8 | {
9 | ///
10 | /// Defines the interfaces for interfaces.
11 | ///
12 | public interface IRepositoryFactory
13 | {
14 | ///
15 | /// Gets the specified repository for the .
16 | ///
17 | /// True if providing custom repositry
18 | /// The type of the entity.
19 | /// An instance of type inherited from interface.
20 | IRepository GetRepository(bool hasCustomRepository = false) where TEntity : class;
21 | }
22 | }
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2016 love.net
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/src/UnitOfWork/IUnitOfWorkOfT.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Arch team. All rights reserved.
2 |
3 | using System.Threading.Tasks;
4 | using Microsoft.EntityFrameworkCore;
5 |
6 | namespace Arch.EntityFrameworkCore.UnitOfWork
7 | {
8 | ///
9 | /// Defines the interface(s) for generic unit of work.
10 | ///
11 | public interface IUnitOfWork : IUnitOfWork where TContext : DbContext {
12 | ///
13 | /// Gets the db context.
14 | ///
15 | /// The instance of type .
16 | TContext DbContext { get; }
17 |
18 | ///
19 | /// Saves all changes made in this context to the database with distributed transaction.
20 | ///
21 | /// True if save changes ensure auto record the change history.
22 | /// An optional array.
23 | /// A that represents the asynchronous save operation. The task result contains the number of state entities written to database.
24 | Task SaveChangesAsync(bool ensureAutoHistory = false, params IUnitOfWork[] unitOfWorks);
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/samples/UnitOfWork.Host/Models/BlogggingContext.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 | using Microsoft.EntityFrameworkCore;
3 |
4 | namespace Arch.EntityFrameworkCore.UnitOfWork.Host.Models
5 | {
6 | public class BloggingContext : DbContext
7 | {
8 | public BloggingContext(DbContextOptions options)
9 | : base(options)
10 | { }
11 |
12 | public DbSet Blogs { get; set; }
13 | public DbSet Posts { get; set; }
14 |
15 | protected override void OnModelCreating(ModelBuilder modelBuilder)
16 | {
17 | modelBuilder.EnableAutoHistory(null);
18 | }
19 | }
20 |
21 | public class Blog
22 | {
23 | public int Id { get; set; }
24 | public string Url { get; set; }
25 | public string Title { get; set; }
26 |
27 | public List Posts { get; set; }
28 | }
29 |
30 | public class Post
31 | {
32 | public int Id { get; set; }
33 | public string Title { get; set; }
34 | public string Content { get; set; }
35 |
36 | public List Comments { get; set; }
37 | }
38 |
39 | public class Comment
40 | {
41 | public int Id { get; set; }
42 | public string Title { get; set; }
43 | public string Content { get; set; }
44 | }
45 | }
--------------------------------------------------------------------------------
/src/UnitOfWork/UnitOfWork.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 | A plugin for Microsoft.EntityFrameworkCore to support repository, unit of work patterns, and multiple database with distributed transaction supported.
4 | 3.1.0
5 | rigofunc;rigofunc@outlook.com;
6 | netstandard2.0
7 | $(NoWarn);CS1591
8 | true
9 | true
10 | Microsoft.EntityFrameworkCore.UnitOfWork
11 | Microsoft.EntityFrameworkCore.UnitOfWork
12 | Entity Framework Core;entity-framework-core;EF;Data;O/RM;unitofwork;Unit Of Work;unit-of-work
13 | https://github.com/arch/UnitOfWork
14 | MIT
15 | git
16 | https://github.com/arch/UnitOfWork.git
17 | 3.1.16
18 | true
19 | snupkg
20 |
21 |
22 |
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/samples/UnitOfWork.Host/UnitOfWork.Host.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 | netcoreapp3.1
4 | true
5 | Exe
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/src/UnitOfWork/Collections/IPagedList.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Arch team. All rights reserved.
2 |
3 | using System.Collections.Generic;
4 |
5 | namespace Arch.EntityFrameworkCore.UnitOfWork.Collections
6 | {
7 | ///
8 | /// Provides the interface(s) for paged list of any type.
9 | ///
10 | /// The type for paging.
11 | public interface IPagedList
12 | {
13 | ///
14 | /// Gets the index start value.
15 | ///
16 | /// The index start value.
17 | int IndexFrom { get; }
18 | ///
19 | /// Gets the page index (current).
20 | ///
21 | int PageIndex { get; }
22 | ///
23 | /// Gets the page size.
24 | ///
25 | int PageSize { get; }
26 | ///
27 | /// Gets the total count of the list of type
28 | ///
29 | int TotalCount { get; }
30 | ///
31 | /// Gets the total pages.
32 | ///
33 | int TotalPages { get; }
34 | ///
35 | /// Gets the current page items.
36 | ///
37 | IList Items { get; }
38 | ///
39 | /// Gets the has previous page.
40 | ///
41 | /// The has previous page.
42 | bool HasPreviousPage { get; }
43 |
44 | ///
45 | /// Gets the has next page.
46 | ///
47 | /// The has next page.
48 | bool HasNextPage { get; }
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/test/UnitOfWork.Tests/IQueryablePageListExtensionsTests.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 | using System.Linq;
3 | using System.Threading.Tasks;
4 | using Arch.EntityFrameworkCore.UnitOfWork.Collections;
5 | using Arch.EntityFrameworkCore.UnitOfWork.Tests;
6 | using Arch.EntityFrameworkCore.UnitOfWork.Tests.Entities;
7 | using Xunit;
8 |
9 | namespace Arch.EntityFrameworkCore.UnitOfWork.Tests
10 | {
11 | public class IQueryablePageListExtensionsTests
12 | {
13 | [Fact]
14 | public async Task ToPagedListAsyncTest()
15 | {
16 | using (var db = new InMemoryContext())
17 | {
18 | var testItems = TestItems();
19 | await db.AddRangeAsync(testItems);
20 | db.SaveChanges();
21 |
22 | var items = db.Customers.Where(t => t.Age > 1);
23 |
24 | var page = await items.ToPagedListAsync(1, 2);
25 | Assert.NotNull(page);
26 |
27 | Assert.Equal(4, page.TotalCount);
28 | Assert.Equal(2, page.Items.Count);
29 | Assert.Equal("E", page.Items[0].Name);
30 |
31 | page = await items.ToPagedListAsync(0, 2);
32 | Assert.NotNull(page);
33 | Assert.Equal(4, page.TotalCount);
34 | Assert.Equal(2, page.Items.Count);
35 | Assert.Equal("C", page.Items[0].Name);
36 | }
37 | }
38 |
39 | public List TestItems()
40 | {
41 | return new List()
42 | {
43 | new Customer(){Name="A", Age=1},
44 | new Customer(){Name="B", Age=1},
45 | new Customer(){Name="C", Age=2},
46 | new Customer(){Name="D", Age=3},
47 | new Customer(){Name="E", Age=4},
48 | new Customer(){Name="F", Age=5},
49 | };
50 | }
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/samples/UnitOfWork.Host/Startup.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.AspNetCore.Builder;
2 | using Microsoft.AspNetCore.Hosting;
3 | using Arch.EntityFrameworkCore.UnitOfWork.Host.Models;
4 | using Microsoft.EntityFrameworkCore;
5 | using Microsoft.Extensions.Configuration;
6 | using Microsoft.Extensions.DependencyInjection;
7 | using Microsoft.Extensions.Logging;
8 | using Microsoft.Extensions.Hosting;
9 |
10 | namespace Arch.EntityFrameworkCore.UnitOfWork.Host
11 | {
12 | public class Startup
13 | {
14 | public Startup(IWebHostEnvironment env)
15 | {
16 | var builder = new ConfigurationBuilder()
17 | .SetBasePath(env.ContentRootPath)
18 | .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
19 | .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true);
20 |
21 | if (env.IsEnvironment("Development"))
22 | {
23 | }
24 |
25 | builder.AddEnvironmentVariables();
26 | Configuration = builder.Build();
27 | }
28 |
29 | public IConfigurationRoot Configuration { get; }
30 |
31 | // This method gets called by the runtime. Use this method to add services to the container
32 | public void ConfigureServices(IServiceCollection services)
33 | {
34 | // use in memory for testing.
35 | services
36 | .AddDbContext(opt => opt.UseMySql("Server=localhost;database=uow;uid=root;pwd=root1234;"))
37 | //.AddDbContext(opt => opt.UseInMemoryDatabase("UnitOfWork"))
38 | .AddUnitOfWork()
39 | .AddCustomRepository();
40 |
41 | services.AddMvc();
42 | }
43 |
44 | // This method gets called by the runtime. Use this method to configure the HTTP request pipeline
45 | public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ILoggingBuilder loggingBuilder)
46 | {
47 | loggingBuilder.AddConsole();
48 | loggingBuilder.AddDebug();
49 |
50 | app.UseEndpoints(endpoints => {
51 | endpoints.MapDefaultControllerRoute();
52 | });
53 | }
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/src/UnitOfWork/Collections/IQueryablePageListExtensions.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Linq;
3 | using System.Threading;
4 | using System.Threading.Tasks;
5 | using Microsoft.EntityFrameworkCore;
6 |
7 | namespace Arch.EntityFrameworkCore.UnitOfWork.Collections
8 | {
9 | public static class IQueryablePageListExtensions
10 | {
11 | ///
12 | /// Converts the specified source to by the specified and .
13 | ///
14 | /// The type of the source.
15 | /// The source to paging.
16 | /// The index of the page.
17 | /// The size of the page.
18 | ///
19 | /// A to observe while waiting for the task to complete.
20 | ///
21 | /// The start index value.
22 | /// An instance of the inherited from interface.
23 | public static async Task> ToPagedListAsync(this IQueryable source, int pageIndex, int pageSize, int indexFrom = 0, CancellationToken cancellationToken = default(CancellationToken))
24 | {
25 | if (indexFrom > pageIndex)
26 | {
27 | throw new ArgumentException($"indexFrom: {indexFrom} > pageIndex: {pageIndex}, must indexFrom <= pageIndex");
28 | }
29 |
30 | var count = await source.CountAsync(cancellationToken).ConfigureAwait(false);
31 | var items = await source.Skip((pageIndex - indexFrom) * pageSize)
32 | .Take(pageSize).ToListAsync(cancellationToken).ConfigureAwait(false);
33 |
34 | var pagedList = new PagedList()
35 | {
36 | PageIndex = pageIndex,
37 | PageSize = pageSize,
38 | IndexFrom = indexFrom,
39 | TotalCount = count,
40 | Items = items,
41 | TotalPages = (int)Math.Ceiling(count / (double)pageSize)
42 | };
43 |
44 | return pagedList;
45 | }
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/src/UnitOfWork/Collections/IEnumerablePagedListExtensions.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Arch team. All rights reserved.
2 |
3 | using System;
4 | using System.Collections.Generic;
5 |
6 | namespace Arch.EntityFrameworkCore.UnitOfWork.Collections
7 | {
8 | ///
9 | /// Provides some extension methods for to provide paging capability.
10 | ///
11 | public static class IEnumerablePagedListExtensions
12 | {
13 | ///
14 | /// Converts the specified source to by the specified and .
15 | ///
16 | /// The type of the source.
17 | /// The source to paging.
18 | /// The index of the page.
19 | /// The size of the page.
20 | /// The start index value.
21 | /// An instance of the inherited from interface.
22 | public static IPagedList ToPagedList(this IEnumerable source, int pageIndex, int pageSize, int indexFrom = 0) => new PagedList(source, pageIndex, pageSize, indexFrom);
23 |
24 | ///
25 | /// Converts the specified source to by the specified , and
26 | ///
27 | /// The type of the source.
28 | /// The type of the result
29 | /// The source to convert.
30 | /// The converter to change the to .
31 | /// The page index.
32 | /// The page size.
33 | /// The start index value.
34 | /// An instance of the inherited from interface.
35 | public static IPagedList ToPagedList(this IEnumerable source, Func, IEnumerable> converter, int pageIndex, int pageSize, int indexFrom = 0) => new PagedList(source, converter, pageIndex, pageSize, indexFrom);
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | ###############################################################################
2 | # Set default behavior to automatically normalize line endings.
3 | ###############################################################################
4 | * text=auto
5 |
6 | ###############################################################################
7 | # Set default behavior for command prompt diff.
8 | #
9 | # This is need for earlier builds of msysgit that does not have it on by
10 | # default for csharp files.
11 | # Note: This is only used by command line
12 | ###############################################################################
13 | #*.cs diff=csharp
14 |
15 | ###############################################################################
16 | # Set the merge driver for project and solution files
17 | #
18 | # Merging from the command prompt will add diff markers to the files if there
19 | # are conflicts (Merging from VS is not affected by the settings below, in VS
20 | # the diff markers are never inserted). Diff markers may cause the following
21 | # file extensions to fail to load in VS. An alternative would be to treat
22 | # these files as binary and thus will always conflict and require user
23 | # intervention with every merge. To do so, just uncomment the entries below
24 | ###############################################################################
25 | #*.sln merge=binary
26 | #*.csproj merge=binary
27 | #*.vbproj merge=binary
28 | #*.vcxproj merge=binary
29 | #*.vcproj merge=binary
30 | #*.dbproj merge=binary
31 | #*.fsproj merge=binary
32 | #*.lsproj merge=binary
33 | #*.wixproj merge=binary
34 | #*.modelproj merge=binary
35 | #*.sqlproj merge=binary
36 | #*.wwaproj merge=binary
37 |
38 | ###############################################################################
39 | # behavior for image files
40 | #
41 | # image files are treated as binary by default.
42 | ###############################################################################
43 | #*.jpg binary
44 | #*.png binary
45 | #*.gif binary
46 |
47 | ###############################################################################
48 | # diff behavior for common document formats
49 | #
50 | # Convert binary document formats to text before diffing them. This feature
51 | # is only available from the command line. Turn it on by uncommenting the
52 | # entries below.
53 | ###############################################################################
54 | #*.doc diff=astextplain
55 | #*.DOC diff=astextplain
56 | #*.docx diff=astextplain
57 | #*.DOCX diff=astextplain
58 | #*.dot diff=astextplain
59 | #*.DOT diff=astextplain
60 | #*.pdf diff=astextplain
61 | #*.PDF diff=astextplain
62 | #*.rtf diff=astextplain
63 | #*.RTF diff=astextplain
64 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | # EditorConfig is awesome:http://EditorConfig.org
2 |
3 | # top-most EditorConfig file
4 | root = true
5 |
6 | # Don't use tabs for indentation.
7 | [*]
8 | indent_style = space
9 | # (Please don't specify an indent_size here; that has too many unintended consequences.)
10 |
11 | # Code files
12 | [*.{cs,csx,vb,vbx}]
13 | indent_size = 4
14 |
15 | # Xml project files
16 | [*.{csproj,vbproj,vcxproj,vcxproj.filters,proj,projitems,shproj}]
17 | indent_size = 2
18 |
19 | # Xml config files
20 | [*.{props,targets,ruleset,config,nuspec,resx,vsixmanifest,vsct}]
21 | indent_size = 2
22 |
23 | # JSON files
24 | [*.json]
25 | indent_size = 2
26 |
27 | # Dotnet code style settings:
28 | [*.{cs,vb}]
29 | # Sort using and Import directives with System.* appearing first
30 | dotnet_sort_system_directives_first = true
31 | # Avoid "this." and "Me." if not necessary
32 | dotnet_style_qualification_for_field = false:suggestion
33 | dotnet_style_qualification_for_property = false:suggestion
34 | dotnet_style_qualification_for_method = false:suggestion
35 | dotnet_style_qualification_for_event = false:suggestion
36 |
37 | # Use language keywords instead of framework type names for type references
38 | dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion
39 | dotnet_style_predefined_type_for_member_access = true:suggestion
40 |
41 | # Suggest more modern language features when available
42 | dotnet_style_object_initializer = true:suggestion
43 | dotnet_style_collection_initializer = true:suggestion
44 | dotnet_style_coalesce_expression = true:suggestion
45 | dotnet_style_null_propagation = true:suggestion
46 | dotnet_style_explicit_tuple_names = true:suggestion
47 |
48 | # CSharp code style settings:
49 | [*.cs]
50 | # Prefer "var" everywhere
51 | csharp_style_var_for_built_in_types = true:suggestion
52 | csharp_style_var_when_type_is_apparent = true:suggestion
53 | csharp_style_var_elsewhere = true:suggestion
54 |
55 | # Prefer method-like constructs to have a block body
56 | csharp_style_expression_bodied_methods = true:suggestion
57 | csharp_style_expression_bodied_constructors = true:suggestion
58 | csharp_style_expression_bodied_operators = true:suggestion
59 |
60 | # Prefer property-like constructs to have an expression-body
61 | csharp_style_expression_bodied_properties = true:suggestion
62 | csharp_style_expression_bodied_indexers = true:suggestion
63 | csharp_style_expression_bodied_accessors = true:suggestion
64 |
65 | # Suggest more modern language features when available
66 | csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion
67 | csharp_style_pattern_matching_over_as_with_null_check = true:suggestion
68 | csharp_style_inlined_variable_declaration = true:suggestion
69 | csharp_style_throw_expression = true:suggestion
70 | csharp_style_conditional_delegate_call = true:suggestion
71 |
72 | # Newline settings
73 | csharp_new_line_before_open_brace = all
74 | csharp_new_line_before_else = true
75 | csharp_new_line_before_catch = true
76 | csharp_new_line_before_finally = true
77 | csharp_new_line_before_members_in_object_initializers = true
78 | csharp_new_line_before_members_in_anonymous_types = true
--------------------------------------------------------------------------------
/test/UnitOfWork.Tests/TestGetFirstOrDefaultAsync.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 | using System.Linq;
3 | using Arch.EntityFrameworkCore.UnitOfWork.Tests.Entities;
4 | using Microsoft.EntityFrameworkCore;
5 | using Xunit;
6 |
7 | namespace Arch.EntityFrameworkCore.UnitOfWork.Tests
8 | {
9 | public class TestGetFirstOrDefaultAsync
10 | {
11 | private static readonly InMemoryContext db;
12 |
13 | static TestGetFirstOrDefaultAsync()
14 | {
15 | db = new InMemoryContext();
16 | if (db.Countries.Any() == false)
17 | {
18 | db.AddRange(TestCountries);
19 | db.AddRange(TestCities);
20 | db.AddRange(TestTowns);
21 | db.SaveChanges();
22 | }
23 | }
24 |
25 |
26 | [Fact]
27 | public async void TestGetFirstOrDefaultAsyncGetsCorrectItem()
28 | {
29 | var repository = new Repository(db);
30 | var city = await repository.GetFirstOrDefaultAsync(predicate: t => t.Name == "A");
31 | Assert.NotNull(city);
32 | Assert.Equal(1, city.Id);
33 | }
34 |
35 | [Fact]
36 | public async void TestGetFirstOrDefaultAsyncReturnsNullValue()
37 | {
38 | var repository = new Repository(db);
39 | var city = await repository.GetFirstOrDefaultAsync(predicate: t => t.Name == "Easy-E");
40 | Assert.Null(city);
41 | }
42 |
43 | [Fact]
44 | public async void TestGetFirstOrDefaultAsyncCanInclude()
45 | {
46 | var repository = new Repository(db);
47 | var city = await repository.GetFirstOrDefaultAsync(
48 | predicate: c => c.Name == "A",
49 | include: source => source.Include(t => t.Towns));
50 | Assert.NotNull(city);
51 | Assert.NotNull(city.Towns);
52 | }
53 |
54 |
55 | protected static List TestCountries => new List
56 | {
57 | new Country {Id = 1, Name = "A"},
58 | new Country {Id = 2, Name = "B"}
59 | };
60 |
61 | public static List TestCities => new List
62 | {
63 | new City { Id = 1, Name = "A", CountryId = 1},
64 | new City { Id = 2, Name = "B", CountryId = 2},
65 | new City { Id = 3, Name = "C", CountryId = 1},
66 | new City { Id = 4, Name = "D", CountryId = 2},
67 | new City { Id = 5, Name = "E", CountryId = 1},
68 | new City { Id = 6, Name = "F", CountryId = 2},
69 | };
70 |
71 | public static List TestTowns => new List
72 | {
73 | new Town { Id = 1, Name="TownA", CityId = 1 },
74 | new Town { Id = 2, Name="TownB", CityId = 2 },
75 | new Town { Id = 3, Name="TownC", CityId = 3 },
76 | new Town { Id = 4, Name="TownD", CityId = 4 },
77 | new Town { Id = 5, Name="TownE", CityId = 5 },
78 | new Town { Id = 6, Name="TownF", CityId = 6 },
79 | };
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/UnitOfWork.sln:
--------------------------------------------------------------------------------
1 |
2 | Microsoft Visual Studio Solution File, Format Version 12.00
3 | # Visual Studio Version 16
4 | VisualStudioVersion = 16.0.29609.76
5 | MinimumVisualStudioVersion = 10.0.40219.1
6 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{7134BBDC-D68D-4FD4-85B5-5A39EDD5BFD9}"
7 | ProjectSection(SolutionItems) = preProject
8 | README.md = README.md
9 | EndProjectSection
10 | EndProject
11 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{14F86AF9-D2C9-468D-8C71-9BC4785F68A0}"
12 | EndProject
13 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{D3C7F3F5-4AB2-4868-8D2C-516DFCFDC349}"
14 | EndProject
15 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{9661083F-4BCD-42D4-9EF8-1187DD9EDA60}"
16 | EndProject
17 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "UnitOfWork", "src\UnitOfWork\UnitOfWork.csproj", "{FD8010C2-982B-470E-A9A5-C0DFA2D14809}"
18 | EndProject
19 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "UnitOfWork.Tests", "test\UnitOfWork.Tests\UnitOfWork.Tests.csproj", "{FDAA4086-21E8-4939-8539-0A1723EC4922}"
20 | EndProject
21 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "UnitOfWork.Host", "samples\UnitOfWork.Host\UnitOfWork.Host.csproj", "{207188FE-2344-4DC1-8656-E02CEF741422}"
22 | EndProject
23 | Global
24 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
25 | Debug|Any CPU = Debug|Any CPU
26 | Release|Any CPU = Release|Any CPU
27 | EndGlobalSection
28 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
29 | {FD8010C2-982B-470E-A9A5-C0DFA2D14809}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
30 | {FD8010C2-982B-470E-A9A5-C0DFA2D14809}.Debug|Any CPU.Build.0 = Debug|Any CPU
31 | {FD8010C2-982B-470E-A9A5-C0DFA2D14809}.Release|Any CPU.ActiveCfg = Release|Any CPU
32 | {FD8010C2-982B-470E-A9A5-C0DFA2D14809}.Release|Any CPU.Build.0 = Release|Any CPU
33 | {FDAA4086-21E8-4939-8539-0A1723EC4922}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
34 | {FDAA4086-21E8-4939-8539-0A1723EC4922}.Debug|Any CPU.Build.0 = Debug|Any CPU
35 | {FDAA4086-21E8-4939-8539-0A1723EC4922}.Release|Any CPU.ActiveCfg = Release|Any CPU
36 | {FDAA4086-21E8-4939-8539-0A1723EC4922}.Release|Any CPU.Build.0 = Release|Any CPU
37 | {207188FE-2344-4DC1-8656-E02CEF741422}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
38 | {207188FE-2344-4DC1-8656-E02CEF741422}.Debug|Any CPU.Build.0 = Debug|Any CPU
39 | {207188FE-2344-4DC1-8656-E02CEF741422}.Release|Any CPU.ActiveCfg = Release|Any CPU
40 | {207188FE-2344-4DC1-8656-E02CEF741422}.Release|Any CPU.Build.0 = Release|Any CPU
41 | EndGlobalSection
42 | GlobalSection(SolutionProperties) = preSolution
43 | HideSolutionNode = FALSE
44 | EndGlobalSection
45 | GlobalSection(NestedProjects) = preSolution
46 | {FD8010C2-982B-470E-A9A5-C0DFA2D14809} = {14F86AF9-D2C9-468D-8C71-9BC4785F68A0}
47 | {FDAA4086-21E8-4939-8539-0A1723EC4922} = {D3C7F3F5-4AB2-4868-8D2C-516DFCFDC349}
48 | {207188FE-2344-4DC1-8656-E02CEF741422} = {9661083F-4BCD-42D4-9EF8-1187DD9EDA60}
49 | EndGlobalSection
50 | GlobalSection(ExtensibilityGlobals) = postSolution
51 | SolutionGuid = {A7B21B30-8D65-449B-B192-64726743EFC3}
52 | EndGlobalSection
53 | EndGlobal
54 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 | # UnitOfWork
3 | A plugin for Microsoft.EntityFrameworkCore to support repository, unit of work patterns, and multiple database with distributed transaction supported.
4 |
5 | ## Support MySQL multiple databases/tables sharding
6 |
7 | > In MySQL, physically, a schema is synonymous with a database. You can substitute the keyword SCHEMA instead of DATABASE in MySQL SQL syntax, for example using CREATE SCHEMA instead of CREATE DATABASE. Some other database products draw a distinction. For example, in the Oracle Database product, a schema represents only a part of a database: the tables and other objects owned by a single user.
8 |
9 | So, for MySQL, the easy way to support this feature is to dynamically change the SCHEMA at the runtime.
10 |
11 | After [v1.1.2](https://www.nuget.org/packages/Microsoft.EntityFrameworkCore.UnitOfWork/1.1.2) had support MySQL multiple databases/tables sharding in the same model in the same machine. For different machine, you can use DbContextFactory to dynamically create DbContext.
12 | You can use [Pomelo.EntityFrameworkCore.MySql](https://www.nuget.org/packages/Pomelo.EntityFrameworkCore.MySql) to test this feature. @[PomeloFoundation](https://github.com/PomeloFoundation)
13 |
14 | # Quickly start
15 |
16 | ## How to use UnitOfWork
17 |
18 | ```csharp
19 | public void ConfigureServices(IServiceCollection services)
20 | {
21 | // use in memory for testing.
22 | services
23 | .AddDbContext(opt => opt.UseInMemoryDatabase())
24 | .AddUnitOfWork()
25 | .AddCustomRepository();
26 | }
27 |
28 | private readonly IUnitOfWork _unitOfWork;
29 |
30 | // 1. IRepositoryFactory used for readonly scenario;
31 | // 2. IUnitOfWork used for read/write scenario;
32 | // 3. IUnitOfWork used for multiple databases scenario;
33 | public ValuesController(IUnitOfWork unitOfWork)
34 | {
35 | _unitOfWork = unitOfWork;
36 |
37 | // Change database only work for MySQL right now.
38 | unitOfWork.ChangeDatabase($"uow_db_{DateTime.Now.Year}");
39 |
40 | var userRepo = unitOfWork.GetRepository();
41 | var postRepo = unitOfWork.GetRepository();
42 |
43 | var ym = DateTime.Now.ToString("yyyyMM");
44 |
45 | userRepo.ChangeTable($"user_{ym}");
46 | postRepo.ChangeTable($"post_{ym}");
47 |
48 | var user = new User
49 | {
50 | UserName = "rigofunc",
51 | Password = "password"
52 | };
53 |
54 | userRepo.Insert(user);
55 |
56 | var post = new Post
57 | {
58 | UserId = user.UserId,
59 | Content = "What a piece of junk!"
60 | };
61 |
62 | postRepo.Insert(post);
63 |
64 | unitOfWork.SaveChanges();
65 |
66 | var find = userRepo.Find(user.UserId);
67 |
68 | find.Password = "p@ssword";
69 |
70 | unitOfWork.SaveChanges();
71 |
72 | // projection
73 |
74 | }
75 | ```
76 |
77 | ## Projection & Including
78 |
79 | ```csharp
80 | // projection
81 | var pagedList = _unitOfWork.GetRepository().GetPagedList(b => new { Name = b.Title, Link = b.Url }, pageIndex: pageIndex, pageSize: pageSize);
82 | var projection = _unitOfWork.GetRepository().GetFirstOrDefault(b => new { Name = b.Title, Link = b.Url }, predicate: x => x.Title.Contains(term));
83 |
84 | // including
85 | [HttpGet]
86 | public async Task> Get()
87 | {
88 | return await _unitOfWork.GetRepository().GetPagedListAsync(include: source => source.Include(blog => blog.Posts).ThenInclude(post => post.Comments));
89 | }
90 | ```
91 |
--------------------------------------------------------------------------------
/src/UnitOfWork/IUnitOfWork.cs:
--------------------------------------------------------------------------------
1 | //-----------------------------------------------------------------------
2 | //
3 | // Copyright (c) Arch team. All rights reserved.
4 | //
5 | //-----------------------------------------------------------------------
6 |
7 | namespace Arch.EntityFrameworkCore.UnitOfWork
8 | {
9 | using System;
10 | using System.Linq;
11 | using System.Threading.Tasks;
12 | using Microsoft.EntityFrameworkCore.ChangeTracking;
13 |
14 | ///
15 | /// Defines the interface(s) for unit of work.
16 | ///
17 | public interface IUnitOfWork : IDisposable
18 | {
19 | ///
20 | /// Changes the database name. This require the databases in the same machine. NOTE: This only work for MySQL right now.
21 | ///
22 | /// The database name.
23 | ///
24 | /// This only been used for supporting multiple databases in the same model. This require the databases in the same machine.
25 | ///
26 | void ChangeDatabase(string database);
27 |
28 | ///
29 | /// Gets the specified repository for the .
30 | ///
31 | /// True if providing custom repositry
32 | /// The type of the entity.
33 | /// An instance of type inherited from interface.
34 | IRepository GetRepository(bool hasCustomRepository = false) where TEntity : class;
35 |
36 | ///
37 | /// Saves all changes made in this context to the database.
38 | ///
39 | /// True if sayve changes ensure auto record the change history.
40 | /// The number of state entries written to the database.
41 | int SaveChanges(bool ensureAutoHistory = false);
42 |
43 | ///
44 | /// Asynchronously saves all changes made in this unit of work to the database.
45 | ///
46 | /// True if save changes ensure auto record the change history.
47 | /// A that represents the asynchronous save operation. The task result contains the number of state entities written to database.
48 | Task SaveChangesAsync(bool ensureAutoHistory = false);
49 |
50 | ///
51 | /// Executes the specified raw SQL command.
52 | ///
53 | /// The raw SQL.
54 | /// The parameters.
55 | /// The number of state entities written to database.
56 | int ExecuteSqlCommand(string sql, params object[] parameters);
57 |
58 | ///
59 | /// Uses raw SQL queries to fetch the specified data.
60 | ///
61 | /// The type of the entity.
62 | /// The raw SQL.
63 | /// The parameters.
64 | /// An that contains elements that satisfy the condition specified by raw SQL.
65 | IQueryable FromSql(string sql, params object[] parameters) where TEntity : class;
66 |
67 | ///
68 | /// Uses TrakGrap Api to attach disconnected entities
69 | ///
70 | /// Root entity
71 | /// Delegate to convert Object's State properities to Entities entry state.
72 | void TrackGraph(object rootEntity, Action callback);
73 | }
74 | }
--------------------------------------------------------------------------------
/test/UnitOfWork.Tests/IRepositoryGetPagedListTest.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 | using System.Threading.Tasks;
3 | using Arch.EntityFrameworkCore.UnitOfWork.Tests.Entities;
4 | using Microsoft.EntityFrameworkCore;
5 | using Xunit;
6 |
7 | namespace Arch.EntityFrameworkCore.UnitOfWork.Tests
8 | {
9 | public class IRepositoryGetPagedListTest
10 | {
11 | private static readonly InMemoryContext db;
12 |
13 | static IRepositoryGetPagedListTest()
14 | {
15 | db = new InMemoryContext();
16 |
17 | db.AddRange(TestCountries);
18 | db.AddRange(TestCities);
19 | db.AddRange(TestTowns);
20 |
21 | db.SaveChanges();
22 | }
23 |
24 | [Fact]
25 | public void GetPagedList()
26 | {
27 | var repository = new Repository(db);
28 |
29 | var page = repository.GetPagedList(predicate: t => t.Name == "C", include: source => source.Include(t => t.Country), pageSize: 1);
30 |
31 | Assert.Equal(1, page.Items.Count);
32 | Assert.NotNull(page.Items[0].Country);
33 |
34 | Assert.Equal(page.Items[0].CountryId, page.Items[0].Country.Id);
35 | Assert.Equal("A", page.Items[0].Country.Name);
36 | Assert.Equal(1, page.Items[0].Country.Id);
37 | }
38 |
39 | [Fact]
40 | public async Task GetPagedListAsync()
41 | {
42 | var repository = new Repository(db);
43 |
44 | var page = await repository.GetPagedListAsync(predicate: t => t.Name == "C", include: source => source.Include(t => t.Country), pageSize: 1);
45 |
46 | Assert.Equal(1, page.Items.Count);
47 | Assert.NotNull(page.Items[0].Country);
48 |
49 | Assert.Equal(page.Items[0].CountryId, page.Items[0].Country.Id);
50 | Assert.Equal("A", page.Items[0].Country.Name);
51 | Assert.Equal(1, page.Items[0].Country.Id);
52 | }
53 |
54 | [Fact]
55 | public async Task GetPagedListWithIncludingMultipleLevelsAsync()
56 | {
57 | var repository = new Repository(db);
58 |
59 | var page = await repository.GetPagedListAsync(predicate: t => t.Name == "A", include: country => country.Include(c => c.Cities).ThenInclude(city => city.Towns), pageSize: 1);
60 |
61 | Assert.Equal(1, page.Items.Count);
62 | Assert.NotNull(page.Items[0].Cities);
63 |
64 | Assert.NotNull(page.Items[0].Cities[0].Towns);
65 | }
66 |
67 | [Fact]
68 | public void GetPagedListWithoutInclude()
69 | {
70 | var repository = new Repository(db);
71 |
72 | var page = repository.GetPagedList(pageIndex: 0, pageSize: 1);
73 |
74 | Assert.Equal(1, page.Items.Count);
75 | Assert.Null(page.Items[0].Country);
76 | }
77 |
78 | protected static List TestCountries => new List
79 | {
80 | new Country {Id = 1, Name = "A"},
81 | new Country {Id = 2, Name = "B"}
82 | };
83 |
84 | public static List TestCities => new List
85 | {
86 | new City { Id = 1, Name = "A", CountryId = 1},
87 | new City { Id = 2, Name = "B", CountryId = 2},
88 | new City { Id = 3, Name = "C", CountryId = 1},
89 | new City { Id = 4, Name = "D", CountryId = 2},
90 | new City { Id = 5, Name = "E", CountryId = 1},
91 | new City { Id = 6, Name = "F", CountryId = 2},
92 | };
93 |
94 | public static List TestTowns => new List
95 | {
96 | new Town { Id = 1, Name="A", CityId = 1 },
97 | new Town { Id = 2, Name="B", CityId = 2 },
98 | new Town { Id = 3, Name="C", CityId = 3 },
99 | new Town { Id = 4, Name="D", CityId = 4 },
100 | new Town { Id = 5, Name="E", CityId = 5 },
101 | new Town { Id = 6, Name="F", CityId = 6 },
102 | };
103 | }
104 | }
105 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | ## Ignore Visual Studio temporary files, build results, and
2 | ## files generated by popular Visual Studio add-ons.
3 |
4 | # vscode
5 | .vscode
6 |
7 | # User-specific files
8 | *.suo
9 | *.user
10 | *.userosscache
11 | *.sln.docstates
12 |
13 | # User-specific files (MonoDevelop/Xamarin Studio)
14 | *.userprefs
15 |
16 | # Build results
17 | [Dd]ebug/
18 | [Dd]ebugPublic/
19 | [Rr]elease/
20 | [Rr]eleases/
21 | x64/
22 | x86/
23 | bld/
24 | [Bb]in/
25 | [Oo]bj/
26 | [Ll]og/
27 |
28 | # Visual Studio 2015 cache/options directory
29 | .vs/
30 | # Uncomment if you have tasks that create the project's static files in wwwroot
31 | #wwwroot/
32 |
33 | # MSTest test Results
34 | [Tt]est[Rr]esult*/
35 | [Bb]uild[Ll]og.*
36 |
37 | # NUNIT
38 | *.VisualState.xml
39 | TestResult.xml
40 |
41 | # Build Results of an ATL Project
42 | [Dd]ebugPS/
43 | [Rr]eleasePS/
44 | dlldata.c
45 |
46 | # DNX
47 | project.lock.json
48 | artifacts/
49 |
50 | *_i.c
51 | *_p.c
52 | *_i.h
53 | *.ilk
54 | *.meta
55 | *.obj
56 | *.pch
57 | *.pdb
58 | *.pgc
59 | *.pgd
60 | *.rsp
61 | *.sbr
62 | *.tlb
63 | *.tli
64 | *.tlh
65 | *.tmp
66 | *.tmp_proj
67 | *.log
68 | *.vspscc
69 | *.vssscc
70 | .builds
71 | *.pidb
72 | *.svclog
73 | *.scc
74 |
75 | # Chutzpah Test files
76 | _Chutzpah*
77 |
78 | # Visual C++ cache files
79 | ipch/
80 | *.aps
81 | *.ncb
82 | *.opendb
83 | *.opensdf
84 | *.sdf
85 | *.cachefile
86 | *.VC.db
87 | *.VC.VC.opendb
88 |
89 | # Visual Studio profiler
90 | *.psess
91 | *.vsp
92 | *.vspx
93 | *.sap
94 |
95 | # TFS 2012 Local Workspace
96 | $tf/
97 |
98 | # Guidance Automation Toolkit
99 | *.gpState
100 |
101 | # ReSharper is a .NET coding add-in
102 | _ReSharper*/
103 | *.[Rr]e[Ss]harper
104 | *.DotSettings.user
105 |
106 | # JustCode is a .NET coding add-in
107 | .JustCode
108 |
109 | # TeamCity is a build add-in
110 | _TeamCity*
111 |
112 | # DotCover is a Code Coverage Tool
113 | *.dotCover
114 |
115 | # NCrunch
116 | _NCrunch_*
117 | .*crunch*.local.xml
118 | nCrunchTemp_*
119 |
120 | # MightyMoose
121 | *.mm.*
122 | AutoTest.Net/
123 |
124 | # Web workbench (sass)
125 | .sass-cache/
126 |
127 | # Installshield output folder
128 | [Ee]xpress/
129 |
130 | # DocProject is a documentation generator add-in
131 | DocProject/buildhelp/
132 | DocProject/Help/*.HxT
133 | DocProject/Help/*.HxC
134 | DocProject/Help/*.hhc
135 | DocProject/Help/*.hhk
136 | DocProject/Help/*.hhp
137 | DocProject/Help/Html2
138 | DocProject/Help/html
139 |
140 | # Click-Once directory
141 | publish/
142 |
143 | # Publish Web Output
144 | *.[Pp]ublish.xml
145 | *.azurePubxml
146 | # TODO: Comment the next line if you want to checkin your web deploy settings
147 | # but database connection strings (with potential passwords) will be unencrypted
148 | *.pubxml
149 | *.publishproj
150 |
151 | # Microsoft Azure Web App publish settings. Comment the next line if you want to
152 | # checkin your Azure Web App publish settings, but sensitive information contained
153 | # in these scripts will be unencrypted
154 | PublishScripts/
155 |
156 | # NuGet Packages
157 | *.nupkg
158 | # The packages folder can be ignored because of Package Restore
159 | **/packages/*
160 | # except build/, which is used as an MSBuild target.
161 | !**/packages/build/
162 | # Uncomment if necessary however generally it will be regenerated when needed
163 | #!**/packages/repositories.config
164 | # NuGet v3's project.json files produces more ignoreable files
165 | *.nuget.props
166 | *.nuget.targets
167 |
168 | # Microsoft Azure Build Output
169 | csx/
170 | *.build.csdef
171 |
172 | # Microsoft Azure Emulator
173 | ecf/
174 | rcf/
175 |
176 | # Windows Store app package directories and files
177 | AppPackages/
178 | BundleArtifacts/
179 | Package.StoreAssociation.xml
180 | _pkginfo.txt
181 |
182 | # Visual Studio cache files
183 | # files ending in .cache can be ignored
184 | *.[Cc]ache
185 | # but keep track of directories ending in .cache
186 | !*.[Cc]ache/
187 |
188 | # Others
189 | ClientBin/
190 | ~$*
191 | *~
192 | *.dbmdl
193 | *.dbproj.schemaview
194 | *.pfx
195 | *.publishsettings
196 | node_modules/
197 | orleans.codegen.cs
198 |
199 | # Since there are multiple workflows, uncomment next line to ignore bower_components
200 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
201 | #bower_components/
202 |
203 | # RIA/Silverlight projects
204 | Generated_Code/
205 |
206 | # Backup & report files from converting an old project file
207 | # to a newer Visual Studio version. Backup files are not needed,
208 | # because we have git ;-)
209 | _UpgradeReport_Files/
210 | Backup*/
211 | UpgradeLog*.XML
212 | UpgradeLog*.htm
213 |
214 | # SQL Server files
215 | *.mdf
216 | *.ldf
217 |
218 | # Business Intelligence projects
219 | *.rdl.data
220 | *.bim.layout
221 | *.bim_*.settings
222 |
223 | # Microsoft Fakes
224 | FakesAssemblies/
225 |
226 | # GhostDoc plugin setting file
227 | *.GhostDoc.xml
228 |
229 | # Node.js Tools for Visual Studio
230 | .ntvs_analysis.dat
231 |
232 | # Visual Studio 6 build log
233 | *.plg
234 |
235 | # Visual Studio 6 workspace options file
236 | *.opt
237 |
238 | # Visual Studio LightSwitch build output
239 | **/*.HTMLClient/GeneratedArtifacts
240 | **/*.DesktopClient/GeneratedArtifacts
241 | **/*.DesktopClient/ModelManifest.xml
242 | **/*.Server/GeneratedArtifacts
243 | **/*.Server/ModelManifest.xml
244 | _Pvt_Extensions
245 |
246 | # Paket dependency manager
247 | .paket/paket.exe
248 | paket-files/
249 |
250 | # FAKE - F# Make
251 | .fake/
252 |
253 | # JetBrains Rider
254 | .idea/
255 | *.sln.iml
256 |
--------------------------------------------------------------------------------
/src/UnitOfWork/UnitOfWorkServiceCollectionExtensions.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Arch team. All rights reserved.
2 |
3 | using Microsoft.EntityFrameworkCore;
4 | using Microsoft.Extensions.DependencyInjection;
5 |
6 | namespace Arch.EntityFrameworkCore.UnitOfWork
7 | {
8 | ///
9 | /// Extension methods for setting up unit of work related services in an .
10 | ///
11 | public static class UnitOfWorkServiceCollectionExtensions
12 | {
13 | ///
14 | /// Registers the unit of work given context as a service in the .
15 | ///
16 | /// The type of the db context.
17 | /// The to add services to.
18 | /// The same service collection so that multiple calls can be chained.
19 | ///
20 | /// This method only support one db context, if been called more than once, will throw exception.
21 | ///
22 | public static IServiceCollection AddUnitOfWork(this IServiceCollection services) where TContext : DbContext
23 | {
24 | services.AddScoped>();
25 | // Following has a issue: IUnitOfWork cannot support multiple dbcontext/database,
26 | // that means cannot call AddUnitOfWork multiple times.
27 | // Solution: check IUnitOfWork whether or null
28 | services.AddScoped>();
29 | services.AddScoped, UnitOfWork>();
30 |
31 | return services;
32 | }
33 |
34 | ///
35 | /// Registers the unit of work given context as a service in the .
36 | ///
37 | /// The type of the db context.
38 | /// The type of the db context.
39 | /// The to add services to.
40 | /// The same service collection so that multiple calls can be chained.
41 | ///
42 | /// This method only support one db context, if been called more than once, will throw exception.
43 | ///
44 | public static IServiceCollection AddUnitOfWork(this IServiceCollection services)
45 | where TContext1 : DbContext
46 | where TContext2 : DbContext
47 | {
48 | services.AddScoped, UnitOfWork>();
49 | services.AddScoped, UnitOfWork>();
50 |
51 | return services;
52 | }
53 |
54 | ///
55 | /// Registers the unit of work given context as a service in the .
56 | ///
57 | /// The type of the db context.
58 | /// The type of the db context.
59 | /// The type of the db context.
60 | /// The to add services to.
61 | /// The same service collection so that multiple calls can be chained.
62 | ///
63 | /// This method only support one db context, if been called more than once, will throw exception.
64 | ///
65 | public static IServiceCollection AddUnitOfWork(this IServiceCollection services)
66 | where TContext1 : DbContext
67 | where TContext2 : DbContext
68 | where TContext3 : DbContext
69 | {
70 | services.AddScoped, UnitOfWork>();
71 | services.AddScoped, UnitOfWork>();
72 | services.AddScoped, UnitOfWork>();
73 |
74 | return services;
75 | }
76 |
77 | ///
78 | /// Registers the unit of work given context as a service in the .
79 | ///
80 | /// The type of the db context.
81 | /// The type of the db context.
82 | /// The type of the db context.
83 | /// The type of the db context.
84 | /// The to add services to.
85 | /// The same service collection so that multiple calls can be chained.
86 | ///
87 | /// This method only support one db context, if been called more than once, will throw exception.
88 | ///
89 | public static IServiceCollection AddUnitOfWork(this IServiceCollection services)
90 | where TContext1 : DbContext
91 | where TContext2 : DbContext
92 | where TContext3 : DbContext
93 | where TContext4 : DbContext
94 | {
95 | services.AddScoped, UnitOfWork>();
96 | services.AddScoped, UnitOfWork>();
97 | services.AddScoped, UnitOfWork>();
98 | services.AddScoped, UnitOfWork>();
99 |
100 | return services;
101 | }
102 |
103 | ///
104 | /// Registers the custom repository as a service in the .
105 | ///
106 | /// The type of the entity.
107 | /// The type of the custom repositry.
108 | /// The to add services to.
109 | /// The same service collection so that multiple calls can be chained.
110 | public static IServiceCollection AddCustomRepository(this IServiceCollection services)
111 | where TEntity : class
112 | where TRepository : class, IRepository
113 | {
114 | services.AddScoped, TRepository>();
115 |
116 | return services;
117 | }
118 | }
119 | }
120 |
--------------------------------------------------------------------------------
/src/UnitOfWork/UnitOfWork.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Arch team. All rights reserved.
2 |
3 | using System;
4 | using System.Collections.Generic;
5 | using System.Data;
6 | using System.Linq;
7 | using System.Text.RegularExpressions;
8 | using System.Threading.Tasks;
9 | using System.Transactions;
10 | using Microsoft.EntityFrameworkCore;
11 | using Microsoft.EntityFrameworkCore.ChangeTracking;
12 | using Microsoft.EntityFrameworkCore.Infrastructure;
13 | using Microsoft.EntityFrameworkCore.Metadata;
14 |
15 | namespace Arch.EntityFrameworkCore.UnitOfWork
16 | {
17 | ///
18 | /// Represents the default implementation of the and interface.
19 | ///
20 | /// The type of the db context.
21 | public class UnitOfWork : IRepositoryFactory, IUnitOfWork, IUnitOfWork where TContext : DbContext
22 | {
23 | private readonly TContext _context;
24 | private bool disposed = false;
25 | private Dictionary repositories;
26 |
27 | ///
28 | /// Initializes a new instance of the class.
29 | ///
30 | /// The context.
31 | public UnitOfWork(TContext context)
32 | {
33 | _context = context ?? throw new ArgumentNullException(nameof(context));
34 | }
35 |
36 | ///
37 | /// Gets the db context.
38 | ///
39 | /// The instance of type .
40 | public TContext DbContext => _context;
41 |
42 | ///
43 | /// Changes the database name. This require the databases in the same machine. NOTE: This only work for MySQL right now.
44 | ///
45 | /// The database name.
46 | ///
47 | /// This only been used for supporting multiple databases in the same model. This require the databases in the same machine.
48 | ///
49 | public void ChangeDatabase(string database)
50 | {
51 | var connection = _context.Database.GetDbConnection();
52 | if (connection.State.HasFlag(ConnectionState.Open))
53 | {
54 | connection.ChangeDatabase(database);
55 | }
56 | else
57 | {
58 | var connectionString = Regex.Replace(connection.ConnectionString.Replace(" ", ""), @"(?<=[Dd]atabase=)\w+(?=;)", database, RegexOptions.Singleline);
59 | connection.ConnectionString = connectionString;
60 | }
61 |
62 | // Following code only working for mysql.
63 | var items = _context.Model.GetEntityTypes();
64 | foreach (var item in items)
65 | {
66 | if (item is IConventionEntityType entityType)
67 | {
68 | entityType.SetSchema(database);
69 | }
70 | }
71 | }
72 |
73 | ///
74 | /// Gets the specified repository for the .
75 | ///
76 | /// True if providing custom repositry
77 | /// The type of the entity.
78 | /// An instance of type inherited from interface.
79 | public IRepository GetRepository(bool hasCustomRepository = false) where TEntity : class
80 | {
81 | if (repositories == null)
82 | {
83 | repositories = new Dictionary();
84 | }
85 |
86 | // what's the best way to support custom reposity?
87 | if (hasCustomRepository)
88 | {
89 | var customRepo = _context.GetService>();
90 | if (customRepo != null)
91 | {
92 | return customRepo;
93 | }
94 | }
95 |
96 | var type = typeof(TEntity);
97 | if (!repositories.ContainsKey(type))
98 | {
99 | repositories[type] = new Repository(_context);
100 | }
101 |
102 | return (IRepository)repositories[type];
103 | }
104 |
105 | ///
106 | /// Executes the specified raw SQL command.
107 | ///
108 | /// The raw SQL.
109 | /// The parameters.
110 | /// The number of state entities written to database.
111 | public int ExecuteSqlCommand(string sql, params object[] parameters) => _context.Database.ExecuteSqlRaw(sql, parameters);
112 |
113 | ///
114 | /// Uses raw SQL queries to fetch the specified data.
115 | ///
116 | /// The type of the entity.
117 | /// The raw SQL.
118 | /// The parameters.
119 | /// An that contains elements that satisfy the condition specified by raw SQL.
120 | public IQueryable FromSql(string sql, params object[] parameters) where TEntity : class => _context.Set().FromSqlRaw(sql, parameters);
121 |
122 | ///
123 | /// Saves all changes made in this context to the database.
124 | ///
125 | /// True if save changes ensure auto record the change history.
126 | /// The number of state entries written to the database.
127 | public int SaveChanges(bool ensureAutoHistory = false)
128 | {
129 | if (ensureAutoHistory)
130 | {
131 | _context.EnsureAutoHistory();
132 | }
133 |
134 | return _context.SaveChanges();
135 | }
136 |
137 | ///
138 | /// Asynchronously saves all changes made in this unit of work to the database.
139 | ///
140 | /// True if save changes ensure auto record the change history.
141 | /// A that represents the asynchronous save operation. The task result contains the number of state entities written to database.
142 | public async Task SaveChangesAsync(bool ensureAutoHistory = false)
143 | {
144 | if (ensureAutoHistory)
145 | {
146 | _context.EnsureAutoHistory();
147 | }
148 |
149 | return await _context.SaveChangesAsync();
150 | }
151 |
152 | ///
153 | /// Saves all changes made in this context to the database with distributed transaction.
154 | ///
155 | /// True if save changes ensure auto record the change history.
156 | /// An optional array.
157 | /// A that represents the asynchronous save operation. The task result contains the number of state entities written to database.
158 | public async Task SaveChangesAsync(bool ensureAutoHistory = false, params IUnitOfWork[] unitOfWorks)
159 | {
160 | using (var ts = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled))
161 | {
162 | var count = 0;
163 | foreach (var unitOfWork in unitOfWorks)
164 | {
165 | count += await unitOfWork.SaveChangesAsync(ensureAutoHistory).ConfigureAwait(false);
166 | }
167 |
168 | count += await SaveChangesAsync(ensureAutoHistory);
169 |
170 | ts.Complete();
171 |
172 | return count;
173 | }
174 | }
175 |
176 | ///
177 | /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
178 | ///
179 | public void Dispose()
180 | {
181 | Dispose(true);
182 |
183 | GC.SuppressFinalize(this);
184 | }
185 |
186 | ///
187 | /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
188 | ///
189 | /// The disposing.
190 | protected virtual void Dispose(bool disposing)
191 | {
192 | if (!disposed)
193 | {
194 | if (disposing)
195 | {
196 | // clear repositories
197 | if (repositories != null)
198 | {
199 | repositories.Clear();
200 | }
201 |
202 | // dispose the db context.
203 | _context.Dispose();
204 | }
205 | }
206 |
207 | disposed = true;
208 | }
209 |
210 | public void TrackGraph(object rootEntity, Action callback)
211 | {
212 | _context.ChangeTracker.TrackGraph(rootEntity, callback);
213 | }
214 | }
215 | }
216 |
--------------------------------------------------------------------------------
/samples/UnitOfWork.Host/Controllers/ValuesController.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 | using System.Linq;
3 | using System.Threading.Tasks;
4 | using Microsoft.AspNetCore.Mvc;
5 | using Arch.EntityFrameworkCore.UnitOfWork.Collections;
6 | using Arch.EntityFrameworkCore.UnitOfWork.Host.Models;
7 | using Microsoft.EntityFrameworkCore;
8 | using Microsoft.Extensions.Logging;
9 |
10 | namespace Arch.EntityFrameworkCore.UnitOfWork.Host.Controllers
11 | {
12 | [Route("api/[controller]")]
13 | public class ValuesController : Controller
14 | {
15 | private readonly IUnitOfWork _unitOfWork;
16 | private ILogger _logger;
17 |
18 | // 1. IRepositoryFactory used for readonly scenario;
19 | // 2. IUnitOfWork used for read/write scenario;
20 | // 3. IUnitOfWork used for multiple databases scenario;
21 | public ValuesController(IUnitOfWork unitOfWork, ILogger logger)
22 | {
23 | _unitOfWork = unitOfWork;
24 | _logger = logger;
25 |
26 | // seeding
27 | var repo = _unitOfWork.GetRepository(hasCustomRepository: true);
28 | if (repo.Count() == 0)
29 | {
30 | repo.Insert(new Blog
31 | {
32 | Id = 1,
33 | Url = "/a/" + 1,
34 | Title = $"a{1}",
35 | Posts = new List{
36 | new Post
37 | {
38 | Id = 1,
39 | Title = "A",
40 | Content = "A's content",
41 | Comments = new List
42 | {
43 | new Comment
44 | {
45 | Id = 1,
46 | Title = "A",
47 | Content = "A's content",
48 | },
49 | new Comment
50 | {
51 | Id = 2,
52 | Title = "b",
53 | Content = "b's content",
54 | },
55 | new Comment
56 | {
57 | Id = 3,
58 | Title = "c",
59 | Content = "c's content",
60 | }
61 | },
62 | },
63 | new Post
64 | {
65 | Id = 2,
66 | Title = "B",
67 | Content = "B's content",
68 | Comments = new List
69 | {
70 | new Comment
71 | {
72 | Id = 4,
73 | Title = "A",
74 | Content = "A's content",
75 | },
76 | new Comment
77 | {
78 | Id = 5,
79 | Title = "b",
80 | Content = "b's content",
81 | },
82 | new Comment
83 | {
84 | Id = 6,
85 | Title = "c",
86 | Content = "c's content",
87 | }
88 | },
89 | },
90 | new Post
91 | {
92 | Id = 3,
93 | Title = "C",
94 | Content = "C's content",
95 | Comments = new List
96 | {
97 | new Comment
98 | {
99 | Id = 7,
100 | Title = "A",
101 | Content = "A's content",
102 | },
103 | new Comment
104 | {
105 | Id = 8,
106 | Title = "b",
107 | Content = "b's content",
108 | },
109 | new Comment
110 | {
111 | Id = 9,
112 | Title = "c",
113 | Content = "c's content",
114 | }
115 | },
116 | },
117 | new Post
118 | {
119 | Id = 4,
120 | Title = "D",
121 | Content = "D's content",
122 | Comments = new List
123 | {
124 | new Comment
125 | {
126 | Id = 10,
127 | Title = "A",
128 | Content = "A's content",
129 | },
130 | new Comment
131 | {
132 | Id = 11,
133 | Title = "b",
134 | Content = "b's content",
135 | },
136 | new Comment
137 | {
138 | Id = 12,
139 | Title = "c",
140 | Content = "c's content",
141 | }
142 | },
143 | }
144 | },
145 | });
146 | _unitOfWork.SaveChanges();
147 | }
148 | }
149 |
150 | // GET api/values
151 | [HttpGet]
152 | public async Task> Get()
153 | {
154 | return await _unitOfWork.GetRepository().GetAllAsync(include: source => source.Include(blog => blog.Posts).ThenInclude(post => post.Comments));
155 | }
156 |
157 | // GET api/values/Page/5/10
158 | [HttpGet("Page/{pageIndex}/{pageSize}")]
159 | public async Task> Get(int pageIndex, int pageSize)
160 | {
161 | // projection
162 | var items = _unitOfWork.GetRepository().GetPagedList(b => new { Name = b.Title, Link = b.Url });
163 |
164 | return await _unitOfWork.GetRepository().GetPagedListAsync(pageIndex: pageIndex, pageSize: pageSize);
165 | }
166 |
167 | // GET api/values/Search/a1
168 | [HttpGet("Search/{term}")]
169 | public async Task> Get(string term)
170 | {
171 | _logger.LogInformation("demo about first or default with include");
172 |
173 | var item = _unitOfWork.GetRepository().GetFirstOrDefault(predicate: x => x.Title.Contains(term), include: source => source.Include(blog => blog.Posts).ThenInclude(post => post.Comments));
174 |
175 | _logger.LogInformation("demo about first or default without include");
176 |
177 | item = _unitOfWork.GetRepository().GetFirstOrDefault(predicate: x => x.Title.Contains(term), orderBy: source => source.OrderByDescending(b => b.Id));
178 |
179 | _logger.LogInformation("demo about first or default with projection");
180 |
181 | var projection = _unitOfWork.GetRepository().GetFirstOrDefault(b => new { Name = b.Title, Link = b.Url }, predicate: x => x.Title.Contains(term));
182 |
183 | return await _unitOfWork.GetRepository().GetPagedListAsync(predicate: x => x.Title.Contains(term));
184 | }
185 |
186 | // GET api/values/4
187 | [HttpGet("{id}")]
188 | public async Task Get(int id)
189 | {
190 | return await _unitOfWork.GetRepository().FindAsync(id);
191 | }
192 |
193 | // POST api/values
194 | [HttpPost]
195 | public async Task Post([FromBody]Blog value)
196 | {
197 | var repo = _unitOfWork.GetRepository();
198 | repo.Insert(value);
199 | await _unitOfWork.SaveChangesAsync();
200 | }
201 | }
202 | }
--------------------------------------------------------------------------------
/src/UnitOfWork/Collections/PagedList.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Arch team. All rights reserved.
2 |
3 | using System;
4 | using System.Collections.Generic;
5 | using System.Linq;
6 |
7 | namespace Arch.EntityFrameworkCore.UnitOfWork.Collections
8 | {
9 | ///
10 | /// Represents the default implementation of the interface.
11 | ///
12 | /// The type of the data to page
13 | public class PagedList : IPagedList
14 | {
15 | ///
16 | /// Gets or sets the index of the page.
17 | ///
18 | /// The index of the page.
19 | public int PageIndex { get; set; }
20 | ///
21 | /// Gets or sets the size of the page.
22 | ///
23 | /// The size of the page.
24 | public int PageSize { get; set; }
25 | ///
26 | /// Gets or sets the total count.
27 | ///
28 | /// The total count.
29 | public int TotalCount { get; set; }
30 | ///
31 | /// Gets or sets the total pages.
32 | ///
33 | /// The total pages.
34 | public int TotalPages { get; set; }
35 | ///
36 | /// Gets or sets the index from.
37 | ///
38 | /// The index from.
39 | public int IndexFrom { get; set; }
40 |
41 | ///
42 | /// Gets or sets the items.
43 | ///
44 | /// The items.
45 | public IList Items { get; set; }
46 |
47 | ///
48 | /// Gets the has previous page.
49 | ///
50 | /// The has previous page.
51 | public bool HasPreviousPage => PageIndex - IndexFrom > 0;
52 |
53 | ///
54 | /// Gets the has next page.
55 | ///
56 | /// The has next page.
57 | public bool HasNextPage => PageIndex - IndexFrom + 1 < TotalPages;
58 |
59 | ///
60 | /// Initializes a new instance of the class.
61 | ///
62 | /// The source.
63 | /// The index of the page.
64 | /// The size of the page.
65 | /// The index from.
66 | internal PagedList(IEnumerable source, int pageIndex, int pageSize, int indexFrom)
67 | {
68 | if (indexFrom > pageIndex)
69 | {
70 | throw new ArgumentException($"indexFrom: {indexFrom} > pageIndex: {pageIndex}, must indexFrom <= pageIndex");
71 | }
72 |
73 | if (source is IQueryable querable)
74 | {
75 | PageIndex = pageIndex;
76 | PageSize = pageSize;
77 | IndexFrom = indexFrom;
78 | TotalCount = querable.Count();
79 | TotalPages = (int)Math.Ceiling(TotalCount / (double)PageSize);
80 |
81 | Items = querable.Skip((PageIndex - IndexFrom) * PageSize).Take(PageSize).ToList();
82 | }
83 | else
84 | {
85 | PageIndex = pageIndex;
86 | PageSize = pageSize;
87 | IndexFrom = indexFrom;
88 | TotalCount = source.Count();
89 | TotalPages = (int)Math.Ceiling(TotalCount / (double)PageSize);
90 |
91 | Items = source.Skip((PageIndex - IndexFrom) * PageSize).Take(PageSize).ToList();
92 | }
93 | }
94 |
95 | ///
96 | /// Initializes a new instance of the class.
97 | ///
98 | internal PagedList() => Items = new T[0];
99 | }
100 |
101 |
102 | ///
103 | /// Provides the implementation of the and converter.
104 | ///
105 | /// The type of the source.
106 | /// The type of the result.
107 | internal class PagedList : IPagedList
108 | {
109 | ///
110 | /// Gets the index of the page.
111 | ///
112 | /// The index of the page.
113 | public int PageIndex { get; }
114 | ///
115 | /// Gets the size of the page.
116 | ///
117 | /// The size of the page.
118 | public int PageSize { get; }
119 | ///
120 | /// Gets the total count.
121 | ///
122 | /// The total count.
123 | public int TotalCount { get; }
124 | ///
125 | /// Gets the total pages.
126 | ///
127 | /// The total pages.
128 | public int TotalPages { get; }
129 | ///
130 | /// Gets the index from.
131 | ///
132 | /// The index from.
133 | public int IndexFrom { get; }
134 |
135 | ///
136 | /// Gets the items.
137 | ///
138 | /// The items.
139 | public IList Items { get; }
140 |
141 | ///
142 | /// Gets the has previous page.
143 | ///
144 | /// The has previous page.
145 | public bool HasPreviousPage => PageIndex - IndexFrom > 0;
146 |
147 | ///
148 | /// Gets the has next page.
149 | ///
150 | /// The has next page.
151 | public bool HasNextPage => PageIndex - IndexFrom + 1 < TotalPages;
152 |
153 | ///
154 | /// Initializes a new instance of the class.
155 | ///
156 | /// The source.
157 | /// The converter.
158 | /// The index of the page.
159 | /// The size of the page.
160 | /// The index from.
161 | public PagedList(IEnumerable source, Func, IEnumerable> converter, int pageIndex, int pageSize, int indexFrom)
162 | {
163 | if (indexFrom > pageIndex)
164 | {
165 | throw new ArgumentException($"indexFrom: {indexFrom} > pageIndex: {pageIndex}, must indexFrom <= pageIndex");
166 | }
167 |
168 | if (source is IQueryable querable)
169 | {
170 | PageIndex = pageIndex;
171 | PageSize = pageSize;
172 | IndexFrom = indexFrom;
173 | TotalCount = querable.Count();
174 | TotalPages = (int)Math.Ceiling(TotalCount / (double)PageSize);
175 |
176 | var items = querable.Skip((PageIndex - IndexFrom) * PageSize).Take(PageSize).ToArray();
177 |
178 | Items = new List(converter(items));
179 | }
180 | else
181 | {
182 | PageIndex = pageIndex;
183 | PageSize = pageSize;
184 | IndexFrom = indexFrom;
185 | TotalCount = source.Count();
186 | TotalPages = (int)Math.Ceiling(TotalCount / (double)PageSize);
187 |
188 | var items = source.Skip((PageIndex - IndexFrom) * PageSize).Take(PageSize).ToArray();
189 |
190 | Items = new List(converter(items));
191 | }
192 | }
193 |
194 | ///
195 | /// Initializes a new instance of the class.
196 | ///
197 | /// The source.
198 | /// The converter.
199 | public PagedList(IPagedList source, Func, IEnumerable> converter)
200 | {
201 | PageIndex = source.PageIndex;
202 | PageSize = source.PageSize;
203 | IndexFrom = source.IndexFrom;
204 | TotalCount = source.TotalCount;
205 | TotalPages = source.TotalPages;
206 |
207 | Items = new List(converter(source.Items));
208 | }
209 | }
210 |
211 | ///
212 | /// Provides some help methods for interface.
213 | ///
214 | public static class PagedList
215 | {
216 | ///
217 | /// Creates an empty of .
218 | ///
219 | /// The type for paging
220 | /// An empty instance of .
221 | public static IPagedList Empty() => new PagedList();
222 | ///
223 | /// Creates a new instance of from source of instance.
224 | ///
225 | /// The type of the result.
226 | /// The type of the source.
227 | /// The source.
228 | /// The converter.
229 | /// An instance of .
230 | public static IPagedList From(IPagedList source, Func, IEnumerable> converter) => new PagedList(source, converter);
231 | }
232 | }
233 |
--------------------------------------------------------------------------------
/src/UnitOfWork/IRepository.cs:
--------------------------------------------------------------------------------
1 | //-----------------------------------------------------------------------
2 | //
3 | // Copyright (c) Arch team. All rights reserved.
4 | //
5 | //-----------------------------------------------------------------------
6 |
7 | namespace Arch.EntityFrameworkCore.UnitOfWork
8 | {
9 | using System;
10 | using System.Collections.Generic;
11 | using System.Linq;
12 | using System.Linq.Expressions;
13 | using System.Threading;
14 | using System.Threading.Tasks;
15 | using Microsoft.EntityFrameworkCore.Query;
16 | using Arch.EntityFrameworkCore.UnitOfWork.Collections;
17 | using Microsoft.EntityFrameworkCore.ChangeTracking;
18 | using Microsoft.EntityFrameworkCore;
19 |
20 | ///
21 | /// Defines the interfaces for generic repository.
22 | ///
23 | /// The type of the entity.
24 | public interface IRepository where TEntity : class
25 | {
26 | ///
27 | /// Changes the table name. This require the tables in the same database.
28 | ///
29 | ///
30 | ///
31 | /// This only been used for supporting multiple tables in the same model. This require the tables in the same database.
32 | ///
33 | void ChangeTable(string table);
34 |
35 | ///
36 | /// Gets the based on a predicate, orderby delegate and page information. This method default no-tracking query.
37 | ///
38 | /// A function to test each element for a condition.
39 | /// A function to order elements.
40 | /// A function to include navigation properties
41 | /// The index of page.
42 | /// The size of the page.
43 | /// True to disable changing tracking; otherwise, false. Default to true.
44 | /// Ignore query filters
45 | /// An that contains elements that satisfy the condition specified by .
46 | /// This method default no-tracking query.
47 | IPagedList GetPagedList(Expression> predicate = null,
48 | Func, IOrderedQueryable> orderBy = null,
49 | Func, IIncludableQueryable> include = null,
50 | int pageIndex = 0,
51 | int pageSize = 20,
52 | bool disableTracking = true,
53 | bool ignoreQueryFilters = false);
54 |
55 | ///
56 | /// Gets the based on a predicate, orderby delegate and page information. This method default no-tracking query.
57 | ///
58 | /// A function to test each element for a condition.
59 | /// A function to order elements.
60 | /// A function to include navigation properties
61 | /// The index of page.
62 | /// The size of the page.
63 | /// True to disable changing tracking; otherwise, false. Default to true.
64 | ///
65 | /// A to observe while waiting for the task to complete.
66 | ///
67 | /// Ignore query filters
68 | /// An that contains elements that satisfy the condition specified by .
69 | /// This method default no-tracking query.
70 | Task> GetPagedListAsync(Expression> predicate = null,
71 | Func, IOrderedQueryable> orderBy = null,
72 | Func, IIncludableQueryable> include = null,
73 | int pageIndex = 0,
74 | int pageSize = 20,
75 | bool disableTracking = true,
76 | CancellationToken cancellationToken = default(CancellationToken),
77 | bool ignoreQueryFilters = false);
78 |
79 | ///
80 | /// Gets the based on a predicate, orderby delegate and page information. This method default no-tracking query.
81 | ///
82 | /// The selector for projection.
83 | /// A function to test each element for a condition.
84 | /// A function to order elements.
85 | /// A function to include navigation properties
86 | /// The index of page.
87 | /// The size of the page.
88 | /// True to disable changing tracking; otherwise, false. Default to true.
89 | /// Ignore query filters
90 | /// An that contains elements that satisfy the condition specified by .
91 | /// This method default no-tracking query.
92 | IPagedList GetPagedList(Expression> selector,
93 | Expression> predicate = null,
94 | Func, IOrderedQueryable> orderBy = null,
95 | Func, IIncludableQueryable> include = null,
96 | int pageIndex = 0,
97 | int pageSize = 20,
98 | bool disableTracking = true,
99 | bool ignoreQueryFilters = false) where TResult : class;
100 |
101 | ///
102 | /// Gets the based on a predicate, orderby delegate and page information. This method default no-tracking query.
103 | ///
104 | /// The selector for projection.
105 | /// A function to test each element for a condition.
106 | /// A function to order elements.
107 | /// A function to include navigation properties
108 | /// The index of page.
109 | /// The size of the page.
110 | /// True to disable changing tracking; otherwise, false. Default to true.
111 | ///
112 | /// A to observe while waiting for the task to complete.
113 | ///
114 | /// Ignore query filters
115 | /// An that contains elements that satisfy the condition specified by .
116 | /// This method default no-tracking query.
117 | Task> GetPagedListAsync(Expression> selector,
118 | Expression> predicate = null,
119 | Func, IOrderedQueryable> orderBy = null,
120 | Func, IIncludableQueryable> include = null,
121 | int pageIndex = 0,
122 | int pageSize = 20,
123 | bool disableTracking = true,
124 | CancellationToken cancellationToken = default(CancellationToken),
125 | bool ignoreQueryFilters = false) where TResult : class;
126 |
127 | ///
128 | /// Gets the first or default entity based on a predicate, orderby delegate and include delegate. This method defaults to a read-only, no-tracking query.
129 | ///
130 | /// A function to test each element for a condition.
131 | /// A function to order elements.
132 | /// A function to include navigation properties
133 | /// true to disable changing tracking; otherwise, false. Default to true.
134 | /// Ignore query filters
135 | /// An that contains elements that satisfy the condition specified by .
136 | /// This method defaults to a read-only, no-tracking query.
137 | TEntity GetFirstOrDefault(Expression> predicate = null,
138 | Func, IOrderedQueryable> orderBy = null,
139 | Func, IIncludableQueryable> include = null,
140 | bool disableTracking = true,
141 | bool ignoreQueryFilters = false);
142 |
143 | ///
144 | /// Gets the first or default entity based on a predicate, orderby delegate and include delegate. This method defaults to a read-only, no-tracking query.
145 | ///
146 | /// The selector for projection.
147 | /// A function to test each element for a condition.
148 | /// A function to order elements.
149 | /// A function to include navigation properties
150 | /// true to disable changing tracking; otherwise, false. Default to true.
151 | /// Ignore query filters
152 | /// An that contains elements that satisfy the condition specified by .
153 | /// This method defaults to a read-only, no-tracking query.
154 | TResult GetFirstOrDefault(Expression> selector,
155 | Expression> predicate = null,
156 | Func, IOrderedQueryable> orderBy = null,
157 | Func, IIncludableQueryable> include = null,
158 | bool disableTracking = true,
159 | bool ignoreQueryFilters = false);
160 |
161 | ///
162 | /// Gets the first or default entity based on a predicate, orderby delegate and include delegate. This method defaults to a read-only, no-tracking query.
163 | ///
164 | /// The selector for projection.
165 | /// A function to test each element for a condition.
166 | /// A function to order elements.
167 | /// A function to include navigation properties
168 | /// true to disable changing tracking; otherwise, false. Default to true.
169 | /// Ignore query filters
170 | /// An that contains elements that satisfy the condition specified by .
171 | /// Ex: This method defaults to a read-only, no-tracking query.
172 | Task GetFirstOrDefaultAsync(Expression> selector,
173 | Expression> predicate = null,
174 | Func, IOrderedQueryable> orderBy = null,
175 | Func, IIncludableQueryable> include = null,
176 | bool disableTracking = true,
177 | bool ignoreQueryFilters = false);
178 |
179 | ///
180 | /// Gets the first or default entity based on a predicate, orderby delegate and include delegate. This method defaults to a read-only, no-tracking query.
181 | ///
182 | /// A function to test each element for a condition.
183 | /// A function to order elements.
184 | /// A function to include navigation properties
185 | /// true to disable changing tracking; otherwise, false. Default to true.
186 | /// Ignore query filters
187 | /// An that contains elements that satisfy the condition specified by .
188 | /// Ex: This method defaults to a read-only, no-tracking query.
189 | Task GetFirstOrDefaultAsync(Expression> predicate = null,
190 | Func, IOrderedQueryable> orderBy = null,
191 | Func, IIncludableQueryable> include = null,
192 | bool disableTracking = true,
193 | bool ignoreQueryFilters = false);
194 |
195 | ///
196 | /// Uses raw SQL queries to fetch the specified data.
197 | ///
198 | /// The raw SQL.
199 | /// The parameters.
200 | /// An that contains elements that satisfy the condition specified by raw SQL.
201 | IQueryable FromSql(string sql, params object[] parameters);
202 |
203 | ///
204 | /// Finds an entity with the given primary key values. If found, is attached to the context and returned. If no entity is found, then null is returned.
205 | ///
206 | /// The values of the primary key for the entity to be found.
207 | /// The found entity or null.
208 | TEntity Find(params object[] keyValues);
209 |
210 | ///
211 | /// Finds an entity with the given primary key values. If found, is attached to the context and returned. If no entity is found, then null is returned.
212 | ///
213 | /// The values of the primary key for the entity to be found.
214 | /// A that represents the asynchronous find operation. The task result contains the found entity or null.
215 | ValueTask FindAsync(params object[] keyValues);
216 |
217 | ///
218 | /// Finds an entity with the given primary key values. If found, is attached to the context and returned. If no entity is found, then null is returned.
219 | ///
220 | /// The values of the primary key for the entity to be found.
221 | /// A to observe while waiting for the task to complete.
222 | /// A that represents the asynchronous find operation. The task result contains the found entity or null.
223 | ValueTask FindAsync(object[] keyValues, CancellationToken cancellationToken);
224 |
225 | ///
226 | /// Gets all entities. This method is not recommended
227 | ///
228 | /// The .
229 | IQueryable GetAll();
230 |
231 | ///
232 | /// Gets all entities. This method is not recommended
233 | ///
234 | /// A function to test each element for a condition.
235 | /// A function to order elements.
236 | /// A function to include navigation properties
237 | /// true to disable changing tracking; otherwise, false. Default to true.
238 | /// Ignore query filters
239 | /// An that contains elements that satisfy the condition specified by .
240 | /// Ex: This method defaults to a read-only, no-tracking query.
241 | IQueryable GetAll(Expression> predicate = null,
242 | Func, IOrderedQueryable> orderBy = null,
243 | Func, IIncludableQueryable> include = null,
244 | bool disableTracking = true,
245 | bool ignoreQueryFilters = false);
246 |
247 | ///
248 | /// Gets all entities. This method is not recommended
249 | ///
250 | /// The selector for projection.
251 | /// A function to test each element for a condition.
252 | /// A function to order elements.
253 | /// A function to include navigation properties
254 | /// true to disable changing tracking; otherwise, false. Default to true.
255 | /// Ignore query filters
256 | /// An that contains elements that satisfy the condition specified by .
257 | /// Ex: This method defaults to a read-only, no-tracking query.
258 | IQueryable GetAll(Expression> selector,
259 | Expression> predicate = null,
260 | Func, IOrderedQueryable> orderBy = null,
261 | Func, IIncludableQueryable> include = null,
262 | bool disableTracking = true,
263 | bool ignoreQueryFilters = false);
264 |
265 | ///
266 | /// Gets all entities. This method is not recommended
267 | ///
268 | /// The .
269 | Task> GetAllAsync();
270 |
271 | ///
272 | /// Gets all entities. This method is not recommended
273 | ///
274 | /// A function to test each element for a condition.
275 | /// A function to order elements.
276 | /// A function to include navigation properties
277 | /// true to disable changing tracking; otherwise, false. Default to true.
278 | /// Ignore query filters
279 | /// An that contains elements that satisfy the condition specified by .
280 | /// Ex: This method defaults to a read-only, no-tracking query.
281 | Task> GetAllAsync(Expression> predicate = null,
282 | Func, IOrderedQueryable> orderBy = null,
283 | Func, IIncludableQueryable> include = null,
284 | bool disableTracking = true,
285 | bool ignoreQueryFilters = false);
286 |
287 | ///
288 | /// Gets all entities. This method is not recommended
289 | ///
290 | /// The selector for projection.
291 | /// A function to test each element for a condition.
292 | /// A function to order elements.
293 | /// A function to include navigation properties
294 | /// true to disable changing tracking; otherwise, false. Default to true.
295 | /// Ignore query filters
296 | /// An that contains elements that satisfy the condition specified by .
297 | /// Ex: This method defaults to a read-only, no-tracking query.
298 | Task> GetAllAsync(Expression> selector,
299 | Expression> predicate = null,
300 | Func, IOrderedQueryable> orderBy = null,
301 | Func, IIncludableQueryable