diff --git a/abp_io/src/Volo.Utils.SolutionTemplating/Volo/Utils/SolutionTemplating/Building/Steps/SwitchEntityFrameworkCoreToMongoDbStep.cs b/abp_io/src/Volo.Utils.SolutionTemplating/Volo/Utils/SolutionTemplating/Building/Steps/SwitchEntityFrameworkCoreToMongoDbStep.cs index 0901d0250b..d8a622fe83 100644 --- a/abp_io/src/Volo.Utils.SolutionTemplating/Volo/Utils/SolutionTemplating/Building/Steps/SwitchEntityFrameworkCoreToMongoDbStep.cs +++ b/abp_io/src/Volo.Utils.SolutionTemplating/Volo/Utils/SolutionTemplating/Building/Steps/SwitchEntityFrameworkCoreToMongoDbStep.cs @@ -21,9 +21,9 @@ namespace Volo.Utils.SolutionTemplating.Building.Steps var lines = file.GetLines(); for (var i = 0; i < lines.Length; i++) { - if (lines[i].Contains("ProjectReference") && lines[i].Contains("MyCompanyName.MyProjectName.EntityFrameworkCore")) + if (lines[i].Contains("ProjectReference") && lines[i].Contains("MyCompanyName.MyProjectName.EntityFrameworkCore.DbMigrations")) { - lines[i] = lines[i].Replace("EntityFrameworkCore", "MongoDB"); + lines[i] = lines[i].Replace("EntityFrameworkCore.DbMigrations", "MongoDB"); file.SetLines(lines); return; } diff --git a/framework/Volo.Abp.sln b/framework/Volo.Abp.sln index 41eb7f1aeb..b0b7803963 100644 --- a/framework/Volo.Abp.sln +++ b/framework/Volo.Abp.sln @@ -228,7 +228,11 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Volo.Abp.Cli.Tests", "test\ EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Volo.Abp.EntityFrameworkCore.Sqlite", "src\Volo.Abp.EntityFrameworkCore.Sqlite\Volo.Abp.EntityFrameworkCore.Sqlite.csproj", "{58CF8957-5045-4F81-884D-72DF48F721CC}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Volo.Abp.Cli.Core", "src\Volo.Abp.Cli.Core\Volo.Abp.Cli.Core.csproj", "{3DA9923E-048E-4FE7-9748-3A0194F5D196}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Volo.Abp.Cli.Core", "src\Volo.Abp.Cli.Core\Volo.Abp.Cli.Core.csproj", "{3DA9923E-048E-4FE7-9748-3A0194F5D196}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Volo.Abp.Specifications", "src\Volo.Abp.Specifications\Volo.Abp.Specifications.csproj", "{2C621EED-563C-4F81-A75E-50879E173544}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Volo.Abp.Specifications.Tests", "test\Volo.Abp.Specifications.Tests\Volo.Abp.Specifications.Tests.csproj", "{D078553A-C70C-4F56-B3E2-9C5BA6384C72}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -684,6 +688,14 @@ Global {3DA9923E-048E-4FE7-9748-3A0194F5D196}.Debug|Any CPU.Build.0 = Debug|Any CPU {3DA9923E-048E-4FE7-9748-3A0194F5D196}.Release|Any CPU.ActiveCfg = Release|Any CPU {3DA9923E-048E-4FE7-9748-3A0194F5D196}.Release|Any CPU.Build.0 = Release|Any CPU + {2C621EED-563C-4F81-A75E-50879E173544}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2C621EED-563C-4F81-A75E-50879E173544}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2C621EED-563C-4F81-A75E-50879E173544}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2C621EED-563C-4F81-A75E-50879E173544}.Release|Any CPU.Build.0 = Release|Any CPU + {D078553A-C70C-4F56-B3E2-9C5BA6384C72}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D078553A-C70C-4F56-B3E2-9C5BA6384C72}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D078553A-C70C-4F56-B3E2-9C5BA6384C72}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D078553A-C70C-4F56-B3E2-9C5BA6384C72}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -801,6 +813,8 @@ Global {92B70EFF-C1B1-4D1D-8BCE-D116908FC6FF} = {447C8A77-E5F0-4538-8687-7383196D04EA} {58CF8957-5045-4F81-884D-72DF48F721CC} = {5DF0E140-0513-4D0D-BE2E-3D4D85CD70E6} {3DA9923E-048E-4FE7-9748-3A0194F5D196} = {5DF0E140-0513-4D0D-BE2E-3D4D85CD70E6} + {2C621EED-563C-4F81-A75E-50879E173544} = {5DF0E140-0513-4D0D-BE2E-3D4D85CD70E6} + {D078553A-C70C-4F56-B3E2-9C5BA6384C72} = {447C8A77-E5F0-4538-8687-7383196D04EA} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {BB97ECF4-9A84-433F-A80B-2A3285BDD1D5} diff --git a/framework/src/Volo.Abp.Cli.Core/Volo.Abp.Cli.Core.csproj b/framework/src/Volo.Abp.Cli.Core/Volo.Abp.Cli.Core.csproj index 41cb647649..46b59109c7 100644 --- a/framework/src/Volo.Abp.Cli.Core/Volo.Abp.Cli.Core.csproj +++ b/framework/src/Volo.Abp.Cli.Core/Volo.Abp.Cli.Core.csproj @@ -17,6 +17,7 @@ + diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/AbpCliCoreModule.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/AbpCliCoreModule.cs index 2dd39cdfce..101e9134d7 100644 --- a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/AbpCliCoreModule.cs +++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/AbpCliCoreModule.cs @@ -22,7 +22,7 @@ namespace Volo.Abp.Cli { options.Commands["help"] = typeof(HelpCommand); options.Commands["new"] = typeof(NewCommand); - options.Commands["add"] = typeof(AddCommand); + options.Commands["add-module"] = typeof(AddModuleCommand); options.Commands["login"] = typeof(LoginCommand); options.Commands["logout"] = typeof(LogoutCommand); }); diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/CliService.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/CliService.cs index 502ab294e6..854eabea96 100644 --- a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/CliService.cs +++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/CliService.cs @@ -13,7 +13,10 @@ namespace Volo.Abp.Cli { public class CliService : ITransientDependency { - public static string Version => typeof(AbpCliCoreModule).Assembly.GetFileVersion(); + //public static string Version => typeof(AbpCliCoreModule).Assembly + // .GetFileVersion() + // .RemovePostFix(".0"); + public static string Version => "0.17.0.0"; public ILogger Logger { get; set; } diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/AddCommand.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/AddModuleCommand.cs similarity index 85% rename from framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/AddCommand.cs rename to framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/AddModuleCommand.cs index e5034725ba..6c5209c2e4 100644 --- a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/AddCommand.cs +++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/AddModuleCommand.cs @@ -7,16 +7,16 @@ using Volo.Abp.ProjectModification; namespace Volo.Abp.Cli.Commands { - public class AddCommand : IConsoleCommand, ITransientDependency + public class AddModuleCommand : IConsoleCommand, ITransientDependency { - public ILogger Logger { get; set; } + public ILogger Logger { get; set; } protected ModuleAdder ModuleAdder { get; } - public AddCommand(ModuleAdder moduleAdder) + public AddModuleCommand(ModuleAdder moduleAdder) { ModuleAdder = moduleAdder; - Logger = NullLogger.Instance; + Logger = NullLogger.Instance; } public async Task ExecuteAsync(CommandLineArgs commandLineArgs) diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/ProjectBuilding/Building/Steps/SwitchEntityFrameworkCoreToMongoDbStep.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/ProjectBuilding/Building/Steps/SwitchEntityFrameworkCoreToMongoDbStep.cs index 6b77808f55..b1f55b4ba7 100644 --- a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/ProjectBuilding/Building/Steps/SwitchEntityFrameworkCoreToMongoDbStep.cs +++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/ProjectBuilding/Building/Steps/SwitchEntityFrameworkCoreToMongoDbStep.cs @@ -21,9 +21,9 @@ namespace Volo.Abp.ProjectBuilding.Building.Steps var lines = file.GetLines(); for (var i = 0; i < lines.Length; i++) { - if (lines[i].Contains("ProjectReference") && lines[i].Contains("MyCompanyName.MyProjectName.EntityFrameworkCore")) + if (lines[i].Contains("ProjectReference") && lines[i].Contains("MyCompanyName.MyProjectName.EntityFrameworkCore.DbMigrations")) { - lines[i] = lines[i].Replace("EntityFrameworkCore", "MongoDB"); + lines[i] = lines[i].Replace("EntityFrameworkCore.DbMigrations", "MongoDB"); file.SetLines(lines); return; } diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/ProjectModification/AbpModuleFinder.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/ProjectModification/AbpModuleFinder.cs new file mode 100644 index 0000000000..ca02137154 --- /dev/null +++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/ProjectModification/AbpModuleFinder.cs @@ -0,0 +1,71 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace Volo.Abp.ProjectModification +{ + public class AbpModuleFinder + { + public List Find(string csprojFilePath) + { + var modules = new List(); + + var csFiles = GetAllCsFilesUnderDirectory(Path.GetDirectoryName(csprojFilePath), new List()); + + foreach (var csFile in csFiles) + { + try + { + var root = CSharpSyntaxTree.ParseText(File.ReadAllText(csFile)).GetRoot(); + + var namespaceSyntaxs = root.DescendantNodes().OfType(); + + foreach (var namespaceSyntax in namespaceSyntaxs) + { + var classDeclarationSyntaxs = namespaceSyntax.DescendantNodes().OfType(); + var syntaxsArray = classDeclarationSyntaxs as ClassDeclarationSyntax[] ?? classDeclarationSyntaxs.ToArray(); + + foreach (var classDeclaration in syntaxsArray) + { + var classDerivedFromAbpModule = classDeclaration.BaseList?.Types.FirstOrDefault(t => t.ToString().Contains("AbpModule")); + + if (classDerivedFromAbpModule != null) + { + modules.Add(csFile); + } + } + } + } + catch (Exception) + { + //ignored! + } + } + + return modules; + } + + private static List GetAllCsFilesUnderDirectory(string path, List allCsFileList) + { + var directory = new DirectoryInfo(path); + var files = directory.GetFiles("*.cs").Select(f => f.DirectoryName + "\\" + f.Name).ToList(); + + foreach (var s in files) + { + allCsFileList.Add(s); + } + + var directories = directory.GetDirectories().Select(d => path + "\\" + d.Name).ToList(); + + foreach (var subDirectory in directories) + { + allCsFileList = GetAllCsFilesUnderDirectory(subDirectory, allCsFileList); + } + + return allCsFileList; + } + } +} diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/ProjectModification/DependsOnAdder.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/ProjectModification/DependsOnAdder.cs new file mode 100644 index 0000000000..d1dfd86d0a --- /dev/null +++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/ProjectModification/DependsOnAdder.cs @@ -0,0 +1,45 @@ +using System; +using System.IO; +using System.Linq; + +namespace Volo.Abp.ProjectModification +{ + public class DependsOnAdder + { + public void Add(string path, string module) + { + ParseModuleNameAndNameSpace(module, out var nameSpace, out var moduleName); + + var file = File.ReadAllText(path); + + var indexOfEndingPublicClass = file.IndexOf("public class", StringComparison.Ordinal); + + var dependsOnAttribute = "[DependsOn(" + moduleName + ")]" + Environment.NewLine + " "; + + file = file.Insert(indexOfEndingPublicClass, dependsOnAttribute); + file = file.Insert(0, GetUsingStatement(nameSpace) + Environment.NewLine); //TODO: Add as the last item in the using list! + + File.WriteAllText(path, file); + } + + private void ParseModuleNameAndNameSpace(string module, out string nameSpace, out string moduleName) + { + var words = module?.Split('.'); + + if (words == null || words.Length <= 1) + { + nameSpace = null; + moduleName = module; + return; + } + + moduleName = words[words.Length - 1]; + nameSpace = string.Join(".", words.Take(words.Length - 1)); + } + + private string GetUsingStatement(string nameSpace) + { + return "using " + nameSpace + ";"; + } + } +} diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/ProjectModification/ModuleAdder.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/ProjectModification/ModuleAdder.cs index b0e5f2a353..d6da6fea68 100644 --- a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/ProjectModification/ModuleAdder.cs +++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/ProjectModification/ModuleAdder.cs @@ -15,12 +15,12 @@ namespace Volo.Abp.ProjectModification { public ILogger Logger { get; set; } - protected ProjectModuleAdder ProjectModuleAdder { get; } + protected ProjectNugetPackageAdder ProjectNugetPackageAdder { get; } protected SolutionModuleAdder SolutionModuleAdder { get; } - public ModuleAdder(ProjectModuleAdder projectModuleAdder, SolutionModuleAdder solutionModuleAdder) + public ModuleAdder(ProjectNugetPackageAdder projectNugetPackageAdder, SolutionModuleAdder solutionModuleAdder) { - ProjectModuleAdder = projectModuleAdder; + ProjectNugetPackageAdder = projectNugetPackageAdder; SolutionModuleAdder = solutionModuleAdder; Logger = NullLogger.Instance; @@ -43,7 +43,7 @@ namespace Volo.Abp.ProjectModification if (!args.ProjectFile.IsNullOrEmpty()) { - await ProjectModuleAdder.AddAsync(args.ProjectFile, args.ModuleName); + await ProjectNugetPackageAdder.AddAsync(args.ProjectFile, args.ModuleName); } if (!args.SolutionFile.IsNullOrEmpty()) diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/ProjectModification/NpmPackageInfo.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/ProjectModification/NpmPackageInfo.cs new file mode 100644 index 0000000000..09ee9a8042 --- /dev/null +++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/ProjectModification/NpmPackageInfo.cs @@ -0,0 +1,11 @@ +namespace Volo.Abp.ProjectModification +{ + public class NpmPackageInfo + { + public string Name { get; set; } + + public int ApplicationType { get; set; } //TODO: Enum? + + public bool IsPro { get; set; } + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/ProjectModification/NugetPackageInfo.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/ProjectModification/NugetPackageInfo.cs new file mode 100644 index 0000000000..59974f23d3 --- /dev/null +++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/ProjectModification/NugetPackageInfo.cs @@ -0,0 +1,13 @@ +namespace Volo.Abp.ProjectModification +{ + public class NugetPackageInfo + { + public string Name { get; set; } + + public string ModuleClass { get; set; } + + public int Target { get; set; } //TODO: Enum? + + public NpmPackageInfo DependedNpmPackage { get; set; } + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/ProjectModification/ProjectModuleAdder.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/ProjectModification/ProjectModuleAdder.cs deleted file mode 100644 index 1960da10a5..0000000000 --- a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/ProjectModification/ProjectModuleAdder.cs +++ /dev/null @@ -1,13 +0,0 @@ -using System.Threading.Tasks; -using Volo.Abp.DependencyInjection; - -namespace Volo.Abp.ProjectModification -{ - public class ProjectModuleAdder : ITransientDependency - { - public async Task AddAsync(string projectFile, string moduleName) - { - - } - } -} \ No newline at end of file diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/ProjectModification/ProjectNpmPackageAdder.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/ProjectModification/ProjectNpmPackageAdder.cs new file mode 100644 index 0000000000..e6566ce21f --- /dev/null +++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/ProjectModification/ProjectNpmPackageAdder.cs @@ -0,0 +1,42 @@ +using System.Diagnostics; +using System.IO; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using Volo.Abp.DependencyInjection; + +namespace Volo.Abp.ProjectModification +{ + public class ProjectNpmPackageAdder : ITransientDependency + { + public ILogger Logger { get; set; } + + public ProjectNpmPackageAdder() + { + Logger = NullLogger.Instance; + } + + public Task AddAsync(string directory, NpmPackageInfo npmPackage) + { + var packageJsonFilePath = Path.Combine(directory, "package.json"); + if (!File.Exists(packageJsonFilePath)) + { + return Task.CompletedTask; + } + + Logger.LogInformation($"Installing '{npmPackage.Name}' package to the project '{packageJsonFilePath}'..."); + + Logger.LogInformation("yarn add " + npmPackage.Name + "..."); + var procStartInfo = new ProcessStartInfo("cmd.exe", "/C yarn add " + npmPackage.Name); + procStartInfo.WindowStyle = ProcessWindowStyle.Normal; + Process.Start(procStartInfo).WaitForExit(); + + Logger.LogInformation("gulp..."); + procStartInfo = new ProcessStartInfo("cmd.exe", "/C gulp"); + procStartInfo.WindowStyle = ProcessWindowStyle.Normal; + Process.Start(procStartInfo).WaitForExit(); + + return Task.CompletedTask; + } + } +} diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/ProjectModification/ProjectNugetPackageAdder.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/ProjectModification/ProjectNugetPackageAdder.cs new file mode 100644 index 0000000000..bf9c0f25f5 --- /dev/null +++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/ProjectModification/ProjectNugetPackageAdder.cs @@ -0,0 +1,81 @@ +using System; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using Volo.Abp.DependencyInjection; +using Volo.Abp.Json; + +namespace Volo.Abp.ProjectModification +{ + public class ProjectNugetPackageAdder : ITransientDependency + { + public ILogger Logger { get; set; } + + protected IJsonSerializer JsonSerializer { get; } + protected ProjectNpmPackageAdder NpmPackageAdder { get; } + + public ProjectNugetPackageAdder( + IJsonSerializer jsonSerializer, + ProjectNpmPackageAdder npmPackageAdder) + { + JsonSerializer = jsonSerializer; + NpmPackageAdder = npmPackageAdder; + Logger = NullLogger.Instance; + } + + public async Task AddAsync(string projectFile, string packageName) + { + var packageInfo = await FindNugetPackageInfoAsync(packageName); + if (packageInfo == null) + { + return; + } + + Logger.LogInformation($"Installing '{packageName}' package to the project '{Path.GetFileNameWithoutExtension(projectFile)}'..."); + var procStartInfo = new ProcessStartInfo("dotnet", "add package " + packageName); + Process.Start(procStartInfo).WaitForExit(); + + var moduleFile = new AbpModuleFinder().Find(projectFile).First(); + new DependsOnAdder().Add(moduleFile, packageInfo.ModuleClass); + + if (packageInfo.DependedNpmPackage != null) + { + await NpmPackageAdder.AddAsync(Path.GetDirectoryName(projectFile), packageInfo.DependedNpmPackage); + } + + Logger.LogInformation("Successfully installed."); + } + + private async Task FindNugetPackageInfoAsync(string moduleName) + { + using (var client = new HttpClient()) + { + var url = "https://localhost:44328/api/app/nugetPackage/byName/?name=" + moduleName; + + var response = await client.GetAsync(url); + + if (!response.IsSuccessStatusCode) + { + if (response.StatusCode == HttpStatusCode.NotFound) + { + Logger.LogError($"ERROR: '{moduleName}' module could not be found."); + } + else + { + Logger.LogError($"ERROR: Remote server returns '{response.StatusCode}'"); + } + + return null; + } + + var responseContent = await response.Content.ReadAsStringAsync(); + return JsonSerializer.Deserialize(responseContent); + } + } + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.Specifications/Volo.Abp.Specifications.csproj b/framework/src/Volo.Abp.Specifications/Volo.Abp.Specifications.csproj new file mode 100644 index 0000000000..97c236cfc5 --- /dev/null +++ b/framework/src/Volo.Abp.Specifications/Volo.Abp.Specifications.csproj @@ -0,0 +1,20 @@ + + + + + + netstandard2.0 + Volo.Abp.Specifications + Volo.Abp.Specifications + $(AssetTargetFallback);portable-net45+win8+wp8+wpa81; + false + false + false + + + + + + + + diff --git a/framework/src/Volo.Abp.Specifications/Volo/Abp/Specifications/AbpSpecificationsModule.cs b/framework/src/Volo.Abp.Specifications/Volo/Abp/Specifications/AbpSpecificationsModule.cs new file mode 100644 index 0000000000..6f55973be1 --- /dev/null +++ b/framework/src/Volo.Abp.Specifications/Volo/Abp/Specifications/AbpSpecificationsModule.cs @@ -0,0 +1,9 @@ +using Volo.Abp.Modularity; + +namespace Volo.Abp.Specifications +{ + public class AbpSpecificationsModule : AbpModule + { + + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.Specifications/Volo/Abp/Specifications/AndNotSpecification.cs b/framework/src/Volo.Abp.Specifications/Volo/Abp/Specifications/AndNotSpecification.cs new file mode 100644 index 0000000000..e0d51bd5c3 --- /dev/null +++ b/framework/src/Volo.Abp.Specifications/Volo/Abp/Specifications/AndNotSpecification.cs @@ -0,0 +1,36 @@ +using System; +using System.Linq.Expressions; + +namespace Volo.Abp.Specifications +{ + /// + /// Represents the combined specification which indicates that the first specification + /// can be satisifed by the given object whereas the second one cannot. + /// + /// The type of the object to which the specification is applied. + public class AndNotSpecification : CompositeSpecification + { + /// + /// Constructs a new instance of class. + /// + /// The first specification. + /// The second specification. + public AndNotSpecification(ISpecification left, ISpecification right) : base(left, right) + { + } + + /// + /// Gets the LINQ expression which represents the current specification. + /// + /// The LINQ expression. + public override Expression> ToExpression() + { + var rightExpression = Right.ToExpression(); + + var bodyNot = Expression.Not(rightExpression.Body); + var bodyNotExpression = Expression.Lambda>(bodyNot, rightExpression.Parameters); + + return Left.ToExpression().And(bodyNotExpression); + } + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.Specifications/Volo/Abp/Specifications/AndSpecification.cs b/framework/src/Volo.Abp.Specifications/Volo/Abp/Specifications/AndSpecification.cs new file mode 100644 index 0000000000..621287d634 --- /dev/null +++ b/framework/src/Volo.Abp.Specifications/Volo/Abp/Specifications/AndSpecification.cs @@ -0,0 +1,31 @@ +using System; +using System.Linq.Expressions; + +namespace Volo.Abp.Specifications +{ + /// + /// Represents the combined specification which indicates that both of the given + /// specifications should be satisfied by the given object. + /// + /// The type of the object to which the specification is applied. + public class AndSpecification : CompositeSpecification + { + /// + /// Constructs a new instance of class. + /// + /// The first specification. + /// The second specification. + public AndSpecification(ISpecification left, ISpecification right) : base(left, right) + { + } + + /// + /// Gets the LINQ expression which represents the current specification. + /// + /// The LINQ expression. + public override Expression> ToExpression() + { + return Left.ToExpression().And(Right.ToExpression()); + } + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.Specifications/Volo/Abp/Specifications/AnySpecification.cs b/framework/src/Volo.Abp.Specifications/Volo/Abp/Specifications/AnySpecification.cs new file mode 100644 index 0000000000..caf6a5f094 --- /dev/null +++ b/framework/src/Volo.Abp.Specifications/Volo/Abp/Specifications/AnySpecification.cs @@ -0,0 +1,22 @@ +using System; +using System.Linq.Expressions; + +namespace Volo.Abp.Specifications +{ + /// + /// Represents the specification that can be satisfied by the given object + /// in any circumstance. + /// + /// The type of the object to which the specification is applied. + public sealed class AnySpecification : Specification + { + /// + /// Gets the LINQ expression which represents the current specification. + /// + /// The LINQ expression. + public override Expression> ToExpression() + { + return o => true; + } + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.Specifications/Volo/Abp/Specifications/CompositeSpecification.cs b/framework/src/Volo.Abp.Specifications/Volo/Abp/Specifications/CompositeSpecification.cs new file mode 100644 index 0000000000..01c1e46d33 --- /dev/null +++ b/framework/src/Volo.Abp.Specifications/Volo/Abp/Specifications/CompositeSpecification.cs @@ -0,0 +1,30 @@ +namespace Volo.Abp.Specifications +{ + /// + /// Represents the base class for composite specifications. + /// + /// The type of the object to which the specification is applied. + public abstract class CompositeSpecification : Specification, ICompositeSpecification + { + /// + /// Constructs a new instance of class. + /// + /// The first specification. + /// The second specification. + protected CompositeSpecification(ISpecification left, ISpecification right) + { + Left = left; + Right = right; + } + + /// + /// Gets the first specification. + /// + public ISpecification Left { get; } + + /// + /// Gets the second specification. + /// + public ISpecification Right { get; } + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.Specifications/Volo/Abp/Specifications/ExpressionFuncExtender.cs b/framework/src/Volo.Abp.Specifications/Volo/Abp/Specifications/ExpressionFuncExtender.cs new file mode 100644 index 0000000000..dd169d5320 --- /dev/null +++ b/framework/src/Volo.Abp.Specifications/Volo/Abp/Specifications/ExpressionFuncExtender.cs @@ -0,0 +1,55 @@ +using System; +using System.Linq; +using System.Linq.Expressions; + +namespace Volo.Abp.Specifications +{ + /// + /// Represents the extender for Expression[Func[T, bool]] type. + /// This is part of the solution which solves + /// the expression parameter problem when going to Entity Framework. + /// For more information about this solution please refer to http://blogs.msdn.com/b/meek/archive/2008/05/02/linq-to-entities-combining-predicates.aspx. + /// + public static class ExpressionFuncExtender + { + private static Expression Compose(this Expression first, Expression second, + Func merge) + { + // build parameter map (from parameters of second to parameters of first) + var map = first.Parameters.Select((f, i) => new {f, s = second.Parameters[i]}) + .ToDictionary(p => p.s, p => p.f); + + // replace parameters in the second lambda expression with parameters from the first + var secondBody = ParameterRebinder.ReplaceParameters(map, second.Body); + + // apply composition of lambda expression bodies to parameters from the first expression + return Expression.Lambda(merge(first.Body, secondBody), first.Parameters); + } + + /// + /// Combines two given expressions by using the AND semantics. + /// + /// The type of the object. + /// The first part of the expression. + /// The second part of the expression. + /// The combined expression. + public static Expression> And(this Expression> first, + Expression> second) + { + return first.Compose(second, Expression.AndAlso); + } + + /// + /// Combines two given expressions by using the OR semantics. + /// + /// The type of the object. + /// The first part of the expression. + /// The second part of the expression. + /// The combined expression. + public static Expression> Or(this Expression> first, + Expression> second) + { + return first.Compose(second, Expression.OrElse); + } + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.Specifications/Volo/Abp/Specifications/ExpressionSpecification.cs b/framework/src/Volo.Abp.Specifications/Volo/Abp/Specifications/ExpressionSpecification.cs new file mode 100644 index 0000000000..14c1bff040 --- /dev/null +++ b/framework/src/Volo.Abp.Specifications/Volo/Abp/Specifications/ExpressionSpecification.cs @@ -0,0 +1,34 @@ +using System; +using System.Linq.Expressions; + +namespace Volo.Abp.Specifications +{ + /// + /// Represents the specification which is represented by the corresponding + /// LINQ expression. + /// + /// The type of the object to which the specification is applied. + public class ExpressionSpecification : Specification + { + private readonly Expression> _expression; + + /// + /// Initializes a new instance of ExpressionSpecification<T> class. + /// + /// The LINQ expression which represents the current + /// specification. + public ExpressionSpecification(Expression> expression) + { + _expression = expression; + } + + /// + /// Gets the LINQ expression which represents the current specification. + /// + /// The LINQ expression. + public override Expression> ToExpression() + { + return _expression; + } + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.Specifications/Volo/Abp/Specifications/ICompositeSpecification.cs b/framework/src/Volo.Abp.Specifications/Volo/Abp/Specifications/ICompositeSpecification.cs new file mode 100644 index 0000000000..2c73409d34 --- /dev/null +++ b/framework/src/Volo.Abp.Specifications/Volo/Abp/Specifications/ICompositeSpecification.cs @@ -0,0 +1,19 @@ +namespace Volo.Abp.Specifications +{ + /// + /// Represents that the implemented classes are composite specifications. + /// + /// The type of the object to which the specification is applied. + public interface ICompositeSpecification : ISpecification + { + /// + /// Gets the left side of the specification. + /// + ISpecification Left { get; } + + /// + /// Gets the right side of the specification. + /// + ISpecification Right { get; } + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.Specifications/Volo/Abp/Specifications/ISpecification.cs b/framework/src/Volo.Abp.Specifications/Volo/Abp/Specifications/ISpecification.cs new file mode 100644 index 0000000000..d19e28347e --- /dev/null +++ b/framework/src/Volo.Abp.Specifications/Volo/Abp/Specifications/ISpecification.cs @@ -0,0 +1,28 @@ +using System; +using System.Linq.Expressions; + +namespace Volo.Abp.Specifications +{ + /// + /// Represents that the implemented classes are specifications. For more + /// information about the specification pattern, please refer to + /// http://martinfowler.com/apsupp/spec.pdf. + /// + /// The type of the object to which the specification is applied. + public interface ISpecification + { + /// + /// Returns a value which indicates whether the specification + /// is satisfied by the given object. + /// + /// The object to which the specification is applied. + /// True if the specification is satisfied, otherwise false. + bool IsSatisfiedBy(T obj); + + /// + /// Gets the LINQ expression which represents the current specification. + /// + /// The LINQ expression. + Expression> ToExpression(); + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.Specifications/Volo/Abp/Specifications/ISpecificationParser.cs b/framework/src/Volo.Abp.Specifications/Volo/Abp/Specifications/ISpecificationParser.cs new file mode 100644 index 0000000000..d2c61034e0 --- /dev/null +++ b/framework/src/Volo.Abp.Specifications/Volo/Abp/Specifications/ISpecificationParser.cs @@ -0,0 +1,19 @@ +namespace Volo.Abp.Specifications +{ + /// + /// Represents that the implemented classes are specification parsers that + /// parses the given specification to a domain specific criteria object, such + /// as the ICriteria instance in NHibernate. + /// + /// The type of the domain specific criteria. + public interface ISpecificationParser + { + /// + /// Parses the given specification to a domain specific criteria object. + /// + /// The type of the object to which the specification is applied. + /// The specified specification instance. + /// The instance of the domain specific criteria. + TCriteria Parse(ISpecification specification); + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.Specifications/Volo/Abp/Specifications/NoneSpecification.cs b/framework/src/Volo.Abp.Specifications/Volo/Abp/Specifications/NoneSpecification.cs new file mode 100644 index 0000000000..fc2e74fdd6 --- /dev/null +++ b/framework/src/Volo.Abp.Specifications/Volo/Abp/Specifications/NoneSpecification.cs @@ -0,0 +1,22 @@ +using System; +using System.Linq.Expressions; + +namespace Volo.Abp.Specifications +{ + /// + /// Represents the specification that can be satisfied by the given object + /// in no circumstance. + /// + /// The type of the object to which the specification is applied. + public sealed class NoneSpecification : Specification + { + /// + /// Gets the LINQ expression which represents the current specification. + /// + /// The LINQ expression. + public override Expression> ToExpression() + { + return o => false; + } + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.Specifications/Volo/Abp/Specifications/NotSpecification.cs b/framework/src/Volo.Abp.Specifications/Volo/Abp/Specifications/NotSpecification.cs new file mode 100644 index 0000000000..34658885cb --- /dev/null +++ b/framework/src/Volo.Abp.Specifications/Volo/Abp/Specifications/NotSpecification.cs @@ -0,0 +1,36 @@ +using System; +using System.Linq.Expressions; + +namespace Volo.Abp.Specifications +{ + /// + /// Represents the specification which indicates the semantics opposite to the given specification. + /// + /// The type of the object to which the specification is applied. + public class NotSpecification : Specification + { + private readonly ISpecification _specification; + + /// + /// Initializes a new instance of class. + /// + /// The specification to be reversed. + public NotSpecification(ISpecification specification) + { + _specification = specification; + } + + /// + /// Gets the LINQ expression which represents the current specification. + /// + /// The LINQ expression. + public override Expression> ToExpression() + { + var expression = _specification.ToExpression(); + return Expression.Lambda>( + Expression.Not(expression.Body), + expression.Parameters + ); + } + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.Specifications/Volo/Abp/Specifications/OrSpecification.cs b/framework/src/Volo.Abp.Specifications/Volo/Abp/Specifications/OrSpecification.cs new file mode 100644 index 0000000000..a6c85f8326 --- /dev/null +++ b/framework/src/Volo.Abp.Specifications/Volo/Abp/Specifications/OrSpecification.cs @@ -0,0 +1,31 @@ +using System; +using System.Linq.Expressions; + +namespace Volo.Abp.Specifications +{ + /// + /// Represents the combined specification which indicates that either of the given + /// specification should be satisfied by the given object. + /// + /// The type of the object to which the specification is applied. + public class OrSpecification : CompositeSpecification + { + /// + /// Initializes a new instance of class. + /// + /// The first specification. + /// The second specification. + public OrSpecification(ISpecification left, ISpecification right) : base(left, right) + { + } + + /// + /// Gets the LINQ expression which represents the current specification. + /// + /// The LINQ expression. + public override Expression> ToExpression() + { + return Left.ToExpression().Or(Right.ToExpression()); + } + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.Specifications/Volo/Abp/Specifications/ParameterRebinder.cs b/framework/src/Volo.Abp.Specifications/Volo/Abp/Specifications/ParameterRebinder.cs new file mode 100644 index 0000000000..603e80cf69 --- /dev/null +++ b/framework/src/Volo.Abp.Specifications/Volo/Abp/Specifications/ParameterRebinder.cs @@ -0,0 +1,37 @@ +using System.Collections.Generic; +using System.Linq.Expressions; + +namespace Volo.Abp.Specifications +{ + /// + /// Represents the parameter rebinder used for rebinding the parameters + /// for the given expressions. This is part of the solution which solves + /// the expression parameter problem when going to Entity Framework. + /// For more information about this solution please refer to http://blogs.msdn.com/b/meek/archive/2008/05/02/linq-to-entities-combining-predicates.aspx. + /// + internal class ParameterRebinder : ExpressionVisitor + { + private readonly Dictionary _map; + + internal ParameterRebinder(Dictionary map) + { + _map = map ?? new Dictionary(); + } + + internal static Expression ReplaceParameters(Dictionary map, + Expression exp) + { + return new ParameterRebinder(map).Visit(exp); + } + + protected override Expression VisitParameter(ParameterExpression p) + { + if (_map.TryGetValue(p, out var replacement)) + { + p = replacement; + } + + return base.VisitParameter(p); + } + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.Specifications/Volo/Abp/Specifications/Specification.cs b/framework/src/Volo.Abp.Specifications/Volo/Abp/Specifications/Specification.cs new file mode 100644 index 0000000000..b1ab72caea --- /dev/null +++ b/framework/src/Volo.Abp.Specifications/Volo/Abp/Specifications/Specification.cs @@ -0,0 +1,38 @@ +using System; +using System.Linq.Expressions; + +namespace Volo.Abp.Specifications +{ + /// + /// Represents the base class for specifications. + /// + /// The type of the object to which the specification is applied. + public abstract class Specification : ISpecification + { + /// + /// Returns a value which indicates whether the specification + /// is satisfied by the given object. + /// + /// The object to which the specification is applied. + /// True if the specification is satisfied, otherwise false. + public virtual bool IsSatisfiedBy(T obj) + { + return ToExpression().Compile()(obj); + } + + /// + /// Gets the LINQ expression which represents the current specification. + /// + /// The LINQ expression. + public abstract Expression> ToExpression(); + + /// + /// Implicitly converts a specification to expression. + /// + /// + public static implicit operator Expression>(Specification specification) + { + return specification.ToExpression(); + } + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.Specifications/Volo/Abp/Specifications/SpecificationExtensions.cs b/framework/src/Volo.Abp.Specifications/Volo/Abp/Specifications/SpecificationExtensions.cs new file mode 100644 index 0000000000..229b3e7a72 --- /dev/null +++ b/framework/src/Volo.Abp.Specifications/Volo/Abp/Specifications/SpecificationExtensions.cs @@ -0,0 +1,72 @@ +using JetBrains.Annotations; + +namespace Volo.Abp.Specifications +{ + public static class SpecificationExtensions + { + /// + /// Combines the current specification instance with another specification instance + /// and returns the combined specification which represents that both the current and + /// the given specification must be satisfied by the given object. + /// + /// The specification + /// The specification instance with which the current specification is combined. + /// The combined specification instance. + public static ISpecification And([NotNull] this ISpecification specification, + [NotNull] ISpecification other) + { + Check.NotNull(specification, nameof(specification)); + Check.NotNull(other, nameof(other)); + + return new AndSpecification(specification, other); + } + + /// + /// Combines the current specification instance with another specification instance + /// and returns the combined specification which represents that either the current or + /// the given specification should be satisfied by the given object. + /// + /// The specification + /// The specification instance with which the current specification + /// is combined. + /// The combined specification instance. + public static ISpecification Or([NotNull] this ISpecification specification, + [NotNull] ISpecification other) + { + Check.NotNull(specification, nameof(specification)); + Check.NotNull(other, nameof(other)); + + return new OrSpecification(specification, other); + } + + /// + /// Combines the current specification instance with another specification instance + /// and returns the combined specification which represents that the current specification + /// should be satisfied by the given object but the specified specification should not. + /// + /// The specification + /// The specification instance with which the current specification + /// is combined. + /// The combined specification instance. + public static ISpecification AndNot([NotNull] this ISpecification specification, + [NotNull] ISpecification other) + { + Check.NotNull(specification, nameof(specification)); + Check.NotNull(other, nameof(other)); + + return new AndNotSpecification(specification, other); + } + + /// + /// Reverses the current specification instance and returns a specification which represents + /// the semantics opposite to the current specification. + /// + /// The reversed specification instance. + public static ISpecification Not([NotNull] this ISpecification specification) + { + Check.NotNull(specification, nameof(specification)); + + return new NotSpecification(specification); + } + } +} \ No newline at end of file diff --git a/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/Repositories/Repository_Specifications_Tests.cs b/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/Repositories/Repository_Specifications_Tests.cs new file mode 100644 index 0000000000..d9de34f1ce --- /dev/null +++ b/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/Repositories/Repository_Specifications_Tests.cs @@ -0,0 +1,8 @@ +using Volo.Abp.TestApp.Testing; + +namespace Volo.Abp.EntityFrameworkCore.Repositories +{ + public class Repository_Specifications_Tests : Repository_Specifications_Tests + { + } +} \ No newline at end of file diff --git a/framework/test/Volo.Abp.MemoryDb.Tests/Volo/Abp/MemoryDb/Repositories/Repository_Specifications_Tests.cs b/framework/test/Volo.Abp.MemoryDb.Tests/Volo/Abp/MemoryDb/Repositories/Repository_Specifications_Tests.cs new file mode 100644 index 0000000000..e3183127a0 --- /dev/null +++ b/framework/test/Volo.Abp.MemoryDb.Tests/Volo/Abp/MemoryDb/Repositories/Repository_Specifications_Tests.cs @@ -0,0 +1,8 @@ +using Volo.Abp.TestApp.Testing; + +namespace Volo.Abp.MemoryDb.Repositories +{ + public class Repository_Specifications_Tests : Repository_Specifications_Tests + { + } +} \ No newline at end of file diff --git a/framework/test/Volo.Abp.MongoDB.Tests/Volo/Abp/MongoDB/Repositories/Repository_Specifications_Tests.cs b/framework/test/Volo.Abp.MongoDB.Tests/Volo/Abp/MongoDB/Repositories/Repository_Specifications_Tests.cs new file mode 100644 index 0000000000..6529b4c5a9 --- /dev/null +++ b/framework/test/Volo.Abp.MongoDB.Tests/Volo/Abp/MongoDB/Repositories/Repository_Specifications_Tests.cs @@ -0,0 +1,8 @@ +using Volo.Abp.TestApp.Testing; + +namespace Volo.Abp.MongoDB.Repositories +{ + public class Repository_Specifications_Tests : Repository_Specifications_Tests + { + } +} \ No newline at end of file diff --git a/framework/test/Volo.Abp.Specifications.Tests/Volo.Abp.Specifications.Tests.csproj b/framework/test/Volo.Abp.Specifications.Tests/Volo.Abp.Specifications.Tests.csproj new file mode 100644 index 0000000000..6f2cbe09f8 --- /dev/null +++ b/framework/test/Volo.Abp.Specifications.Tests/Volo.Abp.Specifications.Tests.csproj @@ -0,0 +1,21 @@ + + + + netcoreapp2.2 + latest + Volo.Abp.Specifications.Tests + Volo.Abp.Specifications.Tests + true + false + false + false + + + + + + + + + + \ No newline at end of file diff --git a/framework/test/Volo.Abp.Specifications.Tests/Volo/Abp/Specifications/Specifications_Tests.cs b/framework/test/Volo.Abp.Specifications.Tests/Volo/Abp/Specifications/Specifications_Tests.cs new file mode 100644 index 0000000000..dbe4fc6b5d --- /dev/null +++ b/framework/test/Volo.Abp.Specifications.Tests/Volo/Abp/Specifications/Specifications_Tests.cs @@ -0,0 +1,191 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Text; +using Shouldly; +using Xunit; + +namespace Volo.Abp.Specifications +{ + public class Specification_Tests + { + private readonly IQueryable _customers; + + public Specification_Tests() + { + _customers = new List + { + new Customer("John", 17, 47000, "England"), + new Customer("Tuana", 2, 500, "Turkey"), + new Customer("Martin", 43, 16000, "USA"), + new Customer("Lee", 32, 24502, "China"), + new Customer("Douglas", 42, 42000, "England"), + new Customer("Abelard", 14, 2332, "German"), + new Customer("Neo", 16, 120000, "USA"), + new Customer("Daan", 39, 6000, "Netherlands"), + new Customer("Alessandro", 22, 8271, "Italy"), + new Customer("Noah", 33, 82192, "Belgium") + }.AsQueryable(); + } + + [Fact] + public void Any_Should_Return_All() + { + _customers + .Where(new AnySpecification()) //Implicitly converted to Expression! + .Count() + .ShouldBe(_customers.Count()); + } + + [Fact] + public void None_Should_Return_Empty() + { + _customers + .Where(new NoneSpecification().ToExpression()) + .Count() + .ShouldBe(0); + } + + [Fact] + public void Not_Should_Return_Reverse_Result() + { + _customers + .Where(new EuropeanCustomerSpecification().Not().ToExpression()) + .Count() + .ShouldBe(3); + } + + [Fact] + public void Should_Support_Native_Expressions_And_Combinations() + { + _customers + .Where(new ExpressionSpecification(c => c.Age >= 18).ToExpression()) + .Count() + .ShouldBe(6); + + _customers + .Where(new EuropeanCustomerSpecification().And(new ExpressionSpecification(c => c.Age >= 18)).ToExpression()) + .Count() + .ShouldBe(4); + } + + [Fact] + public void CustomSpecification_Test() + { + _customers + .Where(new EuropeanCustomerSpecification().ToExpression()) + .Count() + .ShouldBe(7); + + _customers + .Where(new Age18PlusCustomerSpecification().ToExpression()) + .Count() + .ShouldBe(6); + + _customers + .Where(new BalanceCustomerSpecification(10000, 30000).ToExpression()) + .Count() + .ShouldBe(2); + + _customers + .Where(new PremiumCustomerSpecification().ToExpression()) + .Count() + .ShouldBe(3); + } + + [Fact] + public void IsSatisfiedBy_Tests() + { + new PremiumCustomerSpecification().IsSatisfiedBy(new Customer("David", 49, 55000, "USA")).ShouldBeTrue(); + + new PremiumCustomerSpecification().IsSatisfiedBy(new Customer("David", 49, 200, "USA")).ShouldBeFalse(); + new PremiumCustomerSpecification().IsSatisfiedBy(new Customer("David", 12, 55000, "USA")).ShouldBeFalse(); + } + + [Fact] + public void CustomSpecification_Composite_Tests() + { + _customers + .Where(new EuropeanCustomerSpecification().And(new Age18PlusCustomerSpecification()).ToExpression()) + .Count() + .ShouldBe(4); + + _customers + .Where(new EuropeanCustomerSpecification().Not().And(new Age18PlusCustomerSpecification()).ToExpression()) + .Count() + .ShouldBe(2); + + _customers + .Where(new Age18PlusCustomerSpecification().AndNot(new EuropeanCustomerSpecification()).ToExpression()) + .Count() + .ShouldBe(2); + } + + private class Customer + { + public string Name { get; private set; } + + public byte Age { get; private set; } + + public long Balance { get; private set; } + + public string Location { get; private set; } + + public Customer(string name, byte age, long balance, string location) + { + Name = name; + Age = age; + Balance = balance; + Location = location; + } + } + + private class EuropeanCustomerSpecification : Specification + { + public override Expression> ToExpression() + { + return c => c.Location == "England" || + c.Location == "Turkey" || + c.Location == "German" || + c.Location == "Netherlands" || + c.Location == "Italy" || + c.Location == "Belgium"; + } + } + + private class Age18PlusCustomerSpecification : Specification + { + public override Expression> ToExpression() + { + return c => c.Age >= 18; + } + } + + private class BalanceCustomerSpecification : Specification + { + public int MinBalance { get; } + + public int MaxBalance { get; } + + public BalanceCustomerSpecification(int minBalance, int maxBalance) + { + MinBalance = minBalance; + MaxBalance = maxBalance; + } + + public override Expression> ToExpression() + { + return c => c.Balance >= MinBalance && c.Balance <= MaxBalance; + } + } + + private class PremiumCustomerSpecification : AndSpecification + { + public PremiumCustomerSpecification() + : base(new Age18PlusCustomerSpecification(), new BalanceCustomerSpecification(20000, int.MaxValue)) + { + } + } + } +} diff --git a/framework/test/Volo.Abp.TestApp/Volo.Abp.TestApp.csproj b/framework/test/Volo.Abp.TestApp/Volo.Abp.TestApp.csproj index 0cb921f63a..4e7b58ee84 100644 --- a/framework/test/Volo.Abp.TestApp/Volo.Abp.TestApp.csproj +++ b/framework/test/Volo.Abp.TestApp/Volo.Abp.TestApp.csproj @@ -16,6 +16,7 @@ + diff --git a/framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Testing/Repository_Specifications_Tests.cs b/framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Testing/Repository_Specifications_Tests.cs new file mode 100644 index 0000000000..a3e7907135 --- /dev/null +++ b/framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Testing/Repository_Specifications_Tests.cs @@ -0,0 +1,40 @@ +using System; +using System.Linq; +using System.Linq.Expressions; +using Shouldly; +using Volo.Abp.Domain.Repositories; +using Volo.Abp.Modularity; +using Volo.Abp.Specifications; +using Volo.Abp.TestApp.Domain; +using Xunit; + +namespace Volo.Abp.TestApp.Testing +{ + public abstract class Repository_Specifications_Tests : TestAppTestBase + where TStartupModule : IAbpModule + { + protected readonly IRepository CityRepository; + + protected Repository_Specifications_Tests() + { + CityRepository = GetRequiredService>(); + } + + [Fact] + public void SpecificationWithRepository_Test() + { + WithUnitOfWork(() => + { + CityRepository.Count(new CitySpecification().ToExpression()).ShouldBe(1); + }); + } + } + + public class CitySpecification : Specification + { + public override Expression> ToExpression() + { + return city => city.Name == "Istanbul"; + } + } +} \ No newline at end of file diff --git a/nupkg/common.ps1 b/nupkg/common.ps1 index 5f6944b1a6..59329e6af0 100644 --- a/nupkg/common.ps1 +++ b/nupkg/common.ps1 @@ -88,6 +88,7 @@ $projects = ( "framework/src/Volo.Abp.Serialization", "framework/src/Volo.Abp.Settings", "framework/src/Volo.Abp.Sms", + "framework/src/Volo.Abp.Specifications", "framework/src/Volo.Abp.TestBase", "framework/src/Volo.Abp.Threading", "framework/src/Volo.Abp.Timing", diff --git a/templates/mvc/src/MyCompanyName.MyProjectName.Web/MyProjectNameWebModule.cs b/templates/mvc/src/MyCompanyName.MyProjectName.Web/MyProjectNameWebModule.cs index 330cf78fe2..c06daeed28 100644 --- a/templates/mvc/src/MyCompanyName.MyProjectName.Web/MyProjectNameWebModule.cs +++ b/templates/mvc/src/MyCompanyName.MyProjectName.Web/MyProjectNameWebModule.cs @@ -32,10 +32,9 @@ using Volo.Abp.UI; using Volo.Abp.UI.Navigation; using Volo.Abp.VirtualFileSystem; using Volo.Abp.PermissionManagement; +using Volo.Abp.TenantManagement.Web; // using Volo.Abp.EntityFrameworkCore; -using Volo.Abp.TenantManagement.Web; - // namespace MyCompanyName.MyProjectName