6, Multi module Association query of Efcore in Abp Vnext

Posted by ondercsn on Thu, 16 Dec 2021 12:26:27 +0100

abp framework provides a great module development experience. These modules are reusable and also suitable for developing microservices; Since the module can be published independently, its database configuration is also independent. For the module using efcore, each module contains a different Dbcontext;

In Efcore, under the same Dbcontext, multiple entity sets can use linq for arbitrary Association queries, and for association queries under multiple different dbcontexts, even under the same database, they cannot be queried through linq. Let's verify the following framework code through an example. Let's use the code of the file management module in the previous chapter. Suppose we need to associate the file module with the built-in Abp The identity module queries the user's file information, which is required to include the user's name

Before we solve the problem, let's reproduce the problem

First, add a view entity class UserFileView in the Domain project of the file module to receive the results of the union query. The code is as follows

using System;

namespace MyCompany.FileManagement.Entities
{
    public class UserFileView
    {
        public long FileSize { get; set; }
        public string MimeType { get; set; }
        public string Path { get; set; }
        public string Name { get; set; }
        // User id; if it is not empty, it is a personal file; otherwise, it is a public file
        public Guid? OwnerId { get; set; }

        public string SurName { get; set; }

        public string UserName { get; set; }

        public DateTime CreationTime { get; set; }
        public DateTime? LastModificationTime { get; set; }
    }
}

Add a data warehouse interface IUserFilesRepository:

using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Volo.Abp.Domain.Repositories;
namespace MyCompany.FileManagement.Entities
{
    public interface IUserFilesRepository: IRepository
    {
        // Get paging record according to user Id
        Task<List<UserFileView>> GetListAsync(string sorting = null, int maxResultCount = int.MaxValue, int skipCount = 0, Guid? userId = null,
            CancellationToken cancellationToken = default);
        // Gets the total number of records based on user Id
        Task<long> GetCountAsync(Guid? userId = null, CancellationToken cancellationToken = default);
    }
}

Then add the data warehouse implementation class UserFilesRepository in the EntityFrameworkCore project

using Microsoft.EntityFrameworkCore;
using MyCompany.FileManagement.Entities;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Dynamic.Core;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Volo.Abp.Domain.Entities;
using Volo.Abp.Domain.Repositories.EntityFrameworkCore;
using Volo.Abp.EntityFrameworkCore;
using Volo.Abp.Identity;
using Volo.Abp.Identity.EntityFrameworkCore;

namespace MyCompany.FileManagement.EntityFrameworkCore
{
    public class UserFilesRepository: EfCoreRepository<IFileManagementDbContext, BlobFile>, IUserFilesRepository
    {
        // Identity module DbContextProvider
        private readonly IDbContextProvider<IIdentityDbContext> _identityDbContextProvider;
        public UserFilesRepository(
            IDbContextProvider<IFileManagementDbContext> dbContextProvider, IDbContextProvider<IIdentityDbContext> identityDbContextProvider)
         : base(dbContextProvider)
        {
            _identityDbContextProvider = identityDbContextProvider;
        }

        public async Task<List<UserFileView>> GetListAsync(string sorting = null, int maxResultCount = int.MaxValue, int skipCount = 0, Guid? userId = null, CancellationToken cancellationToken = default)
        {
            var query = await GetQuery(userId);

            return await query
               .OrderBy(sorting.IsNullOrWhiteSpace() ? (nameof(UserFileView.FileSize) + " desc") : sorting)
               .PageBy(skipCount, maxResultCount)
               .ToListAsync(GetCancellationToken(cancellationToken));
        }

        public async Task<long> GetCountAsync(Guid? userId = null, CancellationToken cancellationToken = default)
        {
            var query = await GetQuery(userId);

            return await query.LongCountAsync();
        }

        private async Task<IQueryable<UserFileView>> GetQuery(Guid? userId)
        {
            // Gets the DbContext instance of the current module
            var dbContext = await GetDbContextAsync();
            // Gets the DbContext instance of the Identity module
            var identityDbcontext = await GetIdentityDbContext();
            // linq query, external connection query of entities between modules
            var query = from f in dbContext.BlobFiles
                        from u in identityDbcontext.Users.Where(i => i.Id == f.OwnerId).DefaultIfEmpty()
                        select new UserFileView
                        {
                            Name = f.Name,
                            FileSize = f.FileSize,
                            MimeType = f.MimeType,
                            OwnerId = f.OwnerId,
                            Path = f.Path,
                            CreationTime = f.CreationTime,
                            LastModificationTime = f.LastModificationTime,
                            SurName = u.Surname,
                            UserName = u.Name
                        };

            return query
               .WhereIf(userId.HasValue, r => r.OwnerId == userId);
        }
        /// <summary>
        ///Get the IdentityDbContext instance of the Identity module
        /// </summary>
        /// <returns></returns>
        protected Task<IIdentityDbContext> GetIdentityDbContext()
        {
            if (!EntityHelper.IsMultiTenant<IdentityUser>())
            {
                using (CurrentTenant.Change(null))
                {
                    return _identityDbContextProvider.GetDbContextAsync();
                }
            }

            return _identityDbContextProvider.GetDbContextAsync();
        }

    }
}

You can see that in the data warehouse implementation, external connection queries are performed on the User table of the Identity module and the BlobFiles table of the file management module, and the query results are returned to the UserFileView set

Test the above code below and open mycompany TestProject. EntityFrameworkCore. Samplerepositorytests. For tests CS, modify to the following code:

using Shouldly;
using System.Threading.Tasks;
using Xunit;
using MyCompany.FileManagement.Entities;

namespace MyCompany.TestProject.EntityFrameworkCore.Samples
{
    /* This is just an example test class.
     * Normally, you don't test ABP framework code
     * (like default AppUser repository IRepository<AppUser, Guid> here).
     * Only test your custom repository methods.
     */
    public class SampleRepositoryTests : TestProjectEntityFrameworkCoreTestBase
    {
        private readonly IUserFilesRepository _userFilesRepository;

        public SampleRepositoryTests()
        {
            _userFilesRepository = GetRequiredService<IUserFilesRepository>();
        }

        [Fact]
        public async Task Should_Query_AppUser()
        {
            /* Need to manually start Unit Of Work because
             * FirstOrDefaultAsync should be executed while db connection / context is available.
             */
            await WithUnitOfWorkAsync(async () =>
            {
                //Act
                var adminUser = await _userFilesRepository.GetCountAsync();

                //Assert
                adminUser.ShouldBeGreaterThan(0);
            });
        }
    }
}

Right click mycompany TestProject. EntityFrameworkCore. Tests project, select debug test

You can see that the test fails and an exception occurs:

The exception shows that multiple contexts cannot appear in one query

Let's see how to solve this problem. There are two methods:

The first one is very simple. We open mycompany in the main module TestProject. Testprojectdbcontext. For entityframeworkcore project CS file, add the following (the part with comments in the code):

    [ReplaceDbContext(typeof(IIdentityDbContext))]
    [ReplaceDbContext(typeof(ITenantManagementDbContext))]
    [ReplaceDbContext(typeof(IFileManagementDbContext))] // Add file module DbContext
    [ConnectionStringName("Default")]
    public class TestProjectDbContext : AbpDbContext<TestProjectDbContext>,
        IIdentityDbContext,
        ITenantManagementDbContext,
        IFileManagementDbContext // Add an implementation file module IDbContext interface
    {
        /* Add DbSet properties for your Aggregate Roots / Entities here. */
        
        #region Entities from the modules
        
        ...
        // Implement the properties of IFileManagementDbContext
        public DbSet<BlobFile> BlobFiles { get; set; }

        #endregion
    }

Adding the ReplaceDbContext modifier in the main module can be understood as using TestProjectDbContext to replace IFileManagementDbContext and iiidentitydbcontext for query when the program is running, which is equivalent to multi module Association query in one DbContext

When we run the test again, we can see that the test has passed

The second uses sql statements to query

This method needs to add the view entity collection in the DbContext of the file module, and the entity cannot be mapped to the database table

First, add in IFileManagementDbContext

DbSet<UserFileView> UserFileView { get; }

Add in FileManagementDbContext

public DbSet<UserFileView> UserFileView { get; set; }

In filemanagementdbcontextmodelcreatingextensions Add in CS

            // It is only used as a query view and does not map database tables
            builder.Entity<UserFileView>(b =>
            {
                b.HasNoKey().ToSqlQuery("select 1");
            });

In userfilesrepository The method of adding GetQueryFromSql to CS is as follows:

        private async Task<IQueryable<UserFileView>> GetQueryFromSql(Guid? userId)
        {
            // Gets the DbContext instance of the current module
            var dbContext = await GetDbContextAsync();
            string sql = @"SELECT f.Name, f.FileSize, f.MimeType, f.Path, f.OwnerId, f.CreationTime, f.ConcurrencyStamp,
u.Surname as SurName, u.Name as UserName
FROM filemanagementblobfiles f left join abpusers u on f.OwnerId = u.Id";
           
            return dbContext.UserFileView.FromSqlRaw(sql)
               .WhereIf(userId.HasValue, r => r.OwnerId == userId);
        }

Change GetQuery in GetCountAsync method to GetQueryFromSql, and then execute the test. You can see that the test has also passed

To sum up, in the first method, linq query is more applicable and convenient to switch various databases. The disadvantage is that some queries can not be realized by linq; The second method needs to write sql statements. In case of complex sql statements, the versions of different databases are different, so it is necessary to write sql providers for different databases

This article uses the same source code as the previous chapter: Multi module Association query of Efcore in Abp Vnext

Topics: C# abp linq efcore