From 5f38e4bf316dab7ebfada571c20e2c2699b4daa7 Mon Sep 17 00:00:00 2001 From: YinChang Date: Sun, 22 Mar 2020 21:19:43 +0800 Subject: [PATCH 01/28] fix(ng.core) correct params of SetEnvironment Action --- npm/ng-packs/packages/core/src/lib/states/config.state.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/npm/ng-packs/packages/core/src/lib/states/config.state.ts b/npm/ng-packs/packages/core/src/lib/states/config.state.ts index 5269946391..6843fe2dc6 100644 --- a/npm/ng-packs/packages/core/src/lib/states/config.state.ts +++ b/npm/ng-packs/packages/core/src/lib/states/config.state.ts @@ -300,7 +300,7 @@ export class ConfigState { } @Action(SetEnvironment) - setEnvironment({ patchState }: StateContext, environment: Config.Environment) { + setEnvironment({ patchState }: StateContext, { environment }:SetEnvironment) { return patchState({ environment, }); From 63b764da8d4b46b8182ea63c46bf5d871f4148cc Mon Sep 17 00:00:00 2001 From: liangshiwei Date: Tue, 24 Mar 2020 20:52:09 +0800 Subject: [PATCH 02/28] Make CLI available when abp.io is offline --- .../ProjectBuilding/AbpIoSourceCodeStore.cs | 5 ++- .../Analyticses/CliAnalyticsCollect.cs | 41 ++++++++++++------- 2 files changed, 29 insertions(+), 17 deletions(-) diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/AbpIoSourceCodeStore.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/AbpIoSourceCodeStore.cs index 1ffdefbe61..23d8a8b5ff 100644 --- a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/AbpIoSourceCodeStore.cs +++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/AbpIoSourceCodeStore.cs @@ -51,7 +51,8 @@ namespace Volo.Abp.Cli.ProjectBuilding var latestVersion = await GetLatestSourceCodeVersionAsync(name, type); if (version == null) { - version = latestVersion; + version = latestVersion ?? throw new CliUsageException( + "The remote service is currently unavailable, please specify the version (like abp new Acme.BookStore -v 2.3.0(Make sure you have a template cache locally))!"); } var nugetVersion = (await GetTemplateNugetVersionAsync(name, type, version)) ?? version; @@ -122,7 +123,7 @@ namespace Volo.Abp.Cli.ProjectBuilding catch (Exception ex) { Console.WriteLine("Error occured while getting the latest version from {0} : {1}", url, ex.Message); - throw; + return null; } } diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/Analyticses/CliAnalyticsCollect.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/Analyticses/CliAnalyticsCollect.cs index fdfbb8b5d3..eb5d8686d8 100644 --- a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/Analyticses/CliAnalyticsCollect.cs +++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/Analyticses/CliAnalyticsCollect.cs @@ -1,4 +1,5 @@ -using System.Net.Http; +using System; +using System.Net.Http; using System.Text; using System.Threading.Tasks; using Microsoft.Extensions.Logging; @@ -32,27 +33,37 @@ namespace Volo.Abp.Cli.ProjectBuilding.Analyticses public async Task CollectAsync(CliAnalyticsCollectInputDto input) { var postData = _jsonSerializer.Serialize(input); - using (var client = new CliHttpClient()) + var url = $"{CliUrls.WwwAbpIo}api/clianalytics/collect"; + + try { - var responseMessage = await client.PostAsync( - $"{CliUrls.WwwAbpIo}api/clianalytics/collect", - new StringContent(postData, Encoding.UTF8, MimeTypes.Application.Json), - _cancellationTokenProvider.Token - ); - - if (!responseMessage.IsSuccessStatusCode) + using (var client = new CliHttpClient()) { - var exceptionMessage = "Remote server returns '" + (int)responseMessage.StatusCode + "-" + responseMessage.ReasonPhrase + "'. "; - var remoteServiceErrorMessage = await _remoteServiceExceptionHandler.GetAbpRemoteServiceErrorAsync(responseMessage); + var responseMessage = await client.PostAsync( + url, + new StringContent(postData, Encoding.UTF8, MimeTypes.Application.Json), + _cancellationTokenProvider.Token + ); - if (remoteServiceErrorMessage != null) + if (!responseMessage.IsSuccessStatusCode) { - exceptionMessage += remoteServiceErrorMessage; - } + var exceptionMessage = "Remote server returns '" + (int)responseMessage.StatusCode + "-" + responseMessage.ReasonPhrase + "'. "; + var remoteServiceErrorMessage = await _remoteServiceExceptionHandler.GetAbpRemoteServiceErrorAsync(responseMessage); + + if (remoteServiceErrorMessage != null) + { + exceptionMessage += remoteServiceErrorMessage; + } - _logger.LogInformation(exceptionMessage); + _logger.LogInformation(exceptionMessage); + } } } + catch (Exception ex) + { + Console.WriteLine("Error occured while cli analytics from {0} : {1}", url, ex.Message); + } + } } } \ No newline at end of file From 1ba052cba54e7ea77e627057d195663a98960db5 Mon Sep 17 00:00:00 2001 From: liangshiwei Date: Wed, 25 Mar 2020 14:21:47 +0800 Subject: [PATCH 03/28] Output cache template info, ignore CLI analysis exceptions --- .../ProjectBuilding/AbpIoSourceCodeStore.cs | 24 +++++++++++++++---- .../Analyticses/CliAnalyticsCollect.cs | 3 +-- 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/AbpIoSourceCodeStore.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/AbpIoSourceCodeStore.cs index 23d8a8b5ff..170bf25dda 100644 --- a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/AbpIoSourceCodeStore.cs +++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/AbpIoSourceCodeStore.cs @@ -3,6 +3,7 @@ using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; using System; using System.IO; +using System.Linq; using System.Net.Http; using System.Text; using System.Threading.Tasks; @@ -48,17 +49,30 @@ namespace Volo.Abp.Cli.ProjectBuilding string templateSource = null) { + DirectoryHelper.CreateIfNotExists(CliPaths.TemplateCache); + var latestVersion = await GetLatestSourceCodeVersionAsync(name, type); if (version == null) { - version = latestVersion ?? throw new CliUsageException( - "The remote service is currently unavailable, please specify the version (like abp new Acme.BookStore -v 2.3.0(Make sure you have a template cache locally))!"); + if (latestVersion == null) + { + Logger.LogWarning("The remote service is currently unavailable, please specify the version."); + Logger.LogWarning(string.Empty); + Logger.LogWarning("Find the following template in your cache directory: "); + + foreach (var cacheFile in Directory.GetFiles(CliPaths.TemplateCache)) + { + Logger.LogWarning($" {cacheFile}"); + } + + Logger.LogWarning(string.Empty); + throw new CliUsageException("Use command: abp new Acme.BookStore -v version"); + } + version = latestVersion; } var nugetVersion = (await GetTemplateNugetVersionAsync(name, type, version)) ?? version; - - DirectoryHelper.CreateIfNotExists(CliPaths.TemplateCache); - + if (!string.IsNullOrWhiteSpace(templateSource) && !IsNetworkSource(templateSource)) { Logger.LogInformation("Using local " + type + ": " + name + ", version: " + version); diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/Analyticses/CliAnalyticsCollect.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/Analyticses/CliAnalyticsCollect.cs index eb5d8686d8..621631af2a 100644 --- a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/Analyticses/CliAnalyticsCollect.cs +++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/Analyticses/CliAnalyticsCollect.cs @@ -61,9 +61,8 @@ namespace Volo.Abp.Cli.ProjectBuilding.Analyticses } catch (Exception ex) { - Console.WriteLine("Error occured while cli analytics from {0} : {1}", url, ex.Message); + // ignored } - } } } \ No newline at end of file From 6ce7f3bb863165e12393a391b9a5cf95367e0236 Mon Sep 17 00:00:00 2001 From: liangshiwei Date: Wed, 25 Mar 2020 16:33:08 +0800 Subject: [PATCH 04/28] Formatted output --- .../ProjectBuilding/AbpIoSourceCodeStore.cs | 29 +++++++++++++++++-- 1 file changed, 26 insertions(+), 3 deletions(-) diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/AbpIoSourceCodeStore.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/AbpIoSourceCodeStore.cs index 170bf25dda..a2fe686692 100644 --- a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/AbpIoSourceCodeStore.cs +++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/AbpIoSourceCodeStore.cs @@ -2,10 +2,12 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; using System; +using System.Collections.Generic; using System.IO; using System.Linq; using System.Net.Http; using System.Text; +using System.Text.RegularExpressions; using System.Threading.Tasks; using Volo.Abp.Cli.Http; using Volo.Abp.DependencyInjection; @@ -60,14 +62,16 @@ namespace Volo.Abp.Cli.ProjectBuilding Logger.LogWarning(string.Empty); Logger.LogWarning("Find the following template in your cache directory: "); - foreach (var cacheFile in Directory.GetFiles(CliPaths.TemplateCache)) + var templateList = GetLocalTemplates(); + foreach (var cacheFile in templateList) { - Logger.LogWarning($" {cacheFile}"); + Logger.LogWarning($" {cacheFile.TemplateName}: {cacheFile.Version}"); } Logger.LogWarning(string.Empty); throw new CliUsageException("Use command: abp new Acme.BookStore -v version"); } + version = latestVersion; } @@ -210,11 +214,30 @@ namespace Volo.Abp.Cli.ProjectBuilding } } - private static bool IsNetworkSource(string source) + private bool IsNetworkSource(string source) { return source.ToLower().StartsWith("http"); } + private List<(string TemplateName, string Version)> GetLocalTemplates() + { + var templateList = new List<(string TemplateName, string Version)>(); + + var stringBuilder = new StringBuilder(); + foreach (var cacheFile in Directory.GetFiles(CliPaths.TemplateCache)) + { + stringBuilder.AppendLine(cacheFile); + } + + var matches = Regex.Matches(stringBuilder.ToString(),"(app|app-pro|module|module-pro)-(.+).zip"); + foreach (Match match in matches) + { + templateList.Add((match.Groups[1].Value, match.Groups[2].Value)); + } + + return templateList; + } + public class SourceCodeDownloadInputDto { public string Name { get; set; } From 7192c224a0cea2103d31fa3fca64ae1171d04633 Mon Sep 17 00:00:00 2001 From: liangshiwei Date: Wed, 25 Mar 2020 16:53:02 +0800 Subject: [PATCH 05/28] Formatted output --- .../Volo/Abp/Cli/ProjectBuilding/AbpIoSourceCodeStore.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/AbpIoSourceCodeStore.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/AbpIoSourceCodeStore.cs index a2fe686692..2285c3cb86 100644 --- a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/AbpIoSourceCodeStore.cs +++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/AbpIoSourceCodeStore.cs @@ -61,11 +61,12 @@ namespace Volo.Abp.Cli.ProjectBuilding Logger.LogWarning("The remote service is currently unavailable, please specify the version."); Logger.LogWarning(string.Empty); Logger.LogWarning("Find the following template in your cache directory: "); + Logger.LogWarning("\t Template Name\tVersion"); var templateList = GetLocalTemplates(); foreach (var cacheFile in templateList) { - Logger.LogWarning($" {cacheFile.TemplateName}: {cacheFile.Version}"); + Logger.LogWarning($"\t {cacheFile.TemplateName}\t\t{cacheFile.Version}"); } Logger.LogWarning(string.Empty); From 2a62708175176907044ff2cfd62f34449d2cc021 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Halil=20=C4=B0brahim=20Kalkan?= Date: Wed, 25 Mar 2020 12:22:50 +0300 Subject: [PATCH 06/28] #3126 Implemented initial entity extensions system. --- .../Abp/EntityFrameworkCore/AbpDbContext.cs | 62 +++++++++++++ .../Extensions/EntityExtensions.cs | 88 +++++++++++++++++++ .../AbpEntityTypeBuilderExtensions.cs | 2 +- .../ValueConverters/AbpJsonValueConverter.cs | 16 +++- .../ExtraPropertiesValueConverter.cs | 41 +++++++++ ...IdentityDbContextModelBuilderExtensions.cs | 5 +- 6 files changed, 210 insertions(+), 4 deletions(-) create mode 100644 framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/Extensions/EntityExtensions.cs create mode 100644 framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/ValueConverters/ExtraPropertiesValueConverter.cs diff --git a/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/AbpDbContext.cs b/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/AbpDbContext.cs index 6cfbe3d01c..5933997cdc 100644 --- a/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/AbpDbContext.cs +++ b/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/AbpDbContext.cs @@ -18,6 +18,7 @@ using Volo.Abp.Domain.Entities; using Volo.Abp.Domain.Entities.Events; using Volo.Abp.Domain.Repositories; using Volo.Abp.EntityFrameworkCore.EntityHistory; +using Volo.Abp.EntityFrameworkCore.Extensions; using Volo.Abp.EntityFrameworkCore.Modeling; using Volo.Abp.EntityFrameworkCore.ValueConverters; using Volo.Abp.Guids; @@ -155,6 +156,39 @@ namespace Volo.Abp.EntityFrameworkCore ChangeTracker.CascadeDeleteTiming = CascadeTiming.OnSaveChanges; ChangeTracker.DeleteOrphansTiming = CascadeTiming.OnSaveChanges; + + ChangeTracker.Tracked += ChangeTracker_Tracked; + } + + protected virtual void ChangeTracker_Tracked(object sender, EntityTrackedEventArgs e) + { + FillExtraPropertiesForTrackedEntities(e); + } + + private static void FillExtraPropertiesForTrackedEntities(EntityTrackedEventArgs e) + { + var entityType = e.Entry.Metadata.ClrType; + if (entityType == null) + { + return; + } + + if (!(e.Entry.Entity is IHasExtraProperties entity)) + { + return; + } + + if (!e.FromQuery) + { + return; + } + + var propertyNames = EntityExtensions.GetPropertyNames(entityType); + + foreach (var propertyName in propertyNames) + { + entity.SetProperty(propertyName, e.Entry.CurrentValues[propertyName]); + } } protected virtual EntityChangeReport ApplyAbpConcepts() @@ -184,9 +218,37 @@ namespace Volo.Abp.EntityFrameworkCore break; } + HandleExtraPropertiesOnSave(entry); + AddDomainEvents(changeReport, entry.Entity); } + private void HandleExtraPropertiesOnSave(EntityEntry entry) + { + if (entry.State == EntityState.Deleted) + { + return; + } + + var entityType = entry.Metadata.ClrType; + if (entityType == null) + { + return; + } + + if (!(entry.Entity is IHasExtraProperties entity)) + { + return; + } + + var propertyNames = EntityExtensions.GetPropertyNames(entityType); + + foreach (var propertyName in propertyNames) + { + entry.Property(propertyName).CurrentValue = entity.GetProperty(propertyName); + } + } + protected virtual void ApplyAbpConceptsForAddedEntity(EntityEntry entry, EntityChangeReport changeReport) { CheckAndSetId(entry); diff --git a/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/Extensions/EntityExtensions.cs b/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/Extensions/EntityExtensions.cs new file mode 100644 index 0000000000..5f452c6319 --- /dev/null +++ b/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/Extensions/EntityExtensions.cs @@ -0,0 +1,88 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace Volo.Abp.EntityFrameworkCore.Extensions +{ + public class EntityExtensionInfo + { + public Dictionary Properties { get; set; } + + public EntityExtensionInfo() + { + Properties = new Dictionary(); + } + } + + public class PropertyExtensionInfo + { + public List> Actions { get; } + + public Type PropertyType { get; } + + public PropertyExtensionInfo(Type propertyType) + { + PropertyType = propertyType; + Actions = new List>(); + } + } + + public static class EntityExtensions + { + private static readonly Dictionary ExtensionInfos; + + //TODO: Use PropertyBuilder instead + + static EntityExtensions() + { + ExtensionInfos = new Dictionary(); + } + + public static void AddProperty( + string name, + Type propertyType, + Action propertyBuildAction) + { + var extensionInfo = ExtensionInfos + .GetOrAdd(typeof(TEntity), () => new EntityExtensionInfo()); + + var propertyExtensionInfo = extensionInfo.Properties + .GetOrAdd(name, () => new PropertyExtensionInfo(propertyType)); + + propertyExtensionInfo.Actions.Add(propertyBuildAction); + } + + public static void ConfigureProperties(EntityTypeBuilder entityTypeBuilder) + { + var entityExtensionInfo = ExtensionInfos.GetOrDefault(typeof(TEntity)); + if (entityExtensionInfo == null) + { + return; + } + + foreach (var propertyExtensionInfo in entityExtensionInfo.Properties) + { + var property = entityTypeBuilder.Property(propertyExtensionInfo.Value.PropertyType, propertyExtensionInfo.Key); + foreach (var action in propertyExtensionInfo.Value.Actions) + { + action(property); + } + } + } + + public static string[] GetPropertyNames(Type entityType) + { + var entityExtensionInfo = ExtensionInfos.GetOrDefault(entityType); + if (entityExtensionInfo == null) + { + return Array.Empty(); + } + + return entityExtensionInfo + .Properties + .Select(p => p.Key) + .ToArray(); + } + } +} diff --git a/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/Modeling/AbpEntityTypeBuilderExtensions.cs b/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/Modeling/AbpEntityTypeBuilderExtensions.cs index 6db09d4d17..3c54044bd2 100644 --- a/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/Modeling/AbpEntityTypeBuilderExtensions.cs +++ b/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/Modeling/AbpEntityTypeBuilderExtensions.cs @@ -58,7 +58,7 @@ namespace Volo.Abp.EntityFrameworkCore.Modeling { b.Property>(nameof(IHasExtraProperties.ExtraProperties)) .HasColumnName(nameof(IHasExtraProperties.ExtraProperties)) - .HasConversion(new AbpJsonValueConverter>()) + .HasConversion(new ExtraPropertiesValueConverter(b.Metadata.ClrType)) .Metadata.SetValueComparer(new AbpDictionaryValueComparer()); } } diff --git a/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/ValueConverters/AbpJsonValueConverter.cs b/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/ValueConverters/AbpJsonValueConverter.cs index e77b2842a8..6ecf19fa98 100644 --- a/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/ValueConverters/AbpJsonValueConverter.cs +++ b/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/ValueConverters/AbpJsonValueConverter.cs @@ -1,5 +1,6 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; using Newtonsoft.Json; +using Volo.Abp.Data; namespace Volo.Abp.EntityFrameworkCore.ValueConverters { @@ -7,9 +8,20 @@ namespace Volo.Abp.EntityFrameworkCore.ValueConverters { public AbpJsonValueConverter() : base( - d => JsonConvert.SerializeObject(d, Formatting.None), - s => JsonConvert.DeserializeObject(s)) + d => SerializeObject(d), + s => DeserializeObject(s)) { + + } + + private static string SerializeObject(TPropertyType d) + { + return JsonConvert.SerializeObject(d, Formatting.None); + } + + private static TPropertyType DeserializeObject(string s) + { + return JsonConvert.DeserializeObject(s); } } } diff --git a/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/ValueConverters/ExtraPropertiesValueConverter.cs b/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/ValueConverters/ExtraPropertiesValueConverter.cs new file mode 100644 index 0000000000..ffe3399e6a --- /dev/null +++ b/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/ValueConverters/ExtraPropertiesValueConverter.cs @@ -0,0 +1,41 @@ +using System; +using System.Collections.Generic; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Newtonsoft.Json; +using Volo.Abp.EntityFrameworkCore.Extensions; + +namespace Volo.Abp.EntityFrameworkCore.ValueConverters +{ + public class ExtraPropertiesValueConverter : ValueConverter, string> + { + public ExtraPropertiesValueConverter(Type entityType) + : base( + d => SerializeObject(d, entityType), + s => DeserializeObject(s)) + { + + } + + private static string SerializeObject(Dictionary extraProperties, Type entityType) + { + var copyDictionary = new Dictionary(extraProperties); + + if (entityType != null) + { + var propertyNames = EntityExtensions.GetPropertyNames(entityType); + + foreach (var propertyName in propertyNames) + { + copyDictionary.Remove(propertyName); + } + } + + return JsonConvert.SerializeObject(copyDictionary, Formatting.None); + } + + private static Dictionary DeserializeObject(string extraPropertiesAsJson) + { + return JsonConvert.DeserializeObject>(extraPropertiesAsJson); + } + } +} \ No newline at end of file diff --git a/modules/identity/src/Volo.Abp.Identity.EntityFrameworkCore/Volo/Abp/Identity/EntityFrameworkCore/IdentityDbContextModelBuilderExtensions.cs b/modules/identity/src/Volo.Abp.Identity.EntityFrameworkCore/Volo/Abp/Identity/EntityFrameworkCore/IdentityDbContextModelBuilderExtensions.cs index 76ac038c05..6d5d182a51 100644 --- a/modules/identity/src/Volo.Abp.Identity.EntityFrameworkCore/Volo/Abp/Identity/EntityFrameworkCore/IdentityDbContextModelBuilderExtensions.cs +++ b/modules/identity/src/Volo.Abp.Identity.EntityFrameworkCore/Volo/Abp/Identity/EntityFrameworkCore/IdentityDbContextModelBuilderExtensions.cs @@ -1,6 +1,7 @@ using System; using JetBrains.Annotations; using Microsoft.EntityFrameworkCore; +using Volo.Abp.EntityFrameworkCore.Extensions; using Volo.Abp.EntityFrameworkCore.Modeling; using Volo.Abp.Users.EntityFrameworkCore; @@ -36,11 +37,13 @@ namespace Volo.Abp.Identity.EntityFrameworkCore b.Property(u => u.LockoutEnabled).HasDefaultValue(false).HasColumnName(nameof(IdentityUser.LockoutEnabled)); b.Property(u => u.AccessFailedCount).HasDefaultValue(0).HasColumnName(nameof(IdentityUser.AccessFailedCount)); + EntityExtensions.ConfigureProperties(b); + b.HasMany(u => u.Claims).WithOne().HasForeignKey(uc => uc.UserId).IsRequired(); b.HasMany(u => u.Logins).WithOne().HasForeignKey(ul => ul.UserId).IsRequired(); b.HasMany(u => u.Roles).WithOne().HasForeignKey(ur => ur.UserId).IsRequired(); b.HasMany(u => u.Tokens).WithOne().HasForeignKey(ur => ur.UserId).IsRequired(); - + b.HasIndex(u => u.NormalizedUserName); b.HasIndex(u => u.NormalizedEmail); b.HasIndex(u => u.UserName); From 60ce0e96650194191f324bf0bc77a4ab8711cc55 Mon Sep 17 00:00:00 2001 From: liangshiwei Date: Wed, 25 Mar 2020 20:19:59 +0800 Subject: [PATCH 07/28] Match template regex use constant --- .../Volo/Abp/Cli/ProjectBuilding/AbpIoSourceCodeStore.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/AbpIoSourceCodeStore.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/AbpIoSourceCodeStore.cs index 2285c3cb86..0aa2587e20 100644 --- a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/AbpIoSourceCodeStore.cs +++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/AbpIoSourceCodeStore.cs @@ -10,6 +10,8 @@ using System.Text; using System.Text.RegularExpressions; using System.Threading.Tasks; using Volo.Abp.Cli.Http; +using Volo.Abp.Cli.ProjectBuilding.Templates.App; +using Volo.Abp.Cli.ProjectBuilding.Templates.MvcModule; using Volo.Abp.DependencyInjection; using Volo.Abp.Http; using Volo.Abp.IO; @@ -230,7 +232,7 @@ namespace Volo.Abp.Cli.ProjectBuilding stringBuilder.AppendLine(cacheFile); } - var matches = Regex.Matches(stringBuilder.ToString(),"(app|app-pro|module|module-pro)-(.+).zip"); + var matches = Regex.Matches(stringBuilder.ToString(),$"({AppTemplate.TemplateName}|{AppProTemplate.TemplateName}|{ModuleTemplate.TemplateName}|{ModuleProTemplate.TemplateName})-(.+).zip"); foreach (Match match in matches) { templateList.Add((match.Groups[1].Value, match.Groups[2].Value)); From 2863b5cb5d29391acb663a2a1bf9b571a1ee6fa8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Halil=20=C4=B0brahim=20Kalkan?= Date: Wed, 25 Mar 2020 15:24:40 +0300 Subject: [PATCH 08/28] Add test & refactor for EntityExtensionManager --- .../Abp/EntityFrameworkCore/AbpDbContext.cs | 4 +-- ...xtensions.cs => EntityExtensionManager.cs} | 9 +++---- .../ExtraPropertiesValueConverter.cs | 2 +- .../AbpEntityFrameworkCoreTestModule.cs | 6 +++++ .../Domain/ExtraProperties_Tests.cs | 25 ++++++++++++++++++- .../Domain/TestEntityExtensionConfigurator.cs | 22 ++++++++++++++++ .../TestMigrationsDbContext.cs | 3 +++ .../EntityFrameworkCore/TestAppDbContext.cs | 3 +++ .../Volo/Abp/TestApp/TestDataBuilder.cs | 4 +-- .../TestApp/Testing/ExtraProperties_Tests.cs | 1 + 10 files changed, 68 insertions(+), 11 deletions(-) rename framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/Extensions/{EntityExtensions.cs => EntityExtensionManager.cs} (93%) create mode 100644 framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/Domain/TestEntityExtensionConfigurator.cs diff --git a/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/AbpDbContext.cs b/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/AbpDbContext.cs index 5933997cdc..09d5bbd923 100644 --- a/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/AbpDbContext.cs +++ b/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/AbpDbContext.cs @@ -183,7 +183,7 @@ namespace Volo.Abp.EntityFrameworkCore return; } - var propertyNames = EntityExtensions.GetPropertyNames(entityType); + var propertyNames = EntityExtensionManager.GetPropertyNames(entityType); foreach (var propertyName in propertyNames) { @@ -241,7 +241,7 @@ namespace Volo.Abp.EntityFrameworkCore return; } - var propertyNames = EntityExtensions.GetPropertyNames(entityType); + var propertyNames = EntityExtensionManager.GetPropertyNames(entityType); foreach (var propertyName in propertyNames) { diff --git a/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/Extensions/EntityExtensions.cs b/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/Extensions/EntityExtensionManager.cs similarity index 93% rename from framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/Extensions/EntityExtensions.cs rename to framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/Extensions/EntityExtensionManager.cs index 5f452c6319..f6d9ccaaf5 100644 --- a/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/Extensions/EntityExtensions.cs +++ b/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/Extensions/EntityExtensionManager.cs @@ -28,27 +28,26 @@ namespace Volo.Abp.EntityFrameworkCore.Extensions } } - public static class EntityExtensions + public static class EntityExtensionManager { private static readonly Dictionary ExtensionInfos; //TODO: Use PropertyBuilder instead - static EntityExtensions() + static EntityExtensionManager() { ExtensionInfos = new Dictionary(); } - public static void AddProperty( + public static void AddProperty( string name, - Type propertyType, Action propertyBuildAction) { var extensionInfo = ExtensionInfos .GetOrAdd(typeof(TEntity), () => new EntityExtensionInfo()); var propertyExtensionInfo = extensionInfo.Properties - .GetOrAdd(name, () => new PropertyExtensionInfo(propertyType)); + .GetOrAdd(name, () => new PropertyExtensionInfo(typeof(TProperty))); propertyExtensionInfo.Actions.Add(propertyBuildAction); } diff --git a/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/ValueConverters/ExtraPropertiesValueConverter.cs b/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/ValueConverters/ExtraPropertiesValueConverter.cs index ffe3399e6a..fb1abf0e24 100644 --- a/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/ValueConverters/ExtraPropertiesValueConverter.cs +++ b/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/ValueConverters/ExtraPropertiesValueConverter.cs @@ -22,7 +22,7 @@ namespace Volo.Abp.EntityFrameworkCore.ValueConverters if (entityType != null) { - var propertyNames = EntityExtensions.GetPropertyNames(entityType); + var propertyNames = EntityExtensionManager.GetPropertyNames(entityType); foreach (var propertyName in propertyNames) { diff --git a/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/AbpEntityFrameworkCoreTestModule.cs b/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/AbpEntityFrameworkCoreTestModule.cs index 71522d0216..e19d939cba 100644 --- a/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/AbpEntityFrameworkCoreTestModule.cs +++ b/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/AbpEntityFrameworkCoreTestModule.cs @@ -5,6 +5,7 @@ using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Storage; using Microsoft.Extensions.DependencyInjection; using Volo.Abp.Autofac; +using Volo.Abp.EntityFrameworkCore.Domain; using Volo.Abp.EntityFrameworkCore.TestApp.SecondContext; using Volo.Abp.EntityFrameworkCore.TestApp.ThirdDbContext; using Volo.Abp.Modularity; @@ -21,6 +22,11 @@ namespace Volo.Abp.EntityFrameworkCore [DependsOn(typeof(AbpEfCoreTestSecondContextModule))] public class AbpEntityFrameworkCoreTestModule : AbpModule { + public override void PreConfigureServices(ServiceConfigurationContext context) + { + TestEntityExtensionConfigurator.Configure(); + } + public override void ConfigureServices(ServiceConfigurationContext context) { context.Services.AddAbpDbContext(options => diff --git a/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/Domain/ExtraProperties_Tests.cs b/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/Domain/ExtraProperties_Tests.cs index eea827fbee..557116d158 100644 --- a/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/Domain/ExtraProperties_Tests.cs +++ b/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/Domain/ExtraProperties_Tests.cs @@ -1,9 +1,32 @@ -using Volo.Abp.TestApp.Testing; +using System.Threading.Tasks; +using Shouldly; +using Volo.Abp.Data; +using Volo.Abp.TestApp.Testing; +using Xunit; namespace Volo.Abp.EntityFrameworkCore.Domain { public class ExtraProperties_Tests : ExtraProperties_Tests { + [Fact] + public async Task Should_Get_An_Extra_Property_Configured_As_Extension() + { + var london = await CityRepository.FindByNameAsync("London"); + london.HasProperty("PhoneCode").ShouldBeTrue(); + london.GetProperty("PhoneCode").ShouldBe("42"); + } + [Fact] + public async Task Should_Update_An_Existing_Extra_Property_Configured_As_Extension() + { + var london = await CityRepository.FindByNameAsync("London"); + london.GetProperty("PhoneCode").ShouldBe("42"); + + london.ExtraProperties["PhoneCode"] = "53"; + await CityRepository.UpdateAsync(london); + + var london2 = await CityRepository.FindByNameAsync("London"); + london2.GetProperty("PhoneCode").ShouldBe("53"); + } } } diff --git a/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/Domain/TestEntityExtensionConfigurator.cs b/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/Domain/TestEntityExtensionConfigurator.cs new file mode 100644 index 0000000000..2ae6888e7f --- /dev/null +++ b/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/Domain/TestEntityExtensionConfigurator.cs @@ -0,0 +1,22 @@ +using Volo.Abp.EntityFrameworkCore.Extensions; +using Volo.Abp.TestApp.Domain; +using Volo.Abp.Threading; + +namespace Volo.Abp.EntityFrameworkCore.Domain +{ + public static class TestEntityExtensionConfigurator + { + private static readonly OneTimeRunner OneTimeRunner = new OneTimeRunner(); + + public static void Configure() + { + OneTimeRunner.Run(() => + { + EntityExtensionManager.AddProperty( + "PhoneCode", + p => p.HasMaxLength(8) + ); + }); + } + } +} diff --git a/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/TestMigrationsDbContext.cs b/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/TestMigrationsDbContext.cs index da66392a18..232a935dca 100644 --- a/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/TestMigrationsDbContext.cs +++ b/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/TestMigrationsDbContext.cs @@ -1,4 +1,5 @@ using Microsoft.EntityFrameworkCore; +using Volo.Abp.EntityFrameworkCore.Extensions; using Volo.Abp.EntityFrameworkCore.TestApp.SecondContext; using Volo.Abp.EntityFrameworkCore.TestApp.ThirdDbContext; using Volo.Abp.TestApp.Domain; @@ -36,6 +37,8 @@ namespace Volo.Abp.EntityFrameworkCore modelBuilder.Entity(b => { + EntityExtensionManager.ConfigureProperties(b); + b.OwnsMany(c => c.Districts, d => { d.WithOwner().HasForeignKey(x => x.CityId); diff --git a/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/TestApp/EntityFrameworkCore/TestAppDbContext.cs b/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/TestApp/EntityFrameworkCore/TestAppDbContext.cs index 289364411d..e8e7701b05 100644 --- a/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/TestApp/EntityFrameworkCore/TestAppDbContext.cs +++ b/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/TestApp/EntityFrameworkCore/TestAppDbContext.cs @@ -1,5 +1,6 @@ using Microsoft.EntityFrameworkCore; using Volo.Abp.EntityFrameworkCore; +using Volo.Abp.EntityFrameworkCore.Extensions; using Volo.Abp.EntityFrameworkCore.TestApp.ThirdDbContext; using Volo.Abp.TestApp.Domain; @@ -43,6 +44,8 @@ namespace Volo.Abp.TestApp.EntityFrameworkCore modelBuilder.Entity(b => { + EntityExtensionManager.ConfigureProperties(b); + b.OwnsMany(c => c.Districts, d => { d.WithOwner().HasForeignKey(x => x.CityId); diff --git a/framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/TestDataBuilder.cs b/framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/TestDataBuilder.cs index 03e4ed5465..312125d541 100644 --- a/framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/TestDataBuilder.cs +++ b/framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/TestDataBuilder.cs @@ -21,7 +21,7 @@ namespace Volo.Abp.TestApp private readonly IRepository _entityWithIntPksRepository; public TestDataBuilder( - IBasicRepository personRepository, + IBasicRepository personRepository, ICityRepository cityRepository, IRepository entityWithIntPksRepository) { @@ -46,7 +46,7 @@ namespace Volo.Abp.TestApp await _cityRepository.InsertAsync(new City(Guid.NewGuid(), "Tokyo")); await _cityRepository.InsertAsync(new City(Guid.NewGuid(), "Madrid")); - await _cityRepository.InsertAsync(new City(LondonCityId, "London") { ExtraProperties = { { "Population", 10_470_000 } } }); + await _cityRepository.InsertAsync(new City(LondonCityId, "London") { ExtraProperties = { { "Population", 10_470_000 }, { "PhoneCode", "42" } } }); await _cityRepository.InsertAsync(istanbul); await _cityRepository.InsertAsync(new City(Guid.NewGuid(), "Paris")); await _cityRepository.InsertAsync(new City(Guid.NewGuid(), "Washington")); diff --git a/framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Testing/ExtraProperties_Tests.cs b/framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Testing/ExtraProperties_Tests.cs index 67eff13572..47f4c6a4cd 100644 --- a/framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Testing/ExtraProperties_Tests.cs +++ b/framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Testing/ExtraProperties_Tests.cs @@ -41,6 +41,7 @@ namespace Volo.Abp.TestApp.Testing public async Task Should_Update_An_Existing_Extra_Property() { var london = await CityRepository.FindByNameAsync("London"); + london.GetProperty("Population").ShouldBe(10_470_000); london.ExtraProperties["Population"] = 11_000_042; await CityRepository.UpdateAsync(london); From 7da5321d69a57e1a2dbd6ab61a3db9fd75a3701a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Halil=20=C4=B0brahim=20Kalkan?= Date: Wed, 25 Mar 2020 16:20:51 +0300 Subject: [PATCH 09/28] Refactor entity extensions. --- .../Abp/EntityFrameworkCore/AbpDbContext.cs | 19 ++- .../Extensions/EntityExtensionInfo.cs | 14 +++ .../Extensions/EntityExtensionManager.cs | 116 ++++++++++++------ .../Extensions/PropertyExtensionInfo.cs | 17 +++ .../TestMigrationsDbContext.cs | 2 +- .../EntityFrameworkCore/TestAppDbContext.cs | 2 +- ...IdentityDbContextModelBuilderExtensions.cs | 5 +- 7 files changed, 131 insertions(+), 44 deletions(-) create mode 100644 framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/Extensions/EntityExtensionInfo.cs create mode 100644 framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/Extensions/PropertyExtensionInfo.cs diff --git a/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/AbpDbContext.cs b/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/AbpDbContext.cs index 09d5bbd923..2488ec9513 100644 --- a/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/AbpDbContext.cs +++ b/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/AbpDbContext.cs @@ -165,7 +165,7 @@ namespace Volo.Abp.EntityFrameworkCore FillExtraPropertiesForTrackedEntities(e); } - private static void FillExtraPropertiesForTrackedEntities(EntityTrackedEventArgs e) + protected virtual void FillExtraPropertiesForTrackedEntities(EntityTrackedEventArgs e) { var entityType = e.Entry.Metadata.ClrType; if (entityType == null) @@ -187,7 +187,18 @@ namespace Volo.Abp.EntityFrameworkCore foreach (var propertyName in propertyNames) { - entity.SetProperty(propertyName, e.Entry.CurrentValues[propertyName]); + /* Checking "currentValue != null" has a good advantage: + * Assume that you we already using a named extra property, + * then decided to create a field (entity extension) for it. + * In this way, it prevents to delete old value in the JSON and + * updates the field on the next save! + */ + + var currentValue = e.Entry.CurrentValues[propertyName]; + if (currentValue != null) + { + entity.SetProperty(propertyName, currentValue); + } } } @@ -223,9 +234,9 @@ namespace Volo.Abp.EntityFrameworkCore AddDomainEvents(changeReport, entry.Entity); } - private void HandleExtraPropertiesOnSave(EntityEntry entry) + protected virtual void HandleExtraPropertiesOnSave(EntityEntry entry) { - if (entry.State == EntityState.Deleted) + if (entry.State.IsIn(EntityState.Deleted, EntityState.Unchanged)) { return; } diff --git a/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/Extensions/EntityExtensionInfo.cs b/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/Extensions/EntityExtensionInfo.cs new file mode 100644 index 0000000000..f1aa105b9f --- /dev/null +++ b/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/Extensions/EntityExtensionInfo.cs @@ -0,0 +1,14 @@ +using System.Collections.Generic; + +namespace Volo.Abp.EntityFrameworkCore.Extensions +{ + public class EntityExtensionInfo + { + public Dictionary Properties { get; set; } + + public EntityExtensionInfo() + { + Properties = new Dictionary(); + } + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/Extensions/EntityExtensionManager.cs b/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/Extensions/EntityExtensionManager.cs index f6d9ccaaf5..d9f1ae9c31 100644 --- a/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/Extensions/EntityExtensionManager.cs +++ b/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/Extensions/EntityExtensionManager.cs @@ -1,60 +1,96 @@ using System; using System.Collections.Generic; using System.Linq; +using JetBrains.Annotations; using Microsoft.EntityFrameworkCore.Metadata.Builders; +using Volo.Abp.Data; namespace Volo.Abp.EntityFrameworkCore.Extensions { - public class EntityExtensionInfo - { - public Dictionary Properties { get; set; } - - public EntityExtensionInfo() - { - Properties = new Dictionary(); - } - } - - public class PropertyExtensionInfo - { - public List> Actions { get; } - - public Type PropertyType { get; } - - public PropertyExtensionInfo(Type propertyType) - { - PropertyType = propertyType; - Actions = new List>(); - } - } - public static class EntityExtensionManager { private static readonly Dictionary ExtensionInfos; - //TODO: Use PropertyBuilder instead - static EntityExtensionManager() { ExtensionInfos = new Dictionary(); } + /// + /// Adds an extension property for an entity. + /// If it is already added, replaces the + /// by the given one! + /// + /// Type of the entity + /// Type of the new property + /// Name of the property + /// An action to configure the database mapping for the new property public static void AddProperty( - string name, - Action propertyBuildAction) + [NotNull]string propertyName, + [NotNull]Action propertyBuildAction) + { + AddProperty( + typeof(TEntity), + typeof(TProperty), + propertyName, + propertyBuildAction + ); + } + + /// + /// Adds an extension property for an entity. + /// If it is already added, replaces the + /// by the given one! + /// + /// Type of the entity + /// Type of the new property + /// Name of the property + /// An action to configure the database mapping for the new property + public static void AddProperty( + Type entityType, + Type propertyType, + [NotNull]string propertyName, + [NotNull]Action propertyBuildAction) { + Check.NotNull(entityType, nameof(entityType)); + Check.NotNull(propertyType, nameof(propertyType)); + Check.NotNullOrWhiteSpace(propertyName, nameof(propertyName)); + Check.NotNull(propertyBuildAction, nameof(propertyBuildAction)); + var extensionInfo = ExtensionInfos - .GetOrAdd(typeof(TEntity), () => new EntityExtensionInfo()); + .GetOrAdd(entityType, () => new EntityExtensionInfo()); var propertyExtensionInfo = extensionInfo.Properties - .GetOrAdd(name, () => new PropertyExtensionInfo(typeof(TProperty))); + .GetOrAdd(propertyName, () => new PropertyExtensionInfo(propertyType)); - propertyExtensionInfo.Actions.Add(propertyBuildAction); + propertyExtensionInfo.Action = propertyBuildAction; } - public static void ConfigureProperties(EntityTypeBuilder entityTypeBuilder) + /// + /// Configures the entity mapping for the defined extensions. + /// + /// The entity tye + /// Entity type builder + public static void ConfigureExtensions( + [NotNull] this EntityTypeBuilder entityTypeBuilder) + where TEntity : class, IHasExtraProperties { - var entityExtensionInfo = ExtensionInfos.GetOrDefault(typeof(TEntity)); + ConfigureExtensions(typeof(TEntity), entityTypeBuilder); + } + + /// + /// Configures the entity mapping for the defined extensions. + /// + /// Type of the entity + /// Entity type builder + public static void ConfigureExtensions( + [NotNull] Type entityType, + [NotNull] EntityTypeBuilder entityTypeBuilder) + { + Check.NotNull(entityType, nameof(entityType)); + Check.NotNull(entityTypeBuilder, nameof(entityTypeBuilder)); + + var entityExtensionInfo = ExtensionInfos.GetOrDefault(entityType); if (entityExtensionInfo == null) { return; @@ -62,11 +98,21 @@ namespace Volo.Abp.EntityFrameworkCore.Extensions foreach (var propertyExtensionInfo in entityExtensionInfo.Properties) { - var property = entityTypeBuilder.Property(propertyExtensionInfo.Value.PropertyType, propertyExtensionInfo.Key); - foreach (var action in propertyExtensionInfo.Value.Actions) + var propertyName = propertyExtensionInfo.Key; + var propertyType = propertyExtensionInfo.Value.PropertyType; + + /* Prevent multiple calls to the entityTypeBuilder.Property(...) method */ + if (entityTypeBuilder.Metadata.FindProperty(propertyName) != null) { - action(property); + continue; } + + var property = entityTypeBuilder.Property( + propertyType, + propertyName + ); + + propertyExtensionInfo.Value.Action(property); } } diff --git a/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/Extensions/PropertyExtensionInfo.cs b/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/Extensions/PropertyExtensionInfo.cs new file mode 100644 index 0000000000..df29bdd62d --- /dev/null +++ b/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/Extensions/PropertyExtensionInfo.cs @@ -0,0 +1,17 @@ +using System; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace Volo.Abp.EntityFrameworkCore.Extensions +{ + public class PropertyExtensionInfo + { + public Action Action { get; set; } + + public Type PropertyType { get; } + + public PropertyExtensionInfo(Type propertyType) + { + PropertyType = propertyType; + } + } +} \ No newline at end of file diff --git a/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/TestMigrationsDbContext.cs b/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/TestMigrationsDbContext.cs index 232a935dca..83e6d407a4 100644 --- a/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/TestMigrationsDbContext.cs +++ b/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/TestMigrationsDbContext.cs @@ -37,7 +37,7 @@ namespace Volo.Abp.EntityFrameworkCore modelBuilder.Entity(b => { - EntityExtensionManager.ConfigureProperties(b); + b.ConfigureExtensions(); b.OwnsMany(c => c.Districts, d => { diff --git a/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/TestApp/EntityFrameworkCore/TestAppDbContext.cs b/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/TestApp/EntityFrameworkCore/TestAppDbContext.cs index e8e7701b05..701aff4307 100644 --- a/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/TestApp/EntityFrameworkCore/TestAppDbContext.cs +++ b/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/TestApp/EntityFrameworkCore/TestAppDbContext.cs @@ -44,7 +44,7 @@ namespace Volo.Abp.TestApp.EntityFrameworkCore modelBuilder.Entity(b => { - EntityExtensionManager.ConfigureProperties(b); + b.ConfigureExtensions(); b.OwnsMany(c => c.Districts, d => { diff --git a/modules/identity/src/Volo.Abp.Identity.EntityFrameworkCore/Volo/Abp/Identity/EntityFrameworkCore/IdentityDbContextModelBuilderExtensions.cs b/modules/identity/src/Volo.Abp.Identity.EntityFrameworkCore/Volo/Abp/Identity/EntityFrameworkCore/IdentityDbContextModelBuilderExtensions.cs index 6d5d182a51..fff3d98d11 100644 --- a/modules/identity/src/Volo.Abp.Identity.EntityFrameworkCore/Volo/Abp/Identity/EntityFrameworkCore/IdentityDbContextModelBuilderExtensions.cs +++ b/modules/identity/src/Volo.Abp.Identity.EntityFrameworkCore/Volo/Abp/Identity/EntityFrameworkCore/IdentityDbContextModelBuilderExtensions.cs @@ -28,6 +28,7 @@ namespace Volo.Abp.Identity.EntityFrameworkCore b.ConfigureFullAuditedAggregateRoot(); b.ConfigureAbpUser(); + b.ConfigureExtensions(); b.Property(u => u.NormalizedUserName).IsRequired().HasMaxLength(IdentityUserConsts.MaxNormalizedUserNameLength).HasColumnName(nameof(IdentityUser.NormalizedUserName)); b.Property(u => u.NormalizedEmail).IsRequired().HasMaxLength(IdentityUserConsts.MaxNormalizedEmailLength).HasColumnName(nameof(IdentityUser.NormalizedEmail)); @@ -36,9 +37,7 @@ namespace Volo.Abp.Identity.EntityFrameworkCore b.Property(u => u.TwoFactorEnabled).HasDefaultValue(false).HasColumnName(nameof(IdentityUser.TwoFactorEnabled)); b.Property(u => u.LockoutEnabled).HasDefaultValue(false).HasColumnName(nameof(IdentityUser.LockoutEnabled)); b.Property(u => u.AccessFailedCount).HasDefaultValue(0).HasColumnName(nameof(IdentityUser.AccessFailedCount)); - - EntityExtensions.ConfigureProperties(b); - + b.HasMany(u => u.Claims).WithOne().HasForeignKey(uc => uc.UserId).IsRequired(); b.HasMany(u => u.Logins).WithOne().HasForeignKey(ul => ul.UserId).IsRequired(); b.HasMany(u => u.Roles).WithOne().HasForeignKey(ur => ur.UserId).IsRequired(); From 54f33891db443cfa5f975df9e0ca6e2293f46b89 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Halil=20=C4=B0brahim=20Kalkan?= Date: Wed, 25 Mar 2020 16:50:55 +0300 Subject: [PATCH 10/28] Configure entity extensions by convention. --- .../Modeling/AbpEntityTypeBuilderExtensions.cs | 7 +++++-- .../Abp/EntityFrameworkCore/TestMigrationsDbContext.cs | 2 +- .../Abp/TestApp/EntityFrameworkCore/TestAppDbContext.cs | 2 +- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/Modeling/AbpEntityTypeBuilderExtensions.cs b/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/Modeling/AbpEntityTypeBuilderExtensions.cs index 3c54044bd2..eca8831e35 100644 --- a/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/Modeling/AbpEntityTypeBuilderExtensions.cs +++ b/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/Modeling/AbpEntityTypeBuilderExtensions.cs @@ -5,6 +5,7 @@ using Microsoft.EntityFrameworkCore.Metadata.Builders; using Volo.Abp.Auditing; using Volo.Abp.Data; using Volo.Abp.Domain.Entities; +using Volo.Abp.EntityFrameworkCore.Extensions; using Volo.Abp.EntityFrameworkCore.ValueComparers; using Volo.Abp.EntityFrameworkCore.ValueConverters; using Volo.Abp.MultiTenancy; @@ -25,7 +26,7 @@ namespace Volo.Abp.EntityFrameworkCore.Modeling b.TryConfigureCreationTime(); b.TryConfigureLastModificationTime(); b.TryConfigureModificationAudited(); - b.TryConfigureMultiTenant(); + b.TryConfigureMultiTenant(); } public static void ConfigureConcurrencyStamp(this EntityTypeBuilder b) @@ -60,6 +61,8 @@ namespace Volo.Abp.EntityFrameworkCore.Modeling .HasColumnName(nameof(IHasExtraProperties.ExtraProperties)) .HasConversion(new ExtraPropertiesValueConverter(b.Metadata.ClrType)) .Metadata.SetValueComparer(new AbpDictionaryValueComparer()); + + EntityExtensionManager.ConfigureExtensions(b.Metadata.ClrType, b); } } @@ -276,7 +279,7 @@ namespace Volo.Abp.EntityFrameworkCore.Modeling } public static void ConfigureFullAuditedAggregateRoot(this EntityTypeBuilder b) - where T : class + where T : class { b.As().TryConfigureFullAudited(); b.As().TryConfigureExtraProperties(); diff --git a/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/TestMigrationsDbContext.cs b/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/TestMigrationsDbContext.cs index 83e6d407a4..6293e21716 100644 --- a/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/TestMigrationsDbContext.cs +++ b/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/TestMigrationsDbContext.cs @@ -37,7 +37,7 @@ namespace Volo.Abp.EntityFrameworkCore modelBuilder.Entity(b => { - b.ConfigureExtensions(); + //b.ConfigureExtensions(); b.OwnsMany(c => c.Districts, d => { diff --git a/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/TestApp/EntityFrameworkCore/TestAppDbContext.cs b/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/TestApp/EntityFrameworkCore/TestAppDbContext.cs index 701aff4307..3bc1678764 100644 --- a/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/TestApp/EntityFrameworkCore/TestAppDbContext.cs +++ b/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/TestApp/EntityFrameworkCore/TestAppDbContext.cs @@ -44,7 +44,7 @@ namespace Volo.Abp.TestApp.EntityFrameworkCore modelBuilder.Entity(b => { - b.ConfigureExtensions(); + //b.ConfigureExtensions(); b.OwnsMany(c => c.Districts, d => { From 1eb034bdd2d7a57cad8ddf416523a9e15638ee2c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Halil=20=C4=B0brahim=20Kalkan?= Date: Wed, 25 Mar 2020 16:59:20 +0300 Subject: [PATCH 11/28] Use ConfigureByConvention by the identity module --- .../IdentityDbContextModelBuilderExtensions.cs | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/modules/identity/src/Volo.Abp.Identity.EntityFrameworkCore/Volo/Abp/Identity/EntityFrameworkCore/IdentityDbContextModelBuilderExtensions.cs b/modules/identity/src/Volo.Abp.Identity.EntityFrameworkCore/Volo/Abp/Identity/EntityFrameworkCore/IdentityDbContextModelBuilderExtensions.cs index fff3d98d11..c5176755d1 100644 --- a/modules/identity/src/Volo.Abp.Identity.EntityFrameworkCore/Volo/Abp/Identity/EntityFrameworkCore/IdentityDbContextModelBuilderExtensions.cs +++ b/modules/identity/src/Volo.Abp.Identity.EntityFrameworkCore/Volo/Abp/Identity/EntityFrameworkCore/IdentityDbContextModelBuilderExtensions.cs @@ -26,9 +26,8 @@ namespace Volo.Abp.Identity.EntityFrameworkCore { b.ToTable(options.TablePrefix + "Users", options.Schema); - b.ConfigureFullAuditedAggregateRoot(); + b.ConfigureByConvention(); b.ConfigureAbpUser(); - b.ConfigureExtensions(); b.Property(u => u.NormalizedUserName).IsRequired().HasMaxLength(IdentityUserConsts.MaxNormalizedUserNameLength).HasColumnName(nameof(IdentityUser.NormalizedUserName)); b.Property(u => u.NormalizedEmail).IsRequired().HasMaxLength(IdentityUserConsts.MaxNormalizedEmailLength).HasColumnName(nameof(IdentityUser.NormalizedEmail)); @@ -53,6 +52,8 @@ namespace Volo.Abp.Identity.EntityFrameworkCore { b.ToTable(options.TablePrefix + "UserClaims", options.Schema); + b.ConfigureByConvention(); + b.Property(x => x.Id).ValueGeneratedNever(); b.Property(uc => uc.ClaimType).HasMaxLength(IdentityUserClaimConsts.MaxClaimTypeLength).IsRequired(); @@ -65,6 +66,8 @@ namespace Volo.Abp.Identity.EntityFrameworkCore { b.ToTable(options.TablePrefix + "UserRoles", options.Schema); + b.ConfigureByConvention(); + b.HasKey(ur => new { ur.UserId, ur.RoleId }); b.HasOne().WithMany().HasForeignKey(ur => ur.RoleId).IsRequired(); @@ -77,6 +80,8 @@ namespace Volo.Abp.Identity.EntityFrameworkCore { b.ToTable(options.TablePrefix + "UserLogins", options.Schema); + b.ConfigureByConvention(); + b.HasKey(x => new { x.UserId, x.LoginProvider }); b.Property(ul => ul.LoginProvider).HasMaxLength(IdentityUserLoginConsts.MaxLoginProviderLength).IsRequired(); @@ -90,6 +95,8 @@ namespace Volo.Abp.Identity.EntityFrameworkCore { b.ToTable(options.TablePrefix + "UserTokens", options.Schema); + b.ConfigureByConvention(); + b.HasKey(l => new { l.UserId, l.LoginProvider, l.Name }); b.Property(ul => ul.LoginProvider).HasMaxLength(IdentityUserTokenConsts.MaxLoginProviderLength).IsRequired(); @@ -100,8 +107,7 @@ namespace Volo.Abp.Identity.EntityFrameworkCore { b.ToTable(options.TablePrefix + "Roles", options.Schema); - b.ConfigureConcurrencyStamp(); - b.ConfigureExtraProperties(); + b.ConfigureByConvention(); b.Property(r => r.Name).IsRequired().HasMaxLength(IdentityRoleConsts.MaxNameLength); b.Property(r => r.NormalizedName).IsRequired().HasMaxLength(IdentityRoleConsts.MaxNormalizedNameLength); @@ -119,6 +125,8 @@ namespace Volo.Abp.Identity.EntityFrameworkCore { b.ToTable(options.TablePrefix + "RoleClaims", options.Schema); + b.ConfigureByConvention(); + b.Property(x => x.Id).ValueGeneratedNever(); b.Property(uc => uc.ClaimType).HasMaxLength(IdentityRoleClaimConsts.MaxClaimTypeLength).IsRequired(); @@ -131,7 +139,7 @@ namespace Volo.Abp.Identity.EntityFrameworkCore { b.ToTable(options.TablePrefix + "ClaimTypes", options.Schema); - b.ConfigureExtraProperties(); + b.ConfigureByConvention(); b.Property(uc => uc.Name).HasMaxLength(IdentityClaimTypeConsts.MaxNameLength).IsRequired(); // make unique b.Property(uc => uc.Regex).HasMaxLength(IdentityClaimTypeConsts.MaxRegexLength); From 037c156ad6a70161debaeb6aabb403011f9f9608 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Halil=20=C4=B0brahim=20Kalkan?= Date: Wed, 25 Mar 2020 17:06:27 +0300 Subject: [PATCH 12/28] Use ConfigureByConvention by the modules --- ...AbpAuditLoggingDbContextModelBuilderExtensions.cs | 8 +++++--- ...BackgroundJobsDbContextModelCreatingExtensions.cs | 3 +-- .../BloggingDbContextModelBuilderExtensions.cs | 12 +++++++----- 3 files changed, 13 insertions(+), 10 deletions(-) diff --git a/modules/audit-logging/src/Volo.Abp.AuditLogging.EntityFrameworkCore/Volo/Abp/AuditLogging/EntityFrameworkCore/AbpAuditLoggingDbContextModelBuilderExtensions.cs b/modules/audit-logging/src/Volo.Abp.AuditLogging.EntityFrameworkCore/Volo/Abp/AuditLogging/EntityFrameworkCore/AbpAuditLoggingDbContextModelBuilderExtensions.cs index 23b4cb3dbe..619eed057c 100644 --- a/modules/audit-logging/src/Volo.Abp.AuditLogging.EntityFrameworkCore/Volo/Abp/AuditLogging/EntityFrameworkCore/AbpAuditLoggingDbContextModelBuilderExtensions.cs +++ b/modules/audit-logging/src/Volo.Abp.AuditLogging.EntityFrameworkCore/Volo/Abp/AuditLogging/EntityFrameworkCore/AbpAuditLoggingDbContextModelBuilderExtensions.cs @@ -25,7 +25,7 @@ namespace Volo.Abp.AuditLogging.EntityFrameworkCore { b.ToTable(options.TablePrefix + "AuditLogs", options.Schema); - b.ConfigureExtraProperties(); + b.ConfigureByConvention(); b.Property(x => x.ApplicationName).HasMaxLength(AuditLogConsts.MaxApplicationNameLength).HasColumnName(nameof(AuditLog.ApplicationName)); b.Property(x => x.ClientIpAddress).HasMaxLength(AuditLogConsts.MaxClientIpAddressLength).HasColumnName(nameof(AuditLog.ClientIpAddress)); @@ -56,7 +56,7 @@ namespace Volo.Abp.AuditLogging.EntityFrameworkCore { b.ToTable(options.TablePrefix + "AuditLogActions", options.Schema); - b.ConfigureExtraProperties(); + b.ConfigureByConvention(); b.Property(x => x.AuditLogId).HasColumnName(nameof(AuditLogAction.AuditLogId)); b.Property(x => x.ServiceName).HasMaxLength(AuditLogActionConsts.MaxServiceNameLength).HasColumnName(nameof(AuditLogAction.ServiceName)); @@ -73,7 +73,7 @@ namespace Volo.Abp.AuditLogging.EntityFrameworkCore { b.ToTable(options.TablePrefix + "EntityChanges", options.Schema); - b.ConfigureExtraProperties(); + b.ConfigureByConvention(); b.Property(x => x.EntityTypeFullName).HasMaxLength(EntityChangeConsts.MaxEntityTypeFullNameLength).IsRequired().HasColumnName(nameof(EntityChange.EntityTypeFullName)); b.Property(x => x.EntityId).HasMaxLength(EntityChangeConsts.MaxEntityIdLength).IsRequired().HasColumnName(nameof(EntityChange.EntityId)); @@ -92,6 +92,8 @@ namespace Volo.Abp.AuditLogging.EntityFrameworkCore { b.ToTable(options.TablePrefix + "EntityPropertyChanges", options.Schema); + b.ConfigureByConvention(); + b.Property(x => x.NewValue).HasMaxLength(EntityPropertyChangeConsts.MaxNewValueLength).HasColumnName(nameof(EntityPropertyChange.NewValue)); b.Property(x => x.PropertyName).HasMaxLength(EntityPropertyChangeConsts.MaxPropertyNameLength).IsRequired().HasColumnName(nameof(EntityPropertyChange.PropertyName)); b.Property(x => x.PropertyTypeFullName).HasMaxLength(EntityPropertyChangeConsts.MaxPropertyTypeFullNameLength).IsRequired().HasColumnName(nameof(EntityPropertyChange.PropertyTypeFullName)); diff --git a/modules/background-jobs/src/Volo.Abp.BackgroundJobs.EntityFrameworkCore/Volo/Abp/BackgroundJobs/EntityFrameworkCore/BackgroundJobsDbContextModelCreatingExtensions.cs b/modules/background-jobs/src/Volo.Abp.BackgroundJobs.EntityFrameworkCore/Volo/Abp/BackgroundJobs/EntityFrameworkCore/BackgroundJobsDbContextModelCreatingExtensions.cs index 8a3e4b126a..0d16130fb5 100644 --- a/modules/background-jobs/src/Volo.Abp.BackgroundJobs.EntityFrameworkCore/Volo/Abp/BackgroundJobs/EntityFrameworkCore/BackgroundJobsDbContextModelCreatingExtensions.cs +++ b/modules/background-jobs/src/Volo.Abp.BackgroundJobs.EntityFrameworkCore/Volo/Abp/BackgroundJobs/EntityFrameworkCore/BackgroundJobsDbContextModelCreatingExtensions.cs @@ -23,8 +23,7 @@ namespace Volo.Abp.BackgroundJobs.EntityFrameworkCore { b.ToTable(options.TablePrefix + "BackgroundJobs", options.Schema); - b.ConfigureCreationTime(); - b.ConfigureExtraProperties(); + b.ConfigureByConvention(); b.Property(x => x.JobName).IsRequired().HasMaxLength(BackgroundJobRecordConsts.MaxJobNameLength); b.Property(x => x.JobArgs).IsRequired().HasMaxLength(BackgroundJobRecordConsts.MaxJobArgsLength); diff --git a/modules/blogging/src/Volo.Blogging.EntityFrameworkCore/Volo/Blogging/EntityFrameworkCore/BloggingDbContextModelBuilderExtensions.cs b/modules/blogging/src/Volo.Blogging.EntityFrameworkCore/Volo/Blogging/EntityFrameworkCore/BloggingDbContextModelBuilderExtensions.cs index 00fd20dbdc..56ef8277d9 100644 --- a/modules/blogging/src/Volo.Blogging.EntityFrameworkCore/Volo/Blogging/EntityFrameworkCore/BloggingDbContextModelBuilderExtensions.cs +++ b/modules/blogging/src/Volo.Blogging.EntityFrameworkCore/Volo/Blogging/EntityFrameworkCore/BloggingDbContextModelBuilderExtensions.cs @@ -31,15 +31,15 @@ namespace Volo.Blogging.EntityFrameworkCore { b.ToTable(options.TablePrefix + "Users", options.Schema); + b.ConfigureByConvention(); b.ConfigureAbpUser(); - b.ConfigureExtraProperties(); }); builder.Entity(b => { b.ToTable(options.TablePrefix + "Blogs", options.Schema); - b.ConfigureFullAuditedAggregateRoot(); + b.ConfigureByConvention(); b.Property(x => x.Name).IsRequired().HasMaxLength(BlogConsts.MaxNameLength).HasColumnName(nameof(Blog.Name)); b.Property(x => x.ShortName).IsRequired().HasMaxLength(BlogConsts.MaxShortNameLength).HasColumnName(nameof(Blog.ShortName)); @@ -50,7 +50,7 @@ namespace Volo.Blogging.EntityFrameworkCore { b.ToTable(options.TablePrefix + "Posts", options.Schema); - b.ConfigureFullAuditedAggregateRoot(); + b.ConfigureByConvention(); b.Property(x => x.BlogId).HasColumnName(nameof(Post.BlogId)); b.Property(x => x.Title).IsRequired().HasMaxLength(PostConsts.MaxTitleLength).HasColumnName(nameof(Post.Title)); @@ -67,7 +67,7 @@ namespace Volo.Blogging.EntityFrameworkCore { b.ToTable(options.TablePrefix + "Comments", options.Schema); - b.ConfigureFullAuditedAggregateRoot(); + b.ConfigureByConvention(); b.Property(x => x.Text).IsRequired().HasMaxLength(CommentConsts.MaxTextLength).HasColumnName(nameof(Comment.Text)); b.Property(x => x.RepliedCommentId).HasColumnName(nameof(Comment.RepliedCommentId)); @@ -81,7 +81,7 @@ namespace Volo.Blogging.EntityFrameworkCore { b.ToTable(options.TablePrefix + "Tags", options.Schema); - b.ConfigureFullAuditedAggregateRoot(); + b.ConfigureByConvention(); b.Property(x => x.Name).IsRequired().HasMaxLength(TagConsts.MaxNameLength).HasColumnName(nameof(Tag.Name)); b.Property(x => x.Description).HasMaxLength(TagConsts.MaxDescriptionLength).HasColumnName(nameof(Tag.Description)); @@ -94,6 +94,8 @@ namespace Volo.Blogging.EntityFrameworkCore { b.ToTable(options.TablePrefix + "PostTags", options.Schema); + b.ConfigureByConvention(); + b.Property(x => x.PostId).HasColumnName(nameof(PostTag.PostId)); b.Property(x => x.TagId).HasColumnName(nameof(PostTag.TagId)); From bf020e81135604508c2f633a83b7a4a40ec304d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Halil=20=C4=B0brahim=20Kalkan?= Date: Wed, 25 Mar 2020 17:22:21 +0300 Subject: [PATCH 13/28] Use ConfigureByConvention by the modules --- .../DocsDbContextModelBuilderExtensions.cs | 2 + ...agementDbContextModelCreatingExtensions.cs | 3 + ...yServerDbContextModelCreatingExtensions.cs | 286 ++++++++++-------- ...nagementDbContextModelBuilderExtensions.cs | 3 + ...nagementDbContextModelBuilderExtensions.cs | 2 + ...agementDbContextModelCreatingExtensions.cs | 4 +- 6 files changed, 170 insertions(+), 130 deletions(-) diff --git a/modules/docs/src/Volo.Docs.EntityFrameworkCore/Volo/Docs/EntityFrameworkCore/DocsDbContextModelBuilderExtensions.cs b/modules/docs/src/Volo.Docs.EntityFrameworkCore/Volo/Docs/EntityFrameworkCore/DocsDbContextModelBuilderExtensions.cs index 64a85db140..379ca8792d 100644 --- a/modules/docs/src/Volo.Docs.EntityFrameworkCore/Volo/Docs/EntityFrameworkCore/DocsDbContextModelBuilderExtensions.cs +++ b/modules/docs/src/Volo.Docs.EntityFrameworkCore/Volo/Docs/EntityFrameworkCore/DocsDbContextModelBuilderExtensions.cs @@ -63,6 +63,8 @@ namespace Volo.Docs.EntityFrameworkCore { b.ToTable(options.TablePrefix + "DocumentContributors", options.Schema); + b.ConfigureByConvention(); + b.HasKey(x => new { x.DocumentId, x.Username }); }); } diff --git a/modules/feature-management/src/Volo.Abp.FeatureManagement.EntityFrameworkCore/Volo/Abp/FeatureManagement/EntityFrameworkCore/FeatureManagementDbContextModelCreatingExtensions.cs b/modules/feature-management/src/Volo.Abp.FeatureManagement.EntityFrameworkCore/Volo/Abp/FeatureManagement/EntityFrameworkCore/FeatureManagementDbContextModelCreatingExtensions.cs index eb8b18ee0a..d854f89448 100644 --- a/modules/feature-management/src/Volo.Abp.FeatureManagement.EntityFrameworkCore/Volo/Abp/FeatureManagement/EntityFrameworkCore/FeatureManagementDbContextModelCreatingExtensions.cs +++ b/modules/feature-management/src/Volo.Abp.FeatureManagement.EntityFrameworkCore/Volo/Abp/FeatureManagement/EntityFrameworkCore/FeatureManagementDbContextModelCreatingExtensions.cs @@ -1,5 +1,6 @@ using System; using Microsoft.EntityFrameworkCore; +using Volo.Abp.EntityFrameworkCore.Modeling; namespace Volo.Abp.FeatureManagement.EntityFrameworkCore { @@ -22,6 +23,8 @@ namespace Volo.Abp.FeatureManagement.EntityFrameworkCore { b.ToTable(options.TablePrefix + "FeatureValues", options.Schema); + b.ConfigureByConvention(); + b.Property(x => x.Name).HasMaxLength(FeatureValueConsts.MaxNameLength).IsRequired(); b.Property(x => x.Value).HasMaxLength(FeatureValueConsts.MaxValueLength).IsRequired(); b.Property(x => x.ProviderName).HasMaxLength(FeatureValueConsts.MaxProviderNameLength); diff --git a/modules/identityserver/src/Volo.Abp.IdentityServer.EntityFrameworkCore/Volo/Abp/IdentityServer/EntityFrameworkCore/IdentityServerDbContextModelCreatingExtensions.cs b/modules/identityserver/src/Volo.Abp.IdentityServer.EntityFrameworkCore/Volo/Abp/IdentityServer/EntityFrameworkCore/IdentityServerDbContextModelCreatingExtensions.cs index 9f65e4e419..277084e13d 100644 --- a/modules/identityserver/src/Volo.Abp.IdentityServer.EntityFrameworkCore/Volo/Abp/IdentityServer/EntityFrameworkCore/IdentityServerDbContextModelCreatingExtensions.cs +++ b/modules/identityserver/src/Volo.Abp.IdentityServer.EntityFrameworkCore/Volo/Abp/IdentityServer/EntityFrameworkCore/IdentityServerDbContextModelCreatingExtensions.cs @@ -28,264 +28,292 @@ namespace Volo.Abp.IdentityServer.EntityFrameworkCore optionsAction?.Invoke(options); - builder.Entity(client => + builder.Entity(b => { - client.ToTable(options.TablePrefix + "Clients", options.Schema); - - client.ConfigureFullAuditedAggregateRoot(); - - client.Property(x => x.ClientId).HasMaxLength(ClientConsts.ClientIdMaxLength).IsRequired(); - client.Property(x => x.ProtocolType).HasMaxLength(ClientConsts.ProtocolTypeMaxLength).IsRequired(); - client.Property(x => x.ClientName).HasMaxLength(ClientConsts.ClientNameMaxLength); - client.Property(x => x.ClientUri).HasMaxLength(ClientConsts.ClientUriMaxLength); - client.Property(x => x.LogoUri).HasMaxLength(ClientConsts.LogoUriMaxLength); - client.Property(x => x.Description).HasMaxLength(ClientConsts.DescriptionMaxLength); - client.Property(x => x.FrontChannelLogoutUri).HasMaxLength(ClientConsts.FrontChannelLogoutUriMaxLength); - client.Property(x => x.BackChannelLogoutUri).HasMaxLength(ClientConsts.BackChannelLogoutUriMaxLength); - client.Property(x => x.ClientClaimsPrefix).HasMaxLength(ClientConsts.ClientClaimsPrefixMaxLength); - client.Property(x => x.PairWiseSubjectSalt).HasMaxLength(ClientConsts.PairWiseSubjectSaltMaxLength); - client.Property(x => x.UserCodeType).HasMaxLength(ClientConsts.UserCodeTypeMaxLength); - - client.HasMany(x => x.AllowedScopes).WithOne().HasForeignKey(x => x.ClientId).IsRequired(); - client.HasMany(x => x.ClientSecrets).WithOne().HasForeignKey(x => x.ClientId).IsRequired(); - client.HasMany(x => x.AllowedGrantTypes).WithOne().HasForeignKey(x => x.ClientId).IsRequired(); - client.HasMany(x => x.AllowedCorsOrigins).WithOne().HasForeignKey(x => x.ClientId).IsRequired(); - client.HasMany(x => x.RedirectUris).WithOne().HasForeignKey(x => x.ClientId).IsRequired(); - client.HasMany(x => x.PostLogoutRedirectUris).WithOne().HasForeignKey(x => x.ClientId).IsRequired(); - client.HasMany(x => x.IdentityProviderRestrictions).WithOne().HasForeignKey(x => x.ClientId).IsRequired(); - client.HasMany(x => x.Claims).WithOne().HasForeignKey(x => x.ClientId).IsRequired(); - client.HasMany(x => x.Properties).WithOne().HasForeignKey(x => x.ClientId).IsRequired(); - - client.HasIndex(x => x.ClientId); + b.ToTable(options.TablePrefix + "Clients", options.Schema); + + b.ConfigureByConvention(); + + b.Property(x => x.ClientId).HasMaxLength(ClientConsts.ClientIdMaxLength).IsRequired(); + b.Property(x => x.ProtocolType).HasMaxLength(ClientConsts.ProtocolTypeMaxLength).IsRequired(); + b.Property(x => x.ClientName).HasMaxLength(ClientConsts.ClientNameMaxLength); + b.Property(x => x.ClientUri).HasMaxLength(ClientConsts.ClientUriMaxLength); + b.Property(x => x.LogoUri).HasMaxLength(ClientConsts.LogoUriMaxLength); + b.Property(x => x.Description).HasMaxLength(ClientConsts.DescriptionMaxLength); + b.Property(x => x.FrontChannelLogoutUri).HasMaxLength(ClientConsts.FrontChannelLogoutUriMaxLength); + b.Property(x => x.BackChannelLogoutUri).HasMaxLength(ClientConsts.BackChannelLogoutUriMaxLength); + b.Property(x => x.ClientClaimsPrefix).HasMaxLength(ClientConsts.ClientClaimsPrefixMaxLength); + b.Property(x => x.PairWiseSubjectSalt).HasMaxLength(ClientConsts.PairWiseSubjectSaltMaxLength); + b.Property(x => x.UserCodeType).HasMaxLength(ClientConsts.UserCodeTypeMaxLength); + + b.HasMany(x => x.AllowedScopes).WithOne().HasForeignKey(x => x.ClientId).IsRequired(); + b.HasMany(x => x.ClientSecrets).WithOne().HasForeignKey(x => x.ClientId).IsRequired(); + b.HasMany(x => x.AllowedGrantTypes).WithOne().HasForeignKey(x => x.ClientId).IsRequired(); + b.HasMany(x => x.AllowedCorsOrigins).WithOne().HasForeignKey(x => x.ClientId).IsRequired(); + b.HasMany(x => x.RedirectUris).WithOne().HasForeignKey(x => x.ClientId).IsRequired(); + b.HasMany(x => x.PostLogoutRedirectUris).WithOne().HasForeignKey(x => x.ClientId).IsRequired(); + b.HasMany(x => x.IdentityProviderRestrictions).WithOne().HasForeignKey(x => x.ClientId).IsRequired(); + b.HasMany(x => x.Claims).WithOne().HasForeignKey(x => x.ClientId).IsRequired(); + b.HasMany(x => x.Properties).WithOne().HasForeignKey(x => x.ClientId).IsRequired(); + + b.HasIndex(x => x.ClientId); }); - builder.Entity(grantType => + builder.Entity(b => { - grantType.ToTable(options.TablePrefix + "ClientGrantTypes", options.Schema); + b.ToTable(options.TablePrefix + "ClientGrantTypes", options.Schema); + + b.ConfigureByConvention(); - grantType.HasKey(x => new { x.ClientId, x.GrantType }); + b.HasKey(x => new { x.ClientId, x.GrantType }); - grantType.Property(x => x.GrantType).HasMaxLength(ClientGrantTypeConsts.GrantTypeMaxLength).IsRequired(); + b.Property(x => x.GrantType).HasMaxLength(ClientGrantTypeConsts.GrantTypeMaxLength).IsRequired(); }); - builder.Entity(redirectUri => + builder.Entity(b => { - redirectUri.ToTable(options.TablePrefix + "ClientRedirectUris", options.Schema); + b.ToTable(options.TablePrefix + "ClientRedirectUris", options.Schema); - redirectUri.HasKey(x => new { x.ClientId, x.RedirectUri }); + b.ConfigureByConvention(); + + b.HasKey(x => new { x.ClientId, x.RedirectUri }); if (options.DatabaseProvider == EfCoreDatabaseProvider.MySql) { - redirectUri.Property(x => x.RedirectUri).HasMaxLength(300).IsRequired(); + b.Property(x => x.RedirectUri).HasMaxLength(300).IsRequired(); } else { - redirectUri.Property(x => x.RedirectUri).HasMaxLength(ClientRedirectUriConsts.RedirectUriMaxLength).IsRequired(); + b.Property(x => x.RedirectUri).HasMaxLength(ClientRedirectUriConsts.RedirectUriMaxLength).IsRequired(); } }); - builder.Entity(postLogoutRedirectUri => + builder.Entity(b => { - postLogoutRedirectUri.ToTable(options.TablePrefix + "ClientPostLogoutRedirectUris", options.Schema); + b.ToTable(options.TablePrefix + "ClientPostLogoutRedirectUris", options.Schema); + + b.ConfigureByConvention(); - postLogoutRedirectUri.HasKey(x => new { x.ClientId, x.PostLogoutRedirectUri }); + b.HasKey(x => new { x.ClientId, x.PostLogoutRedirectUri }); if (options.DatabaseProvider == EfCoreDatabaseProvider.MySql) { - postLogoutRedirectUri.Property(x => x.PostLogoutRedirectUri).HasMaxLength(300).IsRequired(); + b.Property(x => x.PostLogoutRedirectUri).HasMaxLength(300).IsRequired(); } else { - postLogoutRedirectUri.Property(x => x.PostLogoutRedirectUri).HasMaxLength(ClientPostLogoutRedirectUriConsts.PostLogoutRedirectUriMaxLength).IsRequired(); + b.Property(x => x.PostLogoutRedirectUri).HasMaxLength(ClientPostLogoutRedirectUriConsts.PostLogoutRedirectUriMaxLength).IsRequired(); } }); - builder.Entity(scope => + builder.Entity(b => { - scope.ToTable(options.TablePrefix + "ClientScopes", options.Schema); + b.ToTable(options.TablePrefix + "ClientScopes", options.Schema); - scope.HasKey(x => new { x.ClientId, x.Scope }); + b.ConfigureByConvention(); + + b.HasKey(x => new { x.ClientId, x.Scope }); - scope.Property(x => x.Scope).HasMaxLength(ClientScopeConsts.ScopeMaxLength).IsRequired(); + b.Property(x => x.Scope).HasMaxLength(ClientScopeConsts.ScopeMaxLength).IsRequired(); }); - builder.Entity(secret => + builder.Entity(b => { - secret.ToTable(options.TablePrefix + "ClientSecrets", options.Schema); + b.ToTable(options.TablePrefix + "ClientSecrets", options.Schema); - secret.HasKey(x => new { x.ClientId, x.Type, x.Value }); + b.ConfigureByConvention(); - secret.Property(x => x.Type).HasMaxLength(SecretConsts.TypeMaxLength).IsRequired(); + b.HasKey(x => new { x.ClientId, x.Type, x.Value }); + + b.Property(x => x.Type).HasMaxLength(SecretConsts.TypeMaxLength).IsRequired(); if (options.DatabaseProvider == EfCoreDatabaseProvider.MySql) { - secret.Property(x => x.Value).HasMaxLength(300).IsRequired(); + b.Property(x => x.Value).HasMaxLength(300).IsRequired(); } else { - secret.Property(x => x.Value).HasMaxLength(SecretConsts.ValueMaxLength).IsRequired(); + b.Property(x => x.Value).HasMaxLength(SecretConsts.ValueMaxLength).IsRequired(); } - secret.Property(x => x.Description).HasMaxLength(SecretConsts.DescriptionMaxLength); + b.Property(x => x.Description).HasMaxLength(SecretConsts.DescriptionMaxLength); }); - builder.Entity(claim => + builder.Entity(b => { - claim.ToTable(options.TablePrefix + "ClientClaims", options.Schema); + b.ToTable(options.TablePrefix + "ClientClaims", options.Schema); - claim.HasKey(x => new { x.ClientId, x.Type, x.Value }); + b.ConfigureByConvention(); - claim.Property(x => x.Type).HasMaxLength(ClientClaimConsts.TypeMaxLength).IsRequired(); - claim.Property(x => x.Value).HasMaxLength(ClientClaimConsts.ValueMaxLength).IsRequired(); + b.HasKey(x => new { x.ClientId, x.Type, x.Value }); + + b.Property(x => x.Type).HasMaxLength(ClientClaimConsts.TypeMaxLength).IsRequired(); + b.Property(x => x.Value).HasMaxLength(ClientClaimConsts.ValueMaxLength).IsRequired(); }); - builder.Entity(idPRestriction => + builder.Entity(b => { - idPRestriction.ToTable(options.TablePrefix + "ClientIdPRestrictions", options.Schema); + b.ToTable(options.TablePrefix + "ClientIdPRestrictions", options.Schema); + + b.ConfigureByConvention(); - idPRestriction.HasKey(x => new { x.ClientId, x.Provider }); + b.HasKey(x => new { x.ClientId, x.Provider }); - idPRestriction.Property(x => x.Provider).HasMaxLength(ClientIdPRestrictionConsts.ProviderMaxLength).IsRequired(); + b.Property(x => x.Provider).HasMaxLength(ClientIdPRestrictionConsts.ProviderMaxLength).IsRequired(); }); - builder.Entity(corsOrigin => + builder.Entity(b => { - corsOrigin.ToTable(options.TablePrefix + "ClientCorsOrigins", options.Schema); + b.ToTable(options.TablePrefix + "ClientCorsOrigins", options.Schema); - corsOrigin.HasKey(x => new { x.ClientId, x.Origin }); + b.ConfigureByConvention(); - corsOrigin.Property(x => x.Origin).HasMaxLength(ClientCorsOriginConsts.OriginMaxLength).IsRequired(); + b.HasKey(x => new { x.ClientId, x.Origin }); + + b.Property(x => x.Origin).HasMaxLength(ClientCorsOriginConsts.OriginMaxLength).IsRequired(); }); - builder.Entity(property => + builder.Entity(b => { - property.ToTable(options.TablePrefix + "ClientProperties", options.Schema); + b.ToTable(options.TablePrefix + "ClientProperties", options.Schema); + + b.ConfigureByConvention(); - property.HasKey(x => new { x.ClientId, x.Key }); + b.HasKey(x => new { x.ClientId, x.Key }); - property.Property(x => x.Key).HasMaxLength(ClientPropertyConsts.KeyMaxLength).IsRequired(); - property.Property(x => x.Value).HasMaxLength(ClientPropertyConsts.ValueMaxLength).IsRequired(); + b.Property(x => x.Key).HasMaxLength(ClientPropertyConsts.KeyMaxLength).IsRequired(); + b.Property(x => x.Value).HasMaxLength(ClientPropertyConsts.ValueMaxLength).IsRequired(); }); - builder.Entity(grant => + builder.Entity(b => { - grant.ToTable(options.TablePrefix + "PersistedGrants", options.Schema); + b.ToTable(options.TablePrefix + "PersistedGrants", options.Schema); - grant.ConfigureExtraProperties(); + b.ConfigureByConvention(); - grant.Property(x => x.Key).HasMaxLength(PersistedGrantConsts.KeyMaxLength).ValueGeneratedNever(); - grant.Property(x => x.Type).HasMaxLength(PersistedGrantConsts.TypeMaxLength).IsRequired(); - grant.Property(x => x.SubjectId).HasMaxLength(PersistedGrantConsts.SubjectIdMaxLength); - grant.Property(x => x.ClientId).HasMaxLength(PersistedGrantConsts.ClientIdMaxLength).IsRequired(); - grant.Property(x => x.CreationTime).IsRequired(); + b.Property(x => x.Key).HasMaxLength(PersistedGrantConsts.KeyMaxLength).ValueGeneratedNever(); + b.Property(x => x.Type).HasMaxLength(PersistedGrantConsts.TypeMaxLength).IsRequired(); + b.Property(x => x.SubjectId).HasMaxLength(PersistedGrantConsts.SubjectIdMaxLength); + b.Property(x => x.ClientId).HasMaxLength(PersistedGrantConsts.ClientIdMaxLength).IsRequired(); + b.Property(x => x.CreationTime).IsRequired(); if (options.DatabaseProvider == EfCoreDatabaseProvider.MySql) { - grant.Property(x => x.Data).HasMaxLength(10000).IsRequired(); + b.Property(x => x.Data).HasMaxLength(10000).IsRequired(); } else { - grant.Property(x => x.Data).HasMaxLength(PersistedGrantConsts.DataMaxLength).IsRequired(); + b.Property(x => x.Data).HasMaxLength(PersistedGrantConsts.DataMaxLength).IsRequired(); } - grant.HasKey(x => x.Key); //TODO: What about Id!!! + b.HasKey(x => x.Key); //TODO: What about Id!!! - grant.HasIndex(x => new { x.SubjectId, x.ClientId, x.Type }); - grant.HasIndex(x => x.Expiration); + b.HasIndex(x => new { x.SubjectId, x.ClientId, x.Type }); + b.HasIndex(x => x.Expiration); }); - builder.Entity(identityResource => + builder.Entity(b => { - identityResource.ToTable(options.TablePrefix + "IdentityResources", options.Schema); + b.ToTable(options.TablePrefix + "IdentityResources", options.Schema); - identityResource.ConfigureFullAuditedAggregateRoot(); + b.ConfigureByConvention(); - identityResource.Property(x => x.Name).HasMaxLength(IdentityResourceConsts.NameMaxLength).IsRequired(); - identityResource.Property(x => x.DisplayName).HasMaxLength(IdentityResourceConsts.DisplayNameMaxLength); - identityResource.Property(x => x.Description).HasMaxLength(IdentityResourceConsts.DescriptionMaxLength); - identityResource.Property(x => x.Properties) + b.Property(x => x.Name).HasMaxLength(IdentityResourceConsts.NameMaxLength).IsRequired(); + b.Property(x => x.DisplayName).HasMaxLength(IdentityResourceConsts.DisplayNameMaxLength); + b.Property(x => x.Description).HasMaxLength(IdentityResourceConsts.DescriptionMaxLength); + b.Property(x => x.Properties) .HasConversion(new AbpJsonValueConverter>()) .Metadata.SetValueComparer(new AbpDictionaryValueComparer()); - identityResource.HasMany(x => x.UserClaims).WithOne().HasForeignKey(x => x.IdentityResourceId).IsRequired(); + b.HasMany(x => x.UserClaims).WithOne().HasForeignKey(x => x.IdentityResourceId).IsRequired(); }); - builder.Entity(claim => + builder.Entity(b => { - claim.ToTable(options.TablePrefix + "IdentityClaims", options.Schema); + b.ToTable(options.TablePrefix + "IdentityClaims", options.Schema); + + b.ConfigureByConvention(); - claim.HasKey(x => new { x.IdentityResourceId, x.Type }); + b.HasKey(x => new { x.IdentityResourceId, x.Type }); - claim.Property(x => x.Type).HasMaxLength(UserClaimConsts.TypeMaxLength).IsRequired(); + b.Property(x => x.Type).HasMaxLength(UserClaimConsts.TypeMaxLength).IsRequired(); }); - builder.Entity(apiResource => + builder.Entity(b => { - apiResource.ToTable(options.TablePrefix + "ApiResources", options.Schema); + b.ToTable(options.TablePrefix + "ApiResources", options.Schema); - apiResource.ConfigureFullAuditedAggregateRoot(); + b.ConfigureByConvention(); - apiResource.Property(x => x.Name).HasMaxLength(ApiResourceConsts.NameMaxLength).IsRequired(); - apiResource.Property(x => x.DisplayName).HasMaxLength(ApiResourceConsts.DisplayNameMaxLength); - apiResource.Property(x => x.Description).HasMaxLength(ApiResourceConsts.DescriptionMaxLength); - apiResource.Property(x => x.Properties) + b.Property(x => x.Name).HasMaxLength(ApiResourceConsts.NameMaxLength).IsRequired(); + b.Property(x => x.DisplayName).HasMaxLength(ApiResourceConsts.DisplayNameMaxLength); + b.Property(x => x.Description).HasMaxLength(ApiResourceConsts.DescriptionMaxLength); + b.Property(x => x.Properties) .HasConversion(new AbpJsonValueConverter>()) .Metadata.SetValueComparer(new AbpDictionaryValueComparer()); - apiResource.HasMany(x => x.Secrets).WithOne().HasForeignKey(x => x.ApiResourceId).IsRequired(); - apiResource.HasMany(x => x.Scopes).WithOne().HasForeignKey(x => x.ApiResourceId).IsRequired(); - apiResource.HasMany(x => x.UserClaims).WithOne().HasForeignKey(x => x.ApiResourceId).IsRequired(); + b.HasMany(x => x.Secrets).WithOne().HasForeignKey(x => x.ApiResourceId).IsRequired(); + b.HasMany(x => x.Scopes).WithOne().HasForeignKey(x => x.ApiResourceId).IsRequired(); + b.HasMany(x => x.UserClaims).WithOne().HasForeignKey(x => x.ApiResourceId).IsRequired(); }); - builder.Entity(apiSecret => + builder.Entity(b => { - apiSecret.ToTable(options.TablePrefix + "ApiSecrets", options.Schema); + b.ToTable(options.TablePrefix + "ApiSecrets", options.Schema); - apiSecret.HasKey(x => new { x.ApiResourceId, x.Type, x.Value }); + b.ConfigureByConvention(); + + b.HasKey(x => new { x.ApiResourceId, x.Type, x.Value }); - apiSecret.Property(x => x.Type).HasMaxLength(SecretConsts.TypeMaxLength).IsRequired(); - apiSecret.Property(x => x.Description).HasMaxLength(SecretConsts.DescriptionMaxLength); + b.Property(x => x.Type).HasMaxLength(SecretConsts.TypeMaxLength).IsRequired(); + b.Property(x => x.Description).HasMaxLength(SecretConsts.DescriptionMaxLength); if (options.DatabaseProvider == EfCoreDatabaseProvider.MySql) { - apiSecret.Property(x => x.Value).HasMaxLength(300).IsRequired(); + b.Property(x => x.Value).HasMaxLength(300).IsRequired(); } else { - apiSecret.Property(x => x.Value).HasMaxLength(SecretConsts.ValueMaxLength).IsRequired(); + b.Property(x => x.Value).HasMaxLength(SecretConsts.ValueMaxLength).IsRequired(); } }); - builder.Entity(apiClaim => + builder.Entity(b => { - apiClaim.ToTable(options.TablePrefix + "ApiClaims", options.Schema); + b.ToTable(options.TablePrefix + "ApiClaims", options.Schema); + + b.ConfigureByConvention(); - apiClaim.HasKey(x => new { x.ApiResourceId, x.Type }); + b.HasKey(x => new { x.ApiResourceId, x.Type }); - apiClaim.Property(x => x.Type).HasMaxLength(UserClaimConsts.TypeMaxLength).IsRequired(); + b.Property(x => x.Type).HasMaxLength(UserClaimConsts.TypeMaxLength).IsRequired(); }); - builder.Entity(apiScope => + builder.Entity(b => { - apiScope.ToTable(options.TablePrefix + "ApiScopes", options.Schema); + b.ToTable(options.TablePrefix + "ApiScopes", options.Schema); - apiScope.HasKey(x => new { x.ApiResourceId, x.Name }); + b.ConfigureByConvention(); + + b.HasKey(x => new { x.ApiResourceId, x.Name }); - apiScope.Property(x => x.Name).HasMaxLength(ApiScopeConsts.NameMaxLength).IsRequired(); - apiScope.Property(x => x.DisplayName).HasMaxLength(ApiScopeConsts.DisplayNameMaxLength); - apiScope.Property(x => x.Description).HasMaxLength(ApiScopeConsts.DescriptionMaxLength); + b.Property(x => x.Name).HasMaxLength(ApiScopeConsts.NameMaxLength).IsRequired(); + b.Property(x => x.DisplayName).HasMaxLength(ApiScopeConsts.DisplayNameMaxLength); + b.Property(x => x.Description).HasMaxLength(ApiScopeConsts.DescriptionMaxLength); - apiScope.HasMany(x => x.UserClaims).WithOne().HasForeignKey(x => new { x.ApiResourceId, x.Name }).IsRequired(); + b.HasMany(x => x.UserClaims).WithOne().HasForeignKey(x => new { x.ApiResourceId, x.Name }).IsRequired(); }); - builder.Entity(apiScopeClaim => + builder.Entity(b => { - apiScopeClaim.ToTable(options.TablePrefix + "ApiScopeClaims", options.Schema); + b.ToTable(options.TablePrefix + "ApiScopeClaims", options.Schema); + + b.ConfigureByConvention(); - apiScopeClaim.HasKey(x => new { x.ApiResourceId, x.Name, x.Type }); + b.HasKey(x => new { x.ApiResourceId, x.Name, x.Type }); - apiScopeClaim.Property(x => x.Type).HasMaxLength(UserClaimConsts.TypeMaxLength).IsRequired(); - apiScopeClaim.Property(x => x.Name).HasMaxLength(ApiScopeConsts.NameMaxLength).IsRequired(); + b.Property(x => x.Type).HasMaxLength(UserClaimConsts.TypeMaxLength).IsRequired(); + b.Property(x => x.Name).HasMaxLength(ApiScopeConsts.NameMaxLength).IsRequired(); }); builder.Entity(b => diff --git a/modules/permission-management/src/Volo.Abp.PermissionManagement.EntityFrameworkCore/Volo/Abp/PermissionManagement/EntityFrameworkCore/AbpPermissionManagementDbContextModelBuilderExtensions.cs b/modules/permission-management/src/Volo.Abp.PermissionManagement.EntityFrameworkCore/Volo/Abp/PermissionManagement/EntityFrameworkCore/AbpPermissionManagementDbContextModelBuilderExtensions.cs index 6fefa69c8f..419389568a 100644 --- a/modules/permission-management/src/Volo.Abp.PermissionManagement.EntityFrameworkCore/Volo/Abp/PermissionManagement/EntityFrameworkCore/AbpPermissionManagementDbContextModelBuilderExtensions.cs +++ b/modules/permission-management/src/Volo.Abp.PermissionManagement.EntityFrameworkCore/Volo/Abp/PermissionManagement/EntityFrameworkCore/AbpPermissionManagementDbContextModelBuilderExtensions.cs @@ -1,6 +1,7 @@ using System; using JetBrains.Annotations; using Microsoft.EntityFrameworkCore; +using Volo.Abp.EntityFrameworkCore.Modeling; namespace Volo.Abp.PermissionManagement.EntityFrameworkCore { @@ -23,6 +24,8 @@ namespace Volo.Abp.PermissionManagement.EntityFrameworkCore { b.ToTable(options.TablePrefix + "PermissionGrants", options.Schema); + b.ConfigureByConvention(); + b.Property(x => x.Name).HasMaxLength(PermissionGrantConsts.MaxNameLength).IsRequired(); b.Property(x => x.ProviderName).HasMaxLength(PermissionGrantConsts.MaxProviderNameLength).IsRequired(); b.Property(x => x.ProviderKey).HasMaxLength(PermissionGrantConsts.MaxProviderKeyLength).IsRequired(); diff --git a/modules/setting-management/src/Volo.Abp.SettingManagement.EntityFrameworkCore/Volo/Abp/SettingManagement/EntityFrameworkCore/SettingManagementDbContextModelBuilderExtensions.cs b/modules/setting-management/src/Volo.Abp.SettingManagement.EntityFrameworkCore/Volo/Abp/SettingManagement/EntityFrameworkCore/SettingManagementDbContextModelBuilderExtensions.cs index d0de79aa7e..715d7abfd0 100644 --- a/modules/setting-management/src/Volo.Abp.SettingManagement.EntityFrameworkCore/Volo/Abp/SettingManagement/EntityFrameworkCore/SettingManagementDbContextModelBuilderExtensions.cs +++ b/modules/setting-management/src/Volo.Abp.SettingManagement.EntityFrameworkCore/Volo/Abp/SettingManagement/EntityFrameworkCore/SettingManagementDbContextModelBuilderExtensions.cs @@ -38,6 +38,8 @@ namespace Volo.Abp.SettingManagement.EntityFrameworkCore { b.ToTable(options.TablePrefix + "Settings", options.Schema); + b.ConfigureByConvention(); + b.Property(x => x.Name).HasMaxLength(SettingConsts.MaxNameLength).IsRequired(); b.Property(x => x.Value).HasMaxLength(SettingConsts.MaxValueLength).IsRequired(); b.Property(x => x.ProviderName).HasMaxLength(SettingConsts.MaxProviderNameLength); diff --git a/modules/tenant-management/src/Volo.Abp.TenantManagement.EntityFrameworkCore/Volo/Abp/TenantManagement/EntityFrameworkCore/AbpTenantManagementDbContextModelCreatingExtensions.cs b/modules/tenant-management/src/Volo.Abp.TenantManagement.EntityFrameworkCore/Volo/Abp/TenantManagement/EntityFrameworkCore/AbpTenantManagementDbContextModelCreatingExtensions.cs index 2cb7964041..03858df202 100644 --- a/modules/tenant-management/src/Volo.Abp.TenantManagement.EntityFrameworkCore/Volo/Abp/TenantManagement/EntityFrameworkCore/AbpTenantManagementDbContextModelCreatingExtensions.cs +++ b/modules/tenant-management/src/Volo.Abp.TenantManagement.EntityFrameworkCore/Volo/Abp/TenantManagement/EntityFrameworkCore/AbpTenantManagementDbContextModelCreatingExtensions.cs @@ -24,7 +24,7 @@ namespace Volo.Abp.TenantManagement.EntityFrameworkCore { b.ToTable(options.TablePrefix + "Tenants", options.Schema); - b.ConfigureFullAuditedAggregateRoot(); + b.ConfigureByConvention(); b.Property(t => t.Name).IsRequired().HasMaxLength(TenantConsts.MaxNameLength); @@ -37,6 +37,8 @@ namespace Volo.Abp.TenantManagement.EntityFrameworkCore { b.ToTable(options.TablePrefix + "TenantConnectionStrings", options.Schema); + b.ConfigureByConvention(); + b.HasKey(x => new { x.TenantId, x.Name }); b.Property(cs => cs.Name).IsRequired().HasMaxLength(TenantConnectionStringConsts.MaxNameLength); From 21d49d4156fa55836d5af94e18a61f7428231225 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Halil=20=C4=B0brahim=20Kalkan?= Date: Wed, 25 Mar 2020 17:22:39 +0300 Subject: [PATCH 14/28] Document to use ConfigureByConvention as a best practice. --- docs/en/Best-Practices/Entity-Framework-Core-Integration.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/en/Best-Practices/Entity-Framework-Core-Integration.md b/docs/en/Best-Practices/Entity-Framework-Core-Integration.md index 4f7e20dd70..db8117d798 100644 --- a/docs/en/Best-Practices/Entity-Framework-Core-Integration.md +++ b/docs/en/Best-Practices/Entity-Framework-Core-Integration.md @@ -89,13 +89,15 @@ public static class IdentityDbContextModelBuilderExtensions builder.Entity(b => { - b.ToTable(options.TablePrefix + "Users", options.Schema); + b.ToTable(options.TablePrefix + "Users", options.Schema); + b.ConfigureByConvention(); //code omitted for brevity }); builder.Entity(b => { b.ToTable(options.TablePrefix + "UserClaims", options.Schema); + b.ConfigureByConvention(); //code omitted for brevity }); @@ -104,6 +106,7 @@ public static class IdentityDbContextModelBuilderExtensions } ```` +* **Do** call `b.ConfigureByConvention();` for each entity mapping (as shown above). * **Do** create a **configuration options** class by inheriting from the `ModelBuilderConfigurationOptions`. Example: ````C# From fc3040adfd73a191d8a56db60c35a42c0fa0ed3a Mon Sep 17 00:00:00 2001 From: mehmet-erim Date: Wed, 25 Mar 2020 17:29:40 +0300 Subject: [PATCH 15/28] chore: update symlink manager version --- npm/ng-packs/package.json | 2 +- npm/ng-packs/yarn.lock | 40 +++++++++++++++++++-------------------- 2 files changed, 21 insertions(+), 21 deletions(-) diff --git a/npm/ng-packs/package.json b/npm/ng-packs/package.json index 2c60ba2e83..5092ae340c 100644 --- a/npm/ng-packs/package.json +++ b/npm/ng-packs/package.json @@ -81,7 +81,7 @@ "protractor": "~5.4.0", "rxjs": "~6.4.0", "snq": "^1.0.3", - "symlink-manager": "^1.4.2", + "symlink-manager": "^1.4.3", "ts-node": "~7.0.0", "ts-toolbelt": "^6.3.6", "tsickle": "^0.37.0", diff --git a/npm/ng-packs/yarn.lock b/npm/ng-packs/yarn.lock index a9d0291af1..e4bf556c6b 100644 --- a/npm/ng-packs/yarn.lock +++ b/npm/ng-packs/yarn.lock @@ -5253,18 +5253,18 @@ execa@^1.0.0: signal-exit "^3.0.0" strip-eof "^1.0.0" -execa@^2.0.3: - version "2.1.0" - resolved "https://registry.yarnpkg.com/execa/-/execa-2.1.0.tgz#e5d3ecd837d2a60ec50f3da78fd39767747bbe99" - integrity sha512-Y/URAVapfbYy2Xp/gb6A0E7iR8xeqOCXsuuaoMn7A5PzrXUK84E1gyiEfq0wQd/GHA6GsoHWwhNq8anb0mleIw== +execa@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/execa/-/execa-4.0.0.tgz#7f37d6ec17f09e6b8fc53288611695b6d12b9daf" + integrity sha512-JbDUxwV3BoT5ZVXQrSVbAiaXhXUkIwvbhPIwZ0N13kX+5yCzOhUNdocxB/UQRuYOHRYYwAxKYwJYc0T4D12pDA== dependencies: cross-spawn "^7.0.0" get-stream "^5.0.0" + human-signals "^1.1.1" is-stream "^2.0.0" merge-stream "^2.0.0" - npm-run-path "^3.0.0" + npm-run-path "^4.0.0" onetime "^5.1.0" - p-finally "^2.0.0" signal-exit "^3.0.2" strip-final-newline "^2.0.0" @@ -6291,6 +6291,11 @@ https-proxy-agent@^2.2.1, https-proxy-agent@^2.2.3: agent-base "^4.3.0" debug "^3.1.0" +human-signals@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-1.1.1.tgz#c5b1cd14f50aeae09ab6c59fe63ba3395fe4dfa3" + integrity sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw== + humanize-ms@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/humanize-ms/-/humanize-ms-1.2.1.tgz#c46e3159a293f6b896da29316d8b6fe8bb79bbed" @@ -8804,10 +8809,10 @@ npm-run-path@^2.0.0: dependencies: path-key "^2.0.0" -npm-run-path@^3.0.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-3.1.0.tgz#7f91be317f6a466efed3c9f2980ad8a4ee8b0fa5" - integrity sha512-Dbl4A/VfiVGLgQv29URL9xshU8XDY1GeLy+fsaZ1AA8JDSfjvr5P5+pzRbWqRSBxk6/DW7MIh8lTM/PaGnP2kg== +npm-run-path@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-4.0.1.tgz#b7ecd1e5ed53da8e37a55e1c2269e0b97ed748ea" + integrity sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw== dependencies: path-key "^3.0.0" @@ -9060,11 +9065,6 @@ p-finally@^1.0.0: resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae" integrity sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4= -p-finally@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-2.0.1.tgz#bd6fcaa9c559a096b680806f4d657b3f0f240561" - integrity sha512-vpm09aKwq6H9phqRQzecoDpD8TmVyGw70qmWlyq5onxY7tqyTTFVvxMykxQSQKILBSFlbXpypIw2T1Ml7+DDtw== - p-is-promise@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/p-is-promise/-/p-is-promise-2.1.0.tgz#918cebaea248a62cf7ffab8e3bca8c5f882fc42e" @@ -11376,16 +11376,16 @@ symbol-tree@^3.2.2: resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.4.tgz#430637d248ba77e078883951fb9aa0eed7c63fa2" integrity sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw== -symlink-manager@^1.4.2: - version "1.4.2" - resolved "https://registry.yarnpkg.com/symlink-manager/-/symlink-manager-1.4.2.tgz#8ac78ed829637e435cfc61dcd181b26c3ddb61b1" - integrity sha512-FObjOy2UqeX84MqT0CtuincfIDwieYF85TdyffJhALhpvSvoSTdcWE7YCf1lPuJfrO3ezft/dEuvqy0/BGZkFg== +symlink-manager@^1.4.3: + version "1.4.3" + resolved "https://registry.yarnpkg.com/symlink-manager/-/symlink-manager-1.4.3.tgz#c6ada630dd655eecdb7fb10805f54357d8d3dfbd" + integrity sha512-faiwvs0KkNKNdEEUtIXEHDZV/7fULToYONwOKrzVZ0Z4p5ajm7zSGhnKTJgm8WgOcUzhwImJ4Sxo2GOs5k/wSA== dependencies: arg "^4.1.0" chokidar "^3.0.2" color-support "^1.1.3" esm "^3.2.25" - execa "^2.0.3" + execa "^4.0.0" figlet "^1.2.3" fs-extra "^8.1.0" inquirer "^6.4.1" From 439ca98c8206ca7babff23f748a38a8fab49252a Mon Sep 17 00:00:00 2001 From: Arman Ozak Date: Wed, 25 Mar 2020 17:53:32 +0300 Subject: [PATCH 16/28] feat(core): make TrackByService publicly available --- npm/ng-packs/packages/core/src/lib/services/index.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/npm/ng-packs/packages/core/src/lib/services/index.ts b/npm/ng-packs/packages/core/src/lib/services/index.ts index e7f7f9b985..ad23b74fae 100644 --- a/npm/ng-packs/packages/core/src/lib/services/index.ts +++ b/npm/ng-packs/packages/core/src/lib/services/index.ts @@ -3,7 +3,8 @@ export * from './auth.service'; export * from './config-state.service'; export * from './lazy-load.service'; export * from './localization.service'; +export * from './profile-state.service'; export * from './profile.service'; export * from './rest.service'; -export * from './profile-state.service'; export * from './session-state.service'; +export * from './track-by.service'; From 75a13a0aa4acab0471e92ca95a1904a9ed4efd9e Mon Sep 17 00:00:00 2001 From: Arman Ozak Date: Wed, 25 Mar 2020 17:54:11 +0300 Subject: [PATCH 17/28] feat(core): add doubly linked list as shared utility --- .../core/src/lib/tests/linked-list.spec.ts | 720 ++++++++++++++++++ .../packages/core/src/lib/utils/index.ts | 1 + .../core/src/lib/utils/linked-list.ts | 240 ++++++ 3 files changed, 961 insertions(+) create mode 100644 npm/ng-packs/packages/core/src/lib/tests/linked-list.spec.ts create mode 100644 npm/ng-packs/packages/core/src/lib/utils/linked-list.ts diff --git a/npm/ng-packs/packages/core/src/lib/tests/linked-list.spec.ts b/npm/ng-packs/packages/core/src/lib/tests/linked-list.spec.ts new file mode 100644 index 0000000000..cd0a00096e --- /dev/null +++ b/npm/ng-packs/packages/core/src/lib/tests/linked-list.spec.ts @@ -0,0 +1,720 @@ +import { LinkedList } from '../utils/linked-list'; + +describe('Linked List (Doubly)', () => { + let list: LinkedList; + + beforeEach(() => (list = new LinkedList())); + + describe('#length', () => { + it('should initially be 0', () => { + expect(list.length).toBe(0); + }); + }); + + describe('#head', () => { + it('should initially be undefined', () => { + expect(list.head).toBeUndefined(); + }); + }); + + describe('#tail', () => { + it('should initially be undefined', () => { + expect(list.tail).toBeUndefined(); + }); + }); + + describe('#add', () => { + describe('#head', () => { + it('should add node to the head of the list', () => { + list.addHead('a'); + + // "a" + + expect(list.head.value).toBe('a'); + expect(list.tail.value).toBe('a'); + }); + + it('should create reference to previous and next nodes', () => { + list.add('a').head(); + list.add('b').head(); + list.add('c').head(); + + // "c" <-> "b" <-> "a" + + expect(list.length).toBe(3); + expect(list.head.value).toBe('c'); + expect(list.head.next.value).toBe('b'); + expect(list.head.previous).toBeUndefined(); + expect(list.tail.value).toBe('a'); + expect(list.tail.previous.value).toBe('b'); + expect(list.tail.next).toBeUndefined(); + }); + }); + + describe('#tail', () => { + it('should add node to the tail of the list', () => { + list.addTail('a'); + + // "a" + + expect(list.head.value).toBe('a'); + expect(list.tail.value).toBe('a'); + expect(list.tail.next).toBeUndefined(); + }); + + it('should create reference to previous and next nodes', () => { + list.add('a').tail(); + list.add('b').tail(); + list.add('c').tail(); + + // "a" <-> "b" <-> "c" + + expect(list.length).toBe(3); + expect(list.head.value).toBe('a'); + expect(list.head.next.value).toBe('b'); + expect(list.head.previous).toBeUndefined(); + expect(list.tail.value).toBe('c'); + expect(list.tail.previous.value).toBe('b'); + expect(list.tail.next).toBeUndefined(); + }); + }); + + describe('#after', () => { + it('should place a node after node with given value', () => { + list.add('a').tail(); + list.add('b').tail(); + list.add('c').tail(); + + // "a" <-> "b" <-> "c" + + list.add('x').after('b'); + + // "a" <-> "b" <-> "x" <-> "c" + + expect(list.length).toBe(4); + expect(list.head.value).toBe('a'); + expect(list.head.next.value).toBe('b'); + expect(list.head.next.next.value).toBe('x'); + expect(list.head.next.next.next.value).toBe('c'); + expect(list.tail.value).toBe('c'); + expect(list.tail.previous.value).toBe('x'); + expect(list.tail.previous.previous.value).toBe('b'); + expect(list.tail.previous.previous.previous.value).toBe('a'); + }); + + it('should be able to receive a custom compareFn', () => { + list.add({ x: 1 }).tail(); + list.add({ x: 2 }).tail(); + list.add({ x: 3 }).tail(); + + // {"x":1} <-> {"x":2} <-> {"x":3} + + list.add({ x: 0 }).after({ x: 1 }, (v1: X, v2: X) => v1.x === v2.x); + + // {"x":1} <-> {"x":0} <-> {"x":2} <-> {"x":3} + + expect(list.length).toBe(4); + expect(list.head.value.x).toBe(1); + expect(list.head.next.value.x).toBe(0); + expect(list.head.next.next.value.x).toBe(2); + expect(list.head.next.next.next.value.x).toBe(3); + expect(list.tail.value.x).toBe(3); + expect(list.tail.previous.value.x).toBe(2); + expect(list.tail.previous.previous.value.x).toBe(0); + expect(list.tail.previous.previous.previous.value.x).toBe(1); + }); + }); + + describe('#before', () => { + it('should place a node before node with given value', () => { + list.add('a').tail(); + list.add('b').tail(); + list.add('c').tail(); + + // "a" <-> "b" <-> "c" + + list.add('x').before('b'); + + // "a" <-> "x" <-> "b" <-> "c" + + expect(list.length).toBe(4); + expect(list.head.value).toBe('a'); + expect(list.head.next.value).toBe('x'); + expect(list.head.next.next.value).toBe('b'); + expect(list.head.next.next.next.value).toBe('c'); + expect(list.tail.value).toBe('c'); + expect(list.tail.previous.value).toBe('b'); + expect(list.tail.previous.previous.value).toBe('x'); + expect(list.tail.previous.previous.previous.value).toBe('a'); + }); + + it('should be able to receive a custom compareFn', () => { + list.add({ x: 1 }).tail(); + list.add({ x: 2 }).tail(); + list.add({ x: 3 }).tail(); + + // {"x":1} <-> {"x":2} <-> {"x":3} + + list.add({ x: 0 }).before({ x: 2 }, (v1: X, v2: X) => v1.x === v2.x); + + // {"x":1} <-> {"x":0} <-> {"x":2} <-> {"x":3} + + expect(list.length).toBe(4); + expect(list.head.value.x).toBe(1); + expect(list.head.next.value.x).toBe(0); + expect(list.head.next.next.value.x).toBe(2); + expect(list.head.next.next.next.value.x).toBe(3); + expect(list.tail.value.x).toBe(3); + expect(list.tail.previous.value.x).toBe(2); + expect(list.tail.previous.previous.value.x).toBe(0); + expect(list.tail.previous.previous.previous.value.x).toBe(1); + }); + }); + + describe('#byIndex', () => { + it('should place a node at given index', () => { + list.add('a').tail(); + list.add('b').tail(); + list.add('c').tail(); + + // "a" <-> "b" <-> "c" + + list.add('x').byIndex(1); + + // "a" <-> "x" <-> "b" <-> "c" + + list.add('y').byIndex(3); + + // "a" <-> "x" <-> "b" <-> "y" <-> "c" + + expect(list.length).toBe(5); + expect(list.head.value).toBe('a'); + expect(list.head.next.value).toBe('x'); + expect(list.head.next.next.value).toBe('b'); + expect(list.head.next.next.next.value).toBe('y'); + expect(list.head.next.next.next.next.value).toBe('c'); + expect(list.tail.value).toBe('c'); + expect(list.tail.previous.value).toBe('y'); + expect(list.tail.previous.previous.value).toBe('b'); + expect(list.tail.previous.previous.previous.value).toBe('x'); + expect(list.tail.previous.previous.previous.previous.value).toBe('a'); + }); + }); + }); + + describe('#find', () => { + it('should return the first node found based on given predicate', () => { + list.add('a').tail(); + list.add('x').tail(); + list.add('b').tail(); + list.add('x').tail(); + list.add('c').tail(); + + // "a" <-> "x" <-> "b" <-> "x" <-> "c" + + const node1 = list.find(value => value === 'x'); + + expect(node1.value).toBe('x'); + expect(node1.previous.value).toBe('a'); + expect(node1.next.value).toBe('b'); + + // "a" <-> "x" <-> "b" <-> "x" <-> "c" + + const node2 = list.find((_, index) => index === 3); + + expect(node2.value).toBe('x'); + expect(node2.previous.value).toBe('b'); + expect(node2.next.value).toBe('c'); + }); + + it('should return undefined when list is empty', () => { + const node = list.find(value => value === 'x'); + + expect(node).toBeUndefined(); + }); + + it('should return undefined when predicate finds no match', () => { + list.add('a').tail(); + list.add('b').tail(); + list.add('c').tail(); + + // "a" <-> "b" <-> "c" + + const node = list.find(value => value === 'x'); + + expect(node).toBeUndefined(); + expect(list.length).toBe(3); + expect(list.head.value).toBe('a'); + expect(list.head.next.value).toBe('b'); + expect(list.head.next.next.value).toBe('c'); + expect(list.tail.value).toBe('c'); + expect(list.tail.previous.value).toBe('b'); + expect(list.tail.previous.previous.value).toBe('a'); + }); + }); + + describe('#findIndex', () => { + it('should return the index of the first node found based on given predicate', () => { + list.add('a').tail(); + list.add('x').tail(); + list.add('b').tail(); + list.add('x').tail(); + list.add('c').tail(); + + // "a" <-> "x" <-> "b" <-> "x" <-> "c" + + const index1 = list.findIndex(value => value === 'x'); + + expect(index1).toBe(1); + + // "a" <-> "x" <-> "b" <-> "x" <-> "c" + + let timesFound = 0; + const index2 = list.findIndex(value => { + if (timesFound > 1) return false; + + timesFound += Number(value === 'x'); + + return timesFound > 1; + }); + + expect(index2).toBe(3); + }); + + it('should return -1 when list is empty', () => { + const index = list.findIndex(value => value === 'x'); + + expect(index).toBe(-1); + }); + + it('should return -1 when no match is found', () => { + list.add('a').tail(); + list.add('b').tail(); + list.add('c').tail(); + + // "a" <-> "b" <-> "c" + + const index = list.findIndex(value => value === 'x'); + + expect(index).toBe(-1); + expect(list.length).toBe(3); + expect(list.head.value).toBe('a'); + expect(list.head.next.value).toBe('b'); + expect(list.head.next.next.value).toBe('c'); + expect(list.tail.value).toBe('c'); + expect(list.tail.previous.value).toBe('b'); + expect(list.tail.previous.previous.value).toBe('a'); + }); + }); + + describe('#forEach', () => { + it('should call given function for each node of the list', () => { + list.add('a').tail(); + list.add('b').tail(); + list.add('c').tail(); + + // "a" <-> "b" <-> "c" + + const spy = jest.fn(); + list.forEach(spy); + + expect(spy.mock.calls).toEqual([ + ['a', 0, list], + ['b', 1, list], + ['c', 2, list], + ]); + }); + + it('should not call given function when list is empty', () => { + const spy = jest.fn(); + list.forEach(spy); + + expect(spy).not.toHaveBeenCalled(); + }); + }); + + describe('#drop', () => { + describe('#head', () => { + it('should return undefined when there is no head', () => { + expect(list.drop().head()).toBeUndefined(); + }); + + it('should remove the node from the head of the list', () => { + list.add('a').tail(); + list.add('b').tail(); + list.add('c').tail(); + + // "a" <-> "b" <-> "c" + + list.drop().head(); + + // "b" <-> "c" + + expect(list.length).toBe(2); + expect(list.head.value).toBe('b'); + expect(list.head.next.value).toBe('c'); + expect(list.head.previous).toBeUndefined(); + expect(list.tail.value).toBe('c'); + expect(list.tail.previous.value).toBe('b'); + expect(list.tail.next).toBeUndefined(); + }); + }); + + describe('#head', () => { + it('should return undefined when there is no tail', () => { + expect(list.drop().tail()).toBeUndefined(); + }); + + it('should remove the node from the tail of the list', () => { + list.add('a').tail(); + list.add('b').tail(); + list.add('c').tail(); + + // "a" <-> "b" <-> "c" + + list.drop().tail(); + + // "a" <-> "b" + + expect(list.length).toBe(2); + expect(list.head.value).toBe('a'); + expect(list.head.next.value).toBe('b'); + expect(list.head.previous).toBeUndefined(); + expect(list.tail.value).toBe('b'); + expect(list.tail.previous.value).toBe('a'); + expect(list.tail.next).toBeUndefined(); + }); + }); + + describe('#byIndex', () => { + it('should remove the node at given index', () => { + list.add('a').tail(); + list.add('b').tail(); + list.add('c').tail(); + list.add('d').tail(); + list.add('e').tail(); + + // "a" <-> "b" <-> "c" <-> "d" <-> "e" + + list.drop().byIndex(1); + + // "a" <-> "c" <-> "d" <-> "e" + + list.drop().byIndex(2); + + // "a" <-> "c" <-> "e" + + expect(list.length).toBe(3); + expect(list.head.value).toBe('a'); + expect(list.head.next.value).toBe('c'); + expect(list.head.next.next.value).toBe('e'); + expect(list.tail.value).toBe('e'); + expect(list.tail.previous.value).toBe('c'); + expect(list.tail.previous.previous.value).toBe('a'); + }); + + it('should return undefined when list is empty', () => { + const node = list.drop().byIndex(0); + expect(node).toBeUndefined(); + }); + + it('should return undefined when given index does not exist', () => { + list.add('a').tail(); + list.add('b').tail(); + list.add('c').tail(); + + // "a" <-> "b" <-> "c" + + const node1 = list.drop().byIndex(4); + + // "a" <-> "b" <-> "c" + + expect(node1).toBeUndefined(); + expect(list.length).toBe(3); + expect(list.head.value).toBe('a'); + expect(list.head.next.value).toBe('b'); + expect(list.head.next.next.value).toBe('c'); + expect(list.tail.value).toBe('c'); + expect(list.tail.previous.value).toBe('b'); + expect(list.tail.previous.previous.value).toBe('a'); + + // "a" <-> "b" <-> "c" + + const node2 = list.drop().byIndex(-1); + + // "a" <-> "b" <-> "c" + + expect(node2).toBeUndefined(); + expect(list.length).toBe(3); + expect(list.head.value).toBe('a'); + expect(list.head.next.value).toBe('b'); + expect(list.head.next.next.value).toBe('c'); + expect(list.tail.value).toBe('c'); + expect(list.tail.previous.value).toBe('b'); + expect(list.tail.previous.previous.value).toBe('a'); + }); + }); + + describe('#byValue', () => { + it('should remove the first node with given value', () => { + list.add('a').tail(); + list.add('x').tail(); + list.add('b').tail(); + list.add('x').tail(); + list.add('c').tail(); + + // "a" <-> "x" <-> "b" <-> "x" <-> "c" + + list.drop().byValue('x'); + + // "a" <-> "b" <-> "x" <-> "c" + + expect(list.length).toBe(4); + expect(list.head.value).toBe('a'); + expect(list.head.next.value).toBe('b'); + expect(list.head.next.next.value).toBe('x'); + expect(list.head.next.next.next.value).toBe('c'); + expect(list.tail.value).toBe('c'); + expect(list.tail.previous.value).toBe('x'); + expect(list.tail.previous.previous.value).toBe('b'); + expect(list.tail.previous.previous.previous.value).toBe('a'); + + // "a" <-> "b" <-> "x" <-> "c" + + list.drop().byValue('x'); + + // "a" <-> "b" <-> "c" + + expect(list.length).toBe(3); + expect(list.head.value).toBe('a'); + expect(list.head.next.value).toBe('b'); + expect(list.head.next.next.value).toBe('c'); + expect(list.tail.value).toBe('c'); + expect(list.tail.previous.value).toBe('b'); + expect(list.tail.previous.previous.value).toBe('a'); + }); + + it('should be able to receive a custom compareFn', () => { + list.add({ x: 1 }).tail(); + list.add({ x: 2 }).tail(); + list.add({ x: 3 }).tail(); + + // {"x":1} <-> {"x":2} <-> {"x":3} + + list.drop().byValue({ x: 2 }, (v1: X, v2: X) => v1.x === v2.x); + + // {"x":1} <-> {"x":3} + + expect(list.length).toBe(2); + expect(list.head.value.x).toBe(1); + expect(list.head.next.value.x).toBe(3); + expect(list.tail.value.x).toBe(3); + expect(list.tail.previous.value.x).toBe(1); + }); + + it('should return undefined when list is empty', () => { + const node = list.drop().byValue('x'); + expect(node).toBeUndefined(); + }); + + it('should return undefined when given value is not found', () => { + list.add('a').tail(); + list.add('b').tail(); + list.add('c').tail(); + + // "a" <-> "b" <-> "c" + + const node = list.drop().byValue('x'); + + // "a" <-> "b" <-> "c" + + expect(node).toBeUndefined(); + expect(list.length).toBe(3); + expect(list.head.value).toBe('a'); + expect(list.head.next.value).toBe('b'); + expect(list.head.next.next.value).toBe('c'); + expect(list.tail.value).toBe('c'); + expect(list.tail.previous.value).toBe('b'); + expect(list.tail.previous.previous.value).toBe('a'); + }); + }); + }); + + describe('#get', () => { + it('should return node at given index', () => { + list.add('a').tail(); + list.add('b').tail(); + list.add('c').tail(); + + // "a" <-> "b" <-> "c" + + const node = list.get(1); + + expect(node.value).toBe('b'); + expect(node.previous.value).toBe('a'); + expect(node.next.value).toBe('c'); + }); + + it('should return undefined when list is empty', () => { + const node = list.get(1); + + expect(node).toBeUndefined(); + }); + + it('should return undefined when predicate finds no match', () => { + list.add('a').tail(); + list.add('b').tail(); + list.add('c').tail(); + + // "a" <-> "b" <-> "c" + + const node1 = list.get(4); + + expect(node1).toBeUndefined(); + expect(list.length).toBe(3); + expect(list.head.value).toBe('a'); + expect(list.head.next.value).toBe('b'); + expect(list.head.next.next.value).toBe('c'); + expect(list.tail.value).toBe('c'); + expect(list.tail.previous.value).toBe('b'); + expect(list.tail.previous.previous.value).toBe('a'); + + // "a" <-> "b" <-> "c" + + const node2 = list.get(-1); + + expect(node2).toBeUndefined(); + expect(list.length).toBe(3); + expect(list.head.value).toBe('a'); + expect(list.head.next.value).toBe('b'); + expect(list.head.next.next.value).toBe('c'); + expect(list.tail.value).toBe('c'); + expect(list.tail.previous.value).toBe('b'); + expect(list.tail.previous.previous.value).toBe('a'); + }); + }); + + describe('#indexOf', () => { + it('should return the index of the first node found based on given value', () => { + list.add('a').tail(); + list.add('x').tail(); + list.add('b').tail(); + list.add('x').tail(); + list.add('c').tail(); + + // "a" <-> "x" <-> "b" <-> "x" <-> "c" + + const index1 = list.indexOf('x'); + + expect(index1).toBe(1); + + // "a" <-> "x" <-> "b" <-> "x" <-> "c" + + let timesFound = 0; + const index2 = list.indexOf('x', (v1: string, v2: string) => { + if (timesFound > 1) return false; + + timesFound += Number(v1 === v2); + + return timesFound > 1; + }); + + expect(index2).toBe(3); + }); + + it('should be able to receive a custom compareFn', () => { + list.add({ x: 1 }).tail(); + list.add({ x: 2 }).tail(); + list.add({ x: 3 }).tail(); + + // {"x":1} <-> {"x":2} <-> {"x":3} + + const index = list.indexOf({ x: 2 }, (v1: X, v2: X) => v1.x === v2.x); + + expect(index).toBe(1); + }); + + it('should return -1 when list is empty', () => { + const index = list.indexOf('x'); + + expect(index).toBe(-1); + }); + + it('should return -1 when no match is found', () => { + list.add('a').tail(); + list.add('b').tail(); + list.add('c').tail(); + + // "a" <-> "b" <-> "c" + + const index = list.indexOf('x'); + + expect(index).toBe(-1); + expect(list.length).toBe(3); + expect(list.head.value).toBe('a'); + expect(list.head.next.value).toBe('b'); + expect(list.head.next.next.value).toBe('c'); + expect(list.tail.value).toBe('c'); + expect(list.tail.previous.value).toBe('b'); + expect(list.tail.previous.previous.value).toBe('a'); + }); + }); + + describe('#toArray', () => { + it('should return array representation', () => { + list.addTail('a'); + list.addTail(2); + list.addTail('c'); + list.addTail({ k: 4, v: 'd' }); + + // "a" <-> 2 <-> "c" <-> {"k":4,"v":"d"} + + const arr = list.toArray(); + expect(arr).toEqual(['a', 2, 'c', { k: 4, v: 'd' }]); + }); + + it('should return empty array when list is empty', () => { + const arr = list.toArray(); + expect(arr).toEqual([]); + }); + }); + + describe('#toString', () => { + it('should return string representation', () => { + list.addTail('a'); + list.addTail(2); + list.addTail('c'); + list.addTail({ k: 4, v: 'd' }); + + // "a" <-> 2 <-> "c" <-> {"k":4,"v":"d"} + + const str = list.toString(); + expect(str).toBe('"a" <-> 2 <-> "c" <-> {"k":4,"v":"d"}'); + }); + + it('should return empty string when list is empty', () => { + const str = list.toString(); + expect(str).toBe(''); + }); + }); + + it('should be iterable', () => { + list.addTail('a'); + list.addTail('b'); + list.addTail('c'); + + // "a" <-> "b" <-> "c" + + const arr = []; + + for (let value of list) { + arr.push(value); + } + + expect(arr).toEqual(['a', 'b', 'c']); + }); +}); + +interface X { + [k: string]: any; +} diff --git a/npm/ng-packs/packages/core/src/lib/utils/index.ts b/npm/ng-packs/packages/core/src/lib/utils/index.ts index 0043152ada..a112d9acd8 100644 --- a/npm/ng-packs/packages/core/src/lib/utils/index.ts +++ b/npm/ng-packs/packages/core/src/lib/utils/index.ts @@ -1,5 +1,6 @@ export * from './common-utils'; export * from './generator-utils'; export * from './initial-utils'; +export * from './linked-list'; export * from './route-utils'; export * from './rxjs-utils'; diff --git a/npm/ng-packs/packages/core/src/lib/utils/linked-list.ts b/npm/ng-packs/packages/core/src/lib/utils/linked-list.ts new file mode 100644 index 0000000000..f5a027e882 --- /dev/null +++ b/npm/ng-packs/packages/core/src/lib/utils/linked-list.ts @@ -0,0 +1,240 @@ +import compare from 'just-compare'; + +export class ListNode { + readonly value: T; + next: ListNode | undefined; + previous: ListNode | undefined; + + constructor(value: T) { + this.value = value; + } +} + +export class LinkedList { + private first: ListNode | undefined; + private last: ListNode | undefined; + private size = 0; + + get head(): ListNode | undefined { + return this.first; + } + get tail(): ListNode | undefined { + return this.last; + } + get length(): number { + return this.size; + } + + private linkWith(value: T, previousNode: ListNode, nextNode: ListNode): ListNode { + const node = new ListNode(value); + + if (!previousNode) return this.addHead(value); + if (!nextNode) return this.addTail(value); + + node.previous = previousNode; + previousNode.next = node; + node.next = nextNode; + nextNode.previous = node; + + this.size += 1; + + return node; + } + + add(value: T) { + return { + after: (previousValue: T, compareFn = compare) => { + return this.addAfter(value, previousValue, compareFn); + }, + before: (nextValue: T, compareFn = compare) => { + return this.addBefore(value, nextValue, compareFn); + }, + byIndex: (position: number): ListNode => { + return this.addByIndex(value, position); + }, + head: (): ListNode => { + return this.addHead(value); + }, + tail: (): ListNode => { + return this.addTail(value); + }, + }; + } + + addAfter(value: T, previousValue: T, compareFn = compare): ListNode { + const previous = this.find(currentValue => compareFn(currentValue, previousValue)); + + return previous ? this.linkWith(value, previous, previous.next) : this.addTail(value); + } + + addBefore(value: T, nextValue: T, compareFn = compare): ListNode { + const next = this.find(currentValue => compareFn(currentValue, nextValue)); + + return next ? this.linkWith(value, next.previous, next) : this.addHead(value); + } + + addByIndex(value: T, position: number): ListNode { + if (position <= 0) return this.addHead(value); + if (position >= this.size) return this.addTail(value); + + const next = this.get(position)!; + + return this.linkWith(value, next.previous, next); + } + + addHead(value: T): ListNode { + const node = new ListNode(value); + + node.next = this.first; + + if (this.first) this.first.previous = node; + else this.last = node; + + this.first = node; + this.size += 1; + + return node; + } + + addTail(value: T): ListNode { + const node = new ListNode(value); + + if (this.first) { + node.previous = this.last; + this.last!.next = node; + this.last = node; + } else { + this.first = node; + this.last = node; + } + + this.size += 1; + + return node; + } + + drop() { + return { + byIndex: (position: number) => this.dropByIndex(position), + byValue: (value: T, compareFn = compare) => this.dropByValue(value, compareFn), + head: () => this.dropHead(), + tail: () => this.dropTail(), + }; + } + + dropByIndex(position: number): ListNode | undefined { + if (position === 0) return this.dropHead(); + else if (position === this.size - 1) return this.dropTail(); + + const current = this.get(position); + + if (current) { + current.previous!.next = current.next; + current.next!.previous = current.previous; + + this.size -= 1; + + return current; + } + + return undefined; + } + + dropByValue(value: T, compareFn = compare): ListNode | undefined { + const position = this.findIndex(currentValue => compareFn(currentValue, value)); + + if (position < 0) return undefined; + + return this.dropByIndex(position); + } + + dropHead(): ListNode | undefined { + const head = this.first; + + if (head) { + this.first = head.next; + + if (this.first) this.first.previous = undefined; + else this.last = undefined; + + this.size -= 1; + + return head; + } + + return undefined; + } + + dropTail(): ListNode | undefined { + const tail = this.last; + + if (tail) { + this.last = tail.previous; + + if (this.last) this.last.next = undefined; + else this.first = undefined; + + this.size -= 1; + + return tail; + } + + return undefined; + } + + find(predicate: ListIteratorFunction): ListNode | undefined { + for (let current = this.first, position = 0; current; position += 1, current = current.next) { + if (predicate(current.value, position, this)) return current; + } + + return undefined; + } + + findIndex(predicate: ListIteratorFunction): number { + for (let current = this.first, position = 0; current; position += 1, current = current.next) { + if (predicate(current.value, position, this)) return position; + } + + return -1; + } + + forEach(callback: ListIteratorFunction) { + for (let node = this.first, position = 0; node; position += 1, node = node.next) { + callback(node.value, position, this); + } + } + + get(position: number): ListNode | undefined { + return this.find((_, index) => position === index); + } + + indexOf(value: T, compareFn = compare): number { + return this.findIndex(currentValue => compareFn(currentValue, value)); + } + + toArray(): T[] { + const array = new Array(this.size); + + this.forEach((value, index) => (array[index!] = value)); + + return array; + } + + toString(): string { + return this.toArray() + .map(value => JSON.stringify(value)) + .join(' <-> '); + } + + *[Symbol.iterator]() { + for (let node = this.first, position = 0; node; position += 1, node = node.next) { + yield node.value; + } + } +} + +export type ListIteratorFunction = ( + value: T, + index?: number, + list?: LinkedList, +) => R; From fc3ae6a19090eab3baf73d56e40c28617cad6057 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Halil=20=C4=B0brahim=20Kalkan?= Date: Wed, 25 Mar 2020 18:18:49 +0300 Subject: [PATCH 18/28] Update startup template to use the new entity extension system to customize the IdentityUser entity --- .../Users/AppUser.cs | 12 ++++++- .../MyProjectNameMigrationsDbContext.cs | 7 ---- ...MyProjectNameMigrationsDbContextFactory.cs | 2 ++ .../MyProjectNameDbContext.cs | 9 +++-- ...ectNameDbContextModelCreatingExtensions.cs | 8 ----- .../MyProjectNameEntityExtensions.cs | 33 +++++++++++++++++++ .../MyProjectNameEntityFrameworkCoreModule.cs | 5 +++ 7 files changed, 57 insertions(+), 19 deletions(-) create mode 100644 templates/app/aspnet-core/src/MyCompanyName.MyProjectName.EntityFrameworkCore/EntityFrameworkCore/MyProjectNameEntityExtensions.cs diff --git a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Domain/Users/AppUser.cs b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Domain/Users/AppUser.cs index 08e794b6e8..6fafd6b76d 100644 --- a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Domain/Users/AppUser.cs +++ b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Domain/Users/AppUser.cs @@ -42,7 +42,17 @@ namespace MyCompanyName.MyProjectName.Users /* Add your own properties here. Example: * - * public virtual string MyProperty { get; set; } + * public string MyProperty { get; set; } + * + * If you add a property and using the EF Core, remember these; + * + * 1. update MyProjectNameDbContext.OnModelCreating + * to configure the mapping for your new property + * 2. Update MyProjectNameEntityExtensions to extend the IdentityUser entity + * and add your new property to the migration. + * 3. Use the Add-Migration to add a new database migration. + * 4. Run the .DbMigrator project (or use the Update-Database command) to apply + * schema change to the database. */ private AppUser() diff --git a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.EntityFrameworkCore.DbMigrations/EntityFrameworkCore/MyProjectNameMigrationsDbContext.cs b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.EntityFrameworkCore.DbMigrations/EntityFrameworkCore/MyProjectNameMigrationsDbContext.cs index 800cfaf4b7..13e6fdac87 100644 --- a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.EntityFrameworkCore.DbMigrations/EntityFrameworkCore/MyProjectNameMigrationsDbContext.cs +++ b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.EntityFrameworkCore.DbMigrations/EntityFrameworkCore/MyProjectNameMigrationsDbContext.cs @@ -40,13 +40,6 @@ namespace MyCompanyName.MyProjectName.EntityFrameworkCore builder.ConfigureFeatureManagement(); builder.ConfigureTenantManagement(); - /* Configure customizations for entities from the modules included */ - - builder.Entity(b => - { - b.ConfigureCustomUserProperties(); - }); - /* Configure your own tables/entities inside the ConfigureMyProjectName method */ builder.ConfigureMyProjectName(); diff --git a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.EntityFrameworkCore.DbMigrations/EntityFrameworkCore/MyProjectNameMigrationsDbContextFactory.cs b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.EntityFrameworkCore.DbMigrations/EntityFrameworkCore/MyProjectNameMigrationsDbContextFactory.cs index b399f99cda..7af1004924 100644 --- a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.EntityFrameworkCore.DbMigrations/EntityFrameworkCore/MyProjectNameMigrationsDbContextFactory.cs +++ b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.EntityFrameworkCore.DbMigrations/EntityFrameworkCore/MyProjectNameMigrationsDbContextFactory.cs @@ -11,6 +11,8 @@ namespace MyCompanyName.MyProjectName.EntityFrameworkCore { public MyProjectNameMigrationsDbContext CreateDbContext(string[] args) { + MyProjectNameEntityExtensions.Configure(); + var configuration = BuildConfiguration(); var builder = new DbContextOptionsBuilder() diff --git a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.EntityFrameworkCore/EntityFrameworkCore/MyProjectNameDbContext.cs b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.EntityFrameworkCore/EntityFrameworkCore/MyProjectNameDbContext.cs index a50ee1969c..99c2585c75 100644 --- a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.EntityFrameworkCore/EntityFrameworkCore/MyProjectNameDbContext.cs +++ b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.EntityFrameworkCore/EntityFrameworkCore/MyProjectNameDbContext.cs @@ -3,6 +3,7 @@ using MyCompanyName.MyProjectName.Users; using Volo.Abp.Data; using Volo.Abp.EntityFrameworkCore; using Volo.Abp.EntityFrameworkCore.Modeling; +using Volo.Abp.Identity; using Volo.Abp.Users.EntityFrameworkCore; namespace MyCompanyName.MyProjectName.EntityFrameworkCore @@ -39,12 +40,14 @@ namespace MyCompanyName.MyProjectName.EntityFrameworkCore builder.Entity(b => { - b.ToTable("AbpUsers"); //Sharing the same table "AbpUsers" with the IdentityUser + b.ToTable(AbpIdentityDbProperties.DbTablePrefix + "Users"); //Sharing the same table "AbpUsers" with the IdentityUser + b.ConfigureByConvention(); b.ConfigureAbpUser(); - //Moved customization to a method so we can share it with the MyProjectNameMigrationsDbContext class - b.ConfigureCustomUserProperties(); + /* Configure mappings for your additional properties + * Also see the MyProjectNameEntityExtensions class + */ }); /* Configure your own tables/entities inside the ConfigureMyProjectName method */ diff --git a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.EntityFrameworkCore/EntityFrameworkCore/MyProjectNameDbContextModelCreatingExtensions.cs b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.EntityFrameworkCore/EntityFrameworkCore/MyProjectNameDbContextModelCreatingExtensions.cs index 3528ed5279..cc85590e61 100644 --- a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.EntityFrameworkCore/EntityFrameworkCore/MyProjectNameDbContextModelCreatingExtensions.cs +++ b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.EntityFrameworkCore/EntityFrameworkCore/MyProjectNameDbContextModelCreatingExtensions.cs @@ -1,7 +1,5 @@ using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Metadata.Builders; using Volo.Abp; -using Volo.Abp.Users; namespace MyCompanyName.MyProjectName.EntityFrameworkCore { @@ -20,11 +18,5 @@ namespace MyCompanyName.MyProjectName.EntityFrameworkCore // //... //}); } - - public static void ConfigureCustomUserProperties(this EntityTypeBuilder b) - where TUser: class, IUser - { - //b.Property(nameof(AppUser.MyProperty))... - } } } \ No newline at end of file diff --git a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.EntityFrameworkCore/EntityFrameworkCore/MyProjectNameEntityExtensions.cs b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.EntityFrameworkCore/EntityFrameworkCore/MyProjectNameEntityExtensions.cs new file mode 100644 index 0000000000..2e36827403 --- /dev/null +++ b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.EntityFrameworkCore/EntityFrameworkCore/MyProjectNameEntityExtensions.cs @@ -0,0 +1,33 @@ +using Volo.Abp.EntityFrameworkCore.Extensions; +using Volo.Abp.Identity; +using Volo.Abp.Threading; + +namespace MyCompanyName.MyProjectName.EntityFrameworkCore +{ + public static class MyProjectNameEntityExtensions + { + private static readonly OneTimeRunner OneTimeRunner = new OneTimeRunner(); + + public static void Configure() + { + OneTimeRunner.Run(() => + { + /* You can configure entity extension properties for the + * entities defined in the used modules. + * + * Example: + * + * EntityExtensionManager.AddProperty( + * "MyProperty", + * b => + * { + * b.HasMaxLength(128); + * }); + * + * See the documentation for more: + * https://docs.abp.io/en/abp/latest/Customizing-Application-Modules-Extending-Entities + */ + }); + } + } +} diff --git a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.EntityFrameworkCore/EntityFrameworkCore/MyProjectNameEntityFrameworkCoreModule.cs b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.EntityFrameworkCore/EntityFrameworkCore/MyProjectNameEntityFrameworkCoreModule.cs index 9d2f167136..4c231d0b98 100644 --- a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.EntityFrameworkCore/EntityFrameworkCore/MyProjectNameEntityFrameworkCoreModule.cs +++ b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.EntityFrameworkCore/EntityFrameworkCore/MyProjectNameEntityFrameworkCoreModule.cs @@ -27,6 +27,11 @@ namespace MyCompanyName.MyProjectName.EntityFrameworkCore )] public class MyProjectNameEntityFrameworkCoreModule : AbpModule { + public override void PreConfigureServices(ServiceConfigurationContext context) + { + MyProjectNameEntityExtensions.Configure(); + } + public override void ConfigureServices(ServiceConfigurationContext context) { context.Services.AddAbpDbContext(options => From 30cd18abfa52c9caee23fdd65b04acc14cb48402 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Halil=20=C4=B0brahim=20Kalkan?= Date: Wed, 25 Mar 2020 18:22:42 +0300 Subject: [PATCH 19/28] Update MyProjectNameDbContextModelCreatingExtensions.cs --- .../MyProjectNameDbContextModelCreatingExtensions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/module/aspnet-core/src/MyCompanyName.MyProjectName.EntityFrameworkCore/EntityFrameworkCore/MyProjectNameDbContextModelCreatingExtensions.cs b/templates/module/aspnet-core/src/MyCompanyName.MyProjectName.EntityFrameworkCore/EntityFrameworkCore/MyProjectNameDbContextModelCreatingExtensions.cs index c0f2a85865..0e1e01628d 100644 --- a/templates/module/aspnet-core/src/MyCompanyName.MyProjectName.EntityFrameworkCore/EntityFrameworkCore/MyProjectNameDbContextModelCreatingExtensions.cs +++ b/templates/module/aspnet-core/src/MyCompanyName.MyProjectName.EntityFrameworkCore/EntityFrameworkCore/MyProjectNameDbContextModelCreatingExtensions.cs @@ -26,7 +26,7 @@ namespace MyCompanyName.MyProjectName.EntityFrameworkCore //Configure table & schema name b.ToTable(options.TablePrefix + "Questions", options.Schema); - b.ConfigureFullAuditedAggregateRoot(); + b.ConfigureByConvention(); //Properties b.Property(q => q.Title).IsRequired().HasMaxLength(QuestionConsts.MaxTitleLength); From d56f2c7f9453c54069d63b601fd31f73bb97b5ed Mon Sep 17 00:00:00 2001 From: Arman Ozak Date: Wed, 25 Mar 2020 19:01:09 +0300 Subject: [PATCH 20/28] feat(core): add dropByValueAll to LinkedList --- .../core/src/lib/tests/linked-list.spec.ts | 89 +++++++++++++++++++ .../core/src/lib/utils/linked-list.ts | 13 +++ 2 files changed, 102 insertions(+) diff --git a/npm/ng-packs/packages/core/src/lib/tests/linked-list.spec.ts b/npm/ng-packs/packages/core/src/lib/tests/linked-list.spec.ts index cd0a00096e..77b748cf0d 100644 --- a/npm/ng-packs/packages/core/src/lib/tests/linked-list.spec.ts +++ b/npm/ng-packs/packages/core/src/lib/tests/linked-list.spec.ts @@ -538,6 +538,95 @@ describe('Linked List (Doubly)', () => { expect(list.tail.previous.previous.value).toBe('a'); }); }); + + describe('#byValueAll', () => { + it('should remove all nodes with given value', () => { + list.add('a').tail(); + list.add('x').tail(); + list.add('b').tail(); + list.add('x').tail(); + list.add('c').tail(); + + // "a" <-> "x" <-> "b" <-> "x" <-> "c" + + const dropped = list.drop().byValueAll('x'); + + // "a" <-> "b" <-> "c" + + expect(dropped.length).toBe(2); + expect(dropped[0].value).toEqual('x'); + expect(dropped[0].previous.value).toEqual('a'); + expect(dropped[0].next.value).toEqual('b'); + expect(dropped[1].value).toEqual('x'); + expect(dropped[1].previous.value).toEqual('b'); + expect(dropped[1].next.value).toEqual('c'); + + expect(list.length).toBe(3); + expect(list.head.value).toBe('a'); + expect(list.head.next.value).toBe('b'); + expect(list.head.next.next.value).toBe('c'); + expect(list.tail.value).toBe('c'); + expect(list.tail.previous.value).toBe('b'); + expect(list.tail.previous.previous.value).toBe('a'); + }); + + it('should be able to receive a custom compareFn', () => { + list.add({ x: 1 }).tail(); + list.add({ x: 0 }).tail(); + list.add({ x: 2 }).tail(); + list.add({ x: 0 }).tail(); + list.add({ x: 3 }).tail(); + + // {"x":1} <-> {"x":0} <-> {"x":2} <-> {"x":0} <-> {"x":3} + + const dropped = list.drop().byValueAll({ x: 0 }, (v1: X, v2: X) => v1.x === v2.x); + + // {"x":1} <-> {"x":2} <-> {"x":3} + + expect(dropped.length).toBe(2); + expect(dropped[0].value.x).toEqual(0); + expect(dropped[0].previous.value.x).toEqual(1); + expect(dropped[0].next.value.x).toEqual(2); + expect(dropped[1].value.x).toEqual(0); + expect(dropped[1].previous.value.x).toEqual(2); + expect(dropped[1].next.value.x).toEqual(3); + + expect(list.length).toBe(3); + expect(list.head.value.x).toBe(1); + expect(list.head.next.value.x).toBe(2); + expect(list.head.next.next.value.x).toBe(3); + expect(list.tail.value.x).toBe(3); + expect(list.tail.previous.value.x).toBe(2); + expect(list.tail.previous.previous.value.x).toBe(1); + }); + + it('should return empty array when list is empty', () => { + const dropped = list.drop().byValueAll('x'); + expect(dropped).toEqual([]); + }); + + it('should return empty array when given value is not found', () => { + list.add('a').tail(); + list.add('b').tail(); + list.add('c').tail(); + + // "a" <-> "b" <-> "c" + + const dropped = list.drop().byValueAll('x'); + + // "a" <-> "b" <-> "c" + + expect(dropped).toEqual([]); + + expect(list.length).toBe(3); + expect(list.head.value).toBe('a'); + expect(list.head.next.value).toBe('b'); + expect(list.head.next.next.value).toBe('c'); + expect(list.tail.value).toBe('c'); + expect(list.tail.previous.value).toBe('b'); + expect(list.tail.previous.previous.value).toBe('a'); + }); + }); }); describe('#get', () => { diff --git a/npm/ng-packs/packages/core/src/lib/utils/linked-list.ts b/npm/ng-packs/packages/core/src/lib/utils/linked-list.ts index f5a027e882..9c47a6ad9b 100644 --- a/npm/ng-packs/packages/core/src/lib/utils/linked-list.ts +++ b/npm/ng-packs/packages/core/src/lib/utils/linked-list.ts @@ -117,6 +117,7 @@ export class LinkedList { return { byIndex: (position: number) => this.dropByIndex(position), byValue: (value: T, compareFn = compare) => this.dropByValue(value, compareFn), + byValueAll: (value: T, compareFn = compare) => this.dropByValueAll(value, compareFn), head: () => this.dropHead(), tail: () => this.dropTail(), }; @@ -148,6 +149,18 @@ export class LinkedList { return this.dropByIndex(position); } + dropByValueAll(value: T, compareFn = compare): ListNode[] { + const dropped: ListNode[] = []; + + for (let current = this.first, position = 0; current; position += 1, current = current.next) { + if (compareFn(current.value, value)) { + dropped.push(this.dropByIndex(position - dropped.length)); + } + } + + return dropped; + } + dropHead(): ListNode | undefined { const head = this.first; From 6599c2a6762ea5ae6b27dfd6bc7db88c0ec3b593 Mon Sep 17 00:00:00 2001 From: Arman Ozak Date: Wed, 25 Mar 2020 19:08:55 +0300 Subject: [PATCH 21/28] fix(core): avoid lint errors --- .../packages/core/src/lib/tests/linked-list.spec.ts | 2 +- .../packages/core/src/lib/utils/linked-list.ts | 10 ++++++++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/npm/ng-packs/packages/core/src/lib/tests/linked-list.spec.ts b/npm/ng-packs/packages/core/src/lib/tests/linked-list.spec.ts index 77b748cf0d..d52050c0ad 100644 --- a/npm/ng-packs/packages/core/src/lib/tests/linked-list.spec.ts +++ b/npm/ng-packs/packages/core/src/lib/tests/linked-list.spec.ts @@ -796,7 +796,7 @@ describe('Linked List (Doubly)', () => { const arr = []; - for (let value of list) { + for (const value of list) { arr.push(value); } diff --git a/npm/ng-packs/packages/core/src/lib/utils/linked-list.ts b/npm/ng-packs/packages/core/src/lib/utils/linked-list.ts index 9c47a6ad9b..6d2435acdb 100644 --- a/npm/ng-packs/packages/core/src/lib/utils/linked-list.ts +++ b/npm/ng-packs/packages/core/src/lib/utils/linked-list.ts @@ -1,3 +1,5 @@ +/* tslint:disable:no-non-null-assertion */ + import compare from 'just-compare'; export class ListNode { @@ -25,7 +27,11 @@ export class LinkedList { return this.size; } - private linkWith(value: T, previousNode: ListNode, nextNode: ListNode): ListNode { + private linkWith( + value: T, + previousNode: ListNode | undefined, + nextNode: ListNode | undefined, + ): ListNode { const node = new ListNode(value); if (!previousNode) return this.addHead(value); @@ -154,7 +160,7 @@ export class LinkedList { for (let current = this.first, position = 0; current; position += 1, current = current.next) { if (compareFn(current.value, value)) { - dropped.push(this.dropByIndex(position - dropped.length)); + dropped.push(this.dropByIndex(position - dropped.length)!); } } From 026fb1defba37b72f6353994f356ea8603ddf2f5 Mon Sep 17 00:00:00 2001 From: Arman Ozak Date: Wed, 25 Mar 2020 19:48:28 +0300 Subject: [PATCH 22/28] feat(core): iterate over linked list nodes instead of node values --- .../core/src/lib/tests/linked-list.spec.ts | 39 ++++++++----------- .../core/src/lib/utils/linked-list.ts | 18 ++++----- 2 files changed, 25 insertions(+), 32 deletions(-) diff --git a/npm/ng-packs/packages/core/src/lib/tests/linked-list.spec.ts b/npm/ng-packs/packages/core/src/lib/tests/linked-list.spec.ts index d52050c0ad..518956deba 100644 --- a/npm/ng-packs/packages/core/src/lib/tests/linked-list.spec.ts +++ b/npm/ng-packs/packages/core/src/lib/tests/linked-list.spec.ts @@ -212,7 +212,7 @@ describe('Linked List (Doubly)', () => { // "a" <-> "x" <-> "b" <-> "x" <-> "c" - const node1 = list.find(value => value === 'x'); + const node1 = list.find(node => node.previous && node.previous.value === 'a'); expect(node1.value).toBe('x'); expect(node1.previous.value).toBe('a'); @@ -220,7 +220,7 @@ describe('Linked List (Doubly)', () => { // "a" <-> "x" <-> "b" <-> "x" <-> "c" - const node2 = list.find((_, index) => index === 3); + const node2 = list.find(node => node.next && node.next.value === 'c'); expect(node2.value).toBe('x'); expect(node2.previous.value).toBe('b'); @@ -228,9 +228,9 @@ describe('Linked List (Doubly)', () => { }); it('should return undefined when list is empty', () => { - const node = list.find(value => value === 'x'); + const found = list.find(node => node.value === 'x'); - expect(node).toBeUndefined(); + expect(found).toBeUndefined(); }); it('should return undefined when predicate finds no match', () => { @@ -240,9 +240,9 @@ describe('Linked List (Doubly)', () => { // "a" <-> "b" <-> "c" - const node = list.find(value => value === 'x'); + const found = list.find(node => node.value === 'x'); - expect(node).toBeUndefined(); + expect(found).toBeUndefined(); expect(list.length).toBe(3); expect(list.head.value).toBe('a'); expect(list.head.next.value).toBe('b'); @@ -263,26 +263,19 @@ describe('Linked List (Doubly)', () => { // "a" <-> "x" <-> "b" <-> "x" <-> "c" - const index1 = list.findIndex(value => value === 'x'); + const index1 = list.findIndex(node => node.previous && node.previous.value === 'a'); expect(index1).toBe(1); // "a" <-> "x" <-> "b" <-> "x" <-> "c" - let timesFound = 0; - const index2 = list.findIndex(value => { - if (timesFound > 1) return false; - - timesFound += Number(value === 'x'); - - return timesFound > 1; - }); + const index2 = list.findIndex(node => node.next && node.next.value === 'c'); expect(index2).toBe(3); }); it('should return -1 when list is empty', () => { - const index = list.findIndex(value => value === 'x'); + const index = list.findIndex(node => node.value === 'x'); expect(index).toBe(-1); }); @@ -294,7 +287,7 @@ describe('Linked List (Doubly)', () => { // "a" <-> "b" <-> "c" - const index = list.findIndex(value => value === 'x'); + const index = list.findIndex(node => node.value === 'x'); expect(index).toBe(-1); expect(list.length).toBe(3); @@ -309,9 +302,9 @@ describe('Linked List (Doubly)', () => { describe('#forEach', () => { it('should call given function for each node of the list', () => { - list.add('a').tail(); - list.add('b').tail(); - list.add('c').tail(); + const a = list.add('a').tail(); + const b = list.add('b').tail(); + const c = list.add('c').tail(); // "a" <-> "b" <-> "c" @@ -319,9 +312,9 @@ describe('Linked List (Doubly)', () => { list.forEach(spy); expect(spy.mock.calls).toEqual([ - ['a', 0, list], - ['b', 1, list], - ['c', 2, list], + [a, 0, list], + [b, 1, list], + [c, 2, list], ]); }); diff --git a/npm/ng-packs/packages/core/src/lib/utils/linked-list.ts b/npm/ng-packs/packages/core/src/lib/utils/linked-list.ts index 6d2435acdb..42cc5bc941 100644 --- a/npm/ng-packs/packages/core/src/lib/utils/linked-list.ts +++ b/npm/ng-packs/packages/core/src/lib/utils/linked-list.ts @@ -68,13 +68,13 @@ export class LinkedList { } addAfter(value: T, previousValue: T, compareFn = compare): ListNode { - const previous = this.find(currentValue => compareFn(currentValue, previousValue)); + const previous = this.find(node => compareFn(node.value, previousValue)); return previous ? this.linkWith(value, previous, previous.next) : this.addTail(value); } addBefore(value: T, nextValue: T, compareFn = compare): ListNode { - const next = this.find(currentValue => compareFn(currentValue, nextValue)); + const next = this.find(node => compareFn(node.value, nextValue)); return next ? this.linkWith(value, next.previous, next) : this.addHead(value); } @@ -148,7 +148,7 @@ export class LinkedList { } dropByValue(value: T, compareFn = compare): ListNode | undefined { - const position = this.findIndex(currentValue => compareFn(currentValue, value)); + const position = this.findIndex(node => compareFn(node.value, value)); if (position < 0) return undefined; @@ -203,7 +203,7 @@ export class LinkedList { find(predicate: ListIteratorFunction): ListNode | undefined { for (let current = this.first, position = 0; current; position += 1, current = current.next) { - if (predicate(current.value, position, this)) return current; + if (predicate(current, position, this)) return current; } return undefined; @@ -211,7 +211,7 @@ export class LinkedList { findIndex(predicate: ListIteratorFunction): number { for (let current = this.first, position = 0; current; position += 1, current = current.next) { - if (predicate(current.value, position, this)) return position; + if (predicate(current, position, this)) return position; } return -1; @@ -219,7 +219,7 @@ export class LinkedList { forEach(callback: ListIteratorFunction) { for (let node = this.first, position = 0; node; position += 1, node = node.next) { - callback(node.value, position, this); + callback(node, position, this); } } @@ -228,13 +228,13 @@ export class LinkedList { } indexOf(value: T, compareFn = compare): number { - return this.findIndex(currentValue => compareFn(currentValue, value)); + return this.findIndex(node => compareFn(node.value, value)); } toArray(): T[] { const array = new Array(this.size); - this.forEach((value, index) => (array[index!] = value)); + this.forEach((node, index) => (array[index!] = node.value)); return array; } @@ -253,7 +253,7 @@ export class LinkedList { } export type ListIteratorFunction = ( - value: T, + node: ListNode, index?: number, list?: LinkedList, ) => R; From c78f3c520b1a6b09b7972e863793c8e8d36f363a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Halil=20=C4=B0brahim=20Kalkan?= Date: Wed, 25 Mar 2020 20:10:19 +0300 Subject: [PATCH 23/28] Update Entities doc for the new entity extension system. --- docs/en/Entities.md | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/docs/en/Entities.md b/docs/en/Entities.md index f0e6c7f301..8e02b047cb 100644 --- a/docs/en/Entities.md +++ b/docs/en/Entities.md @@ -373,16 +373,19 @@ So, you can directly use the `ExtraProperties` property to use the dictionary A The way to store this dictionary in the database depends on the database provider you're using. -* For [Entity Framework Core](Entity-Framework-Core.md), it is stored in a single `ExtraProperties` field as a `JSON` string. Serializing to `JSON` and deserializing from the `JSON` are automatically done by the ABP Framework using the [value conversions](https://docs.microsoft.com/en-us/ef/core/modeling/value-conversions) system of the EF Core. +* For [Entity Framework Core](Entity-Framework-Core.md), here are two type of configurations; + * By default, it is stored in a single `ExtraProperties` field as a `JSON` string (that means all extra properties stored in a single database table field). Serializing to `JSON` and deserializing from the `JSON` are automatically done by the ABP Framework using the [value conversions](https://docs.microsoft.com/en-us/ef/core/modeling/value-conversions) system of the EF Core. + * If you want, you can use the `EntityExtensionManager` to define a separate table field for a desired extra property. Properties those are not configured through the `EntityExtensionManager` will continue to use a single `JSON` field as described above. This feature is especially useful when you are using a pre-built [application module](Modules/Index.md) and want to [extend its entities](Customizing-Application-Modules-Extending-Entities.md). * For [MongoDB](MongoDB.md), it is stored as a **regular field**, since MongoDB naturally supports this kind of [extra elements](https://mongodb.github.io/mongo-csharp-driver/1.11/serialization/#supporting-extra-elements) system. ### Discussion for the Extra Properties -Extra Properties system is especially useful if you are using a **re-usable module** that defines an entity inside and you want to get/set some data related to this entity in an easy way. You normally **don't need** to this system for your own entities, because it has the following drawbacks: +Extra Properties system is especially useful if you are using a **re-usable module** that defines an entity inside and you want to get/set some data related to this entity in an easy way. -* It is **not fully type safe**. +You normally **don't need** to this system for your own entities, because it has the following drawbacks: + +* It is **not fully type safe** since it works with strings as property names. * It is **not easy to [auto map](Object-To-Object-Mapping.md)** these properties from/to other objects. -* It **doesn't create fields** in the database table for EF Core, so it will not be easy to create indexes or search/order by this field in the database side. ### Extra Properties Behind Entities From 9a6a7d4a534440ece776fc7360006c500dea7c2f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Halil=20=C4=B0brahim=20Kalkan?= Date: Wed, 25 Mar 2020 20:36:53 +0300 Subject: [PATCH 24/28] Added Entity Extensions section to the document: Customizing-Application-Modules-Extending-Entities.md --- ...-Application-Modules-Extending-Entities.md | 32 +++++++++++++++++-- docs/en/Entities.md | 2 +- 2 files changed, 31 insertions(+), 3 deletions(-) diff --git a/docs/en/Customizing-Application-Modules-Extending-Entities.md b/docs/en/Customizing-Application-Modules-Extending-Entities.md index def121910e..878f9c671e 100644 --- a/docs/en/Customizing-Application-Modules-Extending-Entities.md +++ b/docs/en/Customizing-Application-Modules-Extending-Entities.md @@ -4,7 +4,7 @@ In some cases, you may want to add some additional properties (and database fiel ## Extra Properties -[Extra properties](Entities.md) is a way of storing some additional data on an entity without changing it. The entity should implement the `IHasExtraProperties` interface to allow it. All the aggregate root entities defined in the pre-built modules implement the `IHasExtraProperties` interface, so you can store extra properties on these entities. +[Extra properties](Entities.md) is a way of storing some additional data on an entity without changing it. The entity should implement the `IHasExtraProperties` interface to allow it. All the aggregate root entities defined in the pre-built modules implement the `IHasExtraProperties` interface, so you can store extra properties on these objects. Example: @@ -25,7 +25,35 @@ Extra properties are stored as a single `JSON` formatted string value in the dat See the [entities document](Entities.md) for more about the extra properties system. -> It is possible to perform a **business logic** based on the value of an extra property. You can **override** a service method and get or set the value as shown above. Overriding services will be discussed below. +> It is possible to perform a **business logic** based on the value of an extra property. You can [override a service method](Customizing-Application-Modules-Overriding-Services.md), then get or set the value as shown above. + +## Entity Extensions (EF Core) + +As mentioned above, all extra properties of an entity are stored as a single JSON object in the database table. This is not so natural especially when you want to; + +* Create **indexes** and **foreign keys** for an extra property. +* Write **SQL** or **LINQ** using the extra property (search table by the property value, for example). +* Creating your **own entity** maps to the same table, but defines an extra property as a **regular property** in the entity (see the [EF Core migration document](Entity-Framework-Core-Migrations.md) for more). + +To overcome the difficulties described above, ABP Framework entity extension system for the Entity Framework Core that allows you to use the same extra properties API defined above, but store a desired property as a separate field in the database table. + +Assume that you want to add a `SocialSecurityNumber` to the `IdentityUser` entity of the [Identity Module](Modules/Identity.md). You can use the `EntityExtensionManager` static class: + +````csharp +EntityExtensionManager.AddProperty( + "SocialSecurityNumber", + b => { b.HasMaxLength(32); } +); +```` + +* You provide the `IdentityUser` as the entity name, `string` as the type of the new property, `SocialSecurityNumber` as the property name (also, the field name in the database table). +* You also need to provide an action that defines the database mapping properties using the [EF Core Fluent API](https://docs.microsoft.com/en-us/ef/core/modeling/entity-properties). + +> This code part must be executed before the related `DbContext` used. The [application startup template](Startup-Templates/Application.md) defines a static class named `YourProjectNameEntityExtensions`. You can define your extensions in this class to ensure that it is executed in the proper time. Otherwise, you should handle it yourself. + +Once you define an entity extension, you then need to use the standard [Add-Migration](https://docs.microsoft.com/en-us/ef/core/miscellaneous/cli/powershell#add-migration) and [Update-Database](https://docs.microsoft.com/en-us/ef/core/miscellaneous/cli/powershell#update-database) commands of the EF Core to create a code first migration class and update your database. + +You can then use the same extra properties system defined in the previous section to manipulate the property over the entity. ## Creating a New Entity Maps to the Same Database Table/Collection diff --git a/docs/en/Entities.md b/docs/en/Entities.md index 8e02b047cb..fee1a3618c 100644 --- a/docs/en/Entities.md +++ b/docs/en/Entities.md @@ -375,7 +375,7 @@ The way to store this dictionary in the database depends on the database provide * For [Entity Framework Core](Entity-Framework-Core.md), here are two type of configurations; * By default, it is stored in a single `ExtraProperties` field as a `JSON` string (that means all extra properties stored in a single database table field). Serializing to `JSON` and deserializing from the `JSON` are automatically done by the ABP Framework using the [value conversions](https://docs.microsoft.com/en-us/ef/core/modeling/value-conversions) system of the EF Core. - * If you want, you can use the `EntityExtensionManager` to define a separate table field for a desired extra property. Properties those are not configured through the `EntityExtensionManager` will continue to use a single `JSON` field as described above. This feature is especially useful when you are using a pre-built [application module](Modules/Index.md) and want to [extend its entities](Customizing-Application-Modules-Extending-Entities.md). + * If you want, you can use the `EntityExtensionManager` to define a separate table field for a desired extra property. Properties those are not configured through the `EntityExtensionManager` will continue to use a single `JSON` field as described above. This feature is especially useful when you are using a pre-built [application module](Modules/Index.md) and want to [extend its entities](Customizing-Application-Modules-Extending-Entities.md). See the [EF Core integration document](Entity-Framework-Core.md) to learn how to use the `EntityExtensionManager`. * For [MongoDB](MongoDB.md), it is stored as a **regular field**, since MongoDB naturally supports this kind of [extra elements](https://mongodb.github.io/mongo-csharp-driver/1.11/serialization/#supporting-extra-elements) system. ### Discussion for the Extra Properties From 81a3f6b8563b365ee8644f9b3e017fd357c0840c Mon Sep 17 00:00:00 2001 From: Arman Ozak Date: Wed, 25 Mar 2020 20:59:52 +0300 Subject: [PATCH 25/28] docs(core): add how to create and use a LinkedList --- docs/en/UI/Angular/Linked-List.md | 844 ++++++++++++++++++++++++++++++ 1 file changed, 844 insertions(+) create mode 100644 docs/en/UI/Angular/Linked-List.md diff --git a/docs/en/UI/Angular/Linked-List.md b/docs/en/UI/Angular/Linked-List.md new file mode 100644 index 0000000000..c3fb307806 --- /dev/null +++ b/docs/en/UI/Angular/Linked-List.md @@ -0,0 +1,844 @@ +# Linked List (Doubly) + + + +The core module provides a useful data structure known as a [doubly linked list](https://en.wikipedia.org/wiki/Doubly_linked_list). Briefly, a doubly linked list is a series of records (a.k.a. nodes) which has information on the previous node, the next node, and its own value (or data). + + + +## Getting Started + +To create a doubly linked list, all you have to do is to import and create a new instance of it: + +```js +import { LinkedList } from '@abp/ng.core'; + +const list = new LinkedList(); +``` + + + +The constructor does not get any parameters. + + + +## Usage + +### How to Add New Nodes + +There are a few methods to create new nodes in a linked list and all of them are separately available as well as revealed from an `add` method. + + + +#### addHead(value: T): ListNode\ + +Adds a node with given value as the first node in list: + +```js +list.addHead('a'); + +// "a" + +list.addHead('b'); + +// "b" <-> "a" + +list.addHead('c'); + +// "c" <-> "b" <-> "a" +``` + + + +#### addTail(value: T): ListNode\ + +Adds a node with given value as the last node in list: + +```js +list.addTail('a'); + +// "a" + +list.addTail('b'); + +// "a" <-> "b" + +list.addTail('c'); + +// "a" <-> "b" <-> "c" +``` + + + +#### addAfter(value: T, previousValue: T, compareFn = compare): ListNode\ + +Adds a node with given value after the first node that has the previous value: + +```js +list.addTail('a'); +list.addTail('b'); +list.addTail('b'); +list.addTail('c'); + +// "a" <-> "b" <-> "b" <-> "c" + +list.addAfter('x', 'b'); + +// "a" <-> "b" <-> "x" <-> "b" <-> "c" +``` + + + +You may pass a custom compare function to detect the searched value: + +```js +list.addTail({ x: 1 }); +list.addTail({ x: 2 }); +list.addTail({ x: 3 }); + +// {"x":1} <-> {"x":2} <-> {"x":3} + +list.addAfter({ x: 0 }, { x: 2 }, (v1, v2) => v1.x === v2.x); + +// {"x":1} <-> {"x":2} <-> {"x":0} <-> {"x":3} +``` + + + +> The default compare function checks deep equality, so you will rarely need to pass that parameter. + + + +#### addBefore(value: T, nextValue: T, compareFn = compare): ListNode\ + +Adds a node with given value before the first node that has the next value: + +```js +list.addTail('a'); +list.addTail('b'); +list.addTail('b'); +list.addTail('c'); + +// "a" <-> "b" <-> "b" <-> "c" + +list.addBefore('x', 'b'); + +// "a" <-> "x" <-> "b" <-> "b" <-> "c" +``` + + + +You may pass a custom compare function to detect the searched value: + +```js +list.addTail({ x: 1 }); +list.addTail({ x: 2 }); +list.addTail({ x: 3 }); + +// {"x":1} <-> {"x":2} <-> {"x":3} + +list.addBefore({ x: 0 }, { x: 2 }, (v1, v2) => v1.x === v2.x); + +// {"x":1} <-> {"x":0} <-> {"x":2} <-> {"x":3} +``` + + + +> The default compare function checks deep equality, so you will rarely need to pass that parameter. + + + +#### addByIndex(value: T, position: number): ListNode\ + +Adds a node with given value at the specified position in the list: + +```js +list.addTail('a'); +list.addTail('b'); +list.addTail('c'); + +// "a" <-> "b" <-> "c" + +list.addByIndex('x', 2); + +// "a" <-> "b" <-> "x" <-> "c" +``` + + + +#### add(value: T).head(): ListNode\ + +Adds a node with given value as the first node in list: + +```js +list.add('a').head(); + +// "a" + +list.add('b').head(); + +// "b" <-> "a" + +list.add('c').head(); + +// "c" <-> "b" <-> "a" +``` + + + +> This is an alternative API for `addHead`. + + + +#### add(value: T).tail(): ListNode\ + +Adds a node with given value as the last node in list: + +```js +list.add('a').tail(); + +// "a" + +list.add('b').tail(); + +// "a" <-> "b" + +list.add('c').tail(); + +// "a" <-> "b" <-> "c" +``` + + + +> This is an alternative API for `addTail`. + + + +#### add(value: T).after(previousValue: T, compareFn = compare): ListNode\ + +Adds a node with given value after the first node that has the previous value: + +```js +list.add('a').tail(); +list.add('b').tail(); +list.add('b').tail(); +list.add('c').tail(); + +// "a" <-> "b" <-> "b" <-> "c" + +list.add('x').after('b'); + +// "a" <-> "b" <-> "x" <-> "b" <-> "c" +``` + + + +You may pass a custom compare function to detect the searched value: + +```js +list.add({ x: 1 }).tail(); +list.add({ x: 2 }).tail(); +list.add({ x: 3 }).tail(); + +// {"x":1} <-> {"x":2} <-> {"x":3} + +list.add({ x: 0 }).after({ x: 2 }, (v1, v2) => v1.x === v2.x); + +// {"x":1} <-> {"x":2} <-> {"x":0} <-> {"x":3} +``` + + + +> This is an alternative API for `addAfter`. +> +> The default compare function checks deep equality, so you will rarely need to pass that parameter. + + + +#### add(value: T).before(nextValue: T, compareFn = compare): ListNode\ + +Adds a node with given value before the first node that has the next value: + +```js +list.add('a').tail(); +list.add('b').tail(); +list.add('b').tail(); +list.add('c').tail(); + +// "a" <-> "b" <-> "b" <-> "c" + +list.add('x').before('b'); + +// "a" <-> "x" <-> "b" <-> "b" <-> "c" +``` + + + +You may pass a custom compare function to detect the searched value: + +```js +list.add({ x: 1 }).tail(); +list.add({ x: 2 }).tail(); +list.add({ x: 3 }).tail(); + +// {"x":1} <-> {"x":2} <-> {"x":3} + +list.add({ x: 0 }).before({ x: 2 }, (v1, v2) => v1.x === v2.x); + +// {"x":1} <-> {"x":0} <-> {"x":2} <-> {"x":3} +``` + + + +> This is an alternative API for `addBefore`. +> +> The default compare function checks deep equality, so you will rarely need to pass that parameter. + + + +#### add(value: T).byIndex(position: number): ListNode\ + +Adds a node with given value at the specified position in the list: + +```js +list.add('a').tail(); +list.add('b').tail(); +list.add('c').tail(); + +// "a" <-> "b" <-> "c" + +list.add('x').byIndex(2); + +// "a" <-> "b" <-> "x" <-> "c" +``` + + + +> This is an alternative API for `addByIndex`. + + + +### How to Remove Nodes + +There are a few methods to remove nodes from a linked list and all of them are separately available as well as revealed from a `drop` method. + + + +#### dropHead(): ListNode\ | undefined + +Removes the first node from the list: + +```js +list.addTail('a'); +list.addTail('b'); +list.addTail('c'); + +// "a" <-> "b" <-> "c" + +list.dropHead(); + +// "b" <-> "c" +``` + + + +#### dropTail(): ListNode\ | undefined + +Removes the last node from the list: + +```js +list.addTail('a'); +list.addTail('b'); +list.addTail('c'); + +// "a" <-> "b" <-> "c" + +list.dropTail(); + +// "a" <-> "b" +``` + + + +#### dropByIndex(position: number): ListNode\ | undefined + +Removes the node with the specified position from the list: + +```js +list.addTail('a'); +list.addTail('b'); +list.addTail('c'); + +// "a" <-> "b" <-> "c" + +list.dropByIndex(1); + +// "a" <-> "c" +``` + + + +#### dropByValue(value: T, compareFn = compare): ListNode\ | undefined + +Removes the first node with given value from the list: + +```js +list.addTail('a'); +list.addTail('x'); +list.addTail('b'); +list.addTail('x'); +list.addTail('c'); + +// "a" <-> "x" <-> "b" <-> "x" <-> "c" + +list.dropByValue('x'); + +// "a" <-> "b" <-> "x" <-> "c" +``` + + + +You may pass a custom compare function to detect the searched value: + +```js +list.addTail({ x: 1 }); +list.addTail({ x: 0 }); +list.addTail({ x: 2 }); +list.addTail({ x: 0 }); +list.addTail({ x: 3 }); + +// {"x":1} <-> {"x":0} <-> {"x":2} <-> {"x":0} <-> {"x":3} + +list.dropByValue({ x: 0 }, (v1, v2) => v1.x === v2.x); + +// {"x":1} <-> {"x":2} <-> {"x":0} <-> {"x":3} +``` + + + +> The default compare function checks deep equality, so you will rarely need to pass that parameter. + + + +#### dropByValueAll(value: T, compareFn = compare): ListNode\\[\] + +Removes all nodes with given value from the list: + +```js +list.addTail('a'); +list.addTail('x'); +list.addTail('b'); +list.addTail('x'); +list.addTail('c'); + +// "a" <-> "x" <-> "b" <-> "x" <-> "c" + +list.dropByValueAll('x'); + +// "a" <-> "b" <-> "c" +``` + + + +You may pass a custom compare function to detect the searched value: + +```js +list.addTail({ x: 1 }); +list.addTail({ x: 0 }); +list.addTail({ x: 2 }); +list.addTail({ x: 0 }); +list.addTail({ x: 3 }); + +// {"x":1} <-> {"x":0} <-> {"x":2} <-> {"x":0} <-> {"x":3} + +list.dropByValue({ x: 0 }, (v1, v2) => v1.x === v2.x); + +// {"x":1} <-> {"x":2} <-> {"x":3} +``` + + + +> The default compare function checks deep equality, so you will rarely need to pass that parameter. + + + +#### drop().head(): ListNode\ | undefined + +Removes the first node in list: + +```js +list.add('a').tail(); +list.add('b').tail(); +list.add('c').tail(); + +// "a" <-> "b" <-> "c" + +list.drop().head(); + +// "b" <-> "c" +``` + + + +> This is an alternative API for `dropHead`. + + + +#### drop().tail(): ListNode\ | undefined + +Removes the last node in list: + +```js +list.add('a').tail(); +list.add('b').tail(); +list.add('c').tail(); + +// "a" <-> "b" <-> "c" + +list.drop().tail(); + +// "a" <-> "b" +``` + + + +> This is an alternative API for `dropTail`. + + + +#### drop().byIndex(position: number): ListNode\ | undefined + +Removes the node with the specified position from the list: + +```js +list.add('a').tail(); +list.add('b').tail(); +list.add('c').tail(); + +// "a" <-> "b" <-> "c" + +list.drop().byIndex(1); + +// "a" <-> "c" +``` + + + +> This is an alternative API for `dropByIndex`. + + + +#### drop().byValue(value: T, compareFn = compare): ListNode\ | undefined + +Removes the first node with given value from the list: + +```js +list.add('a').tail(); +list.add('x').tail(); +list.add('b').tail(); +list.add('x').tail(); +list.add('c').tail(); + +// "a" <-> "x" <-> "b" <-> "x" <-> "c" + +list.drop().byValue('x'); + +// "a" <-> "b" <-> "x" <-> "c" +``` + + + +You may pass a custom compare function to detect the searched value: + +```js +list.add({ x: 1 }).tail(); +list.add({ x: 0 }).tail(); +list.add({ x: 2 }).tail(); +list.add({ x: 0 }).tail(); +list.add({ x: 3 }).tail(); + +// {"x":1} <-> {"x":0} <-> {"x":2} <-> {"x":0} <-> {"x":3} + +list.drop().byValue({ x: 0 }, (v1, v2) => v1.x === v2.x); + +// {"x":1} <-> {"x":2} <-> {"x":0} <-> {"x":3} +``` + + + +> This is an alternative API for `dropByValue`. +> +> The default compare function checks deep equality, so you will rarely need to pass that parameter. + + + +#### drop().byValueAll(value: T, compareFn = compare): ListNode\\[\] + +Removes all nodes with given value from the list: + +```js +list.add('a').tail(); +list.add('x').tail(); +list.add('b').tail(); +list.add('x').tail(); +list.add('c').tail(); + +// "a" <-> "x" <-> "b" <-> "x" <-> "c" + +list.drop().byValueAll('x'); + +// "a" <-> "b" <-> "c" +``` + + + +You may pass a custom compare function to detect the searched value: + +```js +list.add({ x: 1 }).tail(); +list.add({ x: 0 }).tail(); +list.add({ x: 2 }).tail(); +list.add({ x: 0 }).tail(); +list.add({ x: 3 }).tail(); + +// {"x":1} <-> {"x":0} <-> {"x":2} <-> {"x":0} <-> {"x":3} + +list.drop().byValueAll({ x: 0 }, (v1, v2) => v1.x === v2.x); + +// {"x":1} <-> {"x":2} <-> {"x":3} +``` + + + +> This is an alternative API for `dropByValueAll`. +> +> The default compare function checks deep equality, so you will rarely need to pass that parameter. + + + +### How to Find Nodes + +There are a few methods to find specific nodes in a linked list. + + + +#### find(predicate: ListIteratorFunction\): ListNode\ | undefined + +Finds the first node from the list that matches the given predicate: + +```js +list.addTail('a'); +list.addTail('b'); +list.addTail('b'); +list.addTail('c'); + +// "a" <-> "b" <-> "b" <-> "c" + +const found = list.find(node => node.value === 'b'); + +/* +found.value === "b" +found.previous.value === "a" +found.next.value === "b" +*/ +``` + + + +#### findIndex(predicate: ListIteratorFunction\): number + +Finds the position of the first node from the list that matches the given predicate: + +```js +list.addTail('a'); +list.addTail('b'); +list.addTail('b'); +list.addTail('c'); + +// "a" <-> "b" <-> "b" <-> "c" + +const i0 = list.findIndex(node => node.next && node.next.value === 'b'); +const i1 = list.findIndex(node => node.value === 'b'); +const i2 = list.findIndex(node => node.previous && node.previous.value === 'b'); +const i3 = list.findIndex(node => node.value === 'x'); + +/* +i0 === 0 +i1 === 1 +i2 === 2 +i3 === -1 +*/ +``` + + + +#### get(position: number): ListNode\ | undefined + +Finds and returns the node with specific position in the list: + +```js +list.addTail('a'); +list.addTail('b'); +list.addTail('c'); + +// "a" <-> "b" <-> "c" + +const found = list.get(1); + +/* +found.value === "b" +found.previous.value === "a" +found.next.value === "c" +*/ +``` + + + +#### indexOf(value: T, compareFn = compare): number + +Finds the position of the first node from the list that has the given value: + +```js +list.addTail('a'); +list.addTail('b'); +list.addTail('b'); +list.addTail('c'); + +// "a" <-> "b" <-> "b" <-> "c" + +const i0 = list.indexOf('a'); +const i1 = list.indexOf('b'); +const i2 = list.indexOf('c'); +const i3 = list.indexOf('x'); + +/* +i0 === 0 +i1 === 1 +i2 === 3 +i3 === -1 +*/ +``` + + + +You may pass a custom compare function to detect the searched value: + +```js +list.addTail({ x: 1 }); +list.addTail({ x: 0 }); +list.addTail({ x: 2 }); +list.addTail({ x: 0 }); +list.addTail({ x: 3 }); + +// {"x":1} <-> {"x":0} <-> {"x":2} <-> {"x":0} <-> {"x":3} + +const i0 = indexOf({ x: 1 }, (v1, v2) => v1.x === v2.x); +const i1 = indexOf({ x: 2 }, (v1, v2) => v1.x === v2.x); +const i2 = indexOf({ x: 3 }, (v1, v2) => v1.x === v2.x); +const i3 = indexOf({ x: 0 }, (v1, v2) => v1.x === v2.x); +const i4 = indexOf({ x: 4 }, (v1, v2) => v1.x === v2.x); + +/* +i0 === 0 +i1 === 2 +i2 === 4 +i3 === 1 +i4 === -1 +*/ +``` + + + +> The default compare function checks deep equality, so you will rarely need to pass that parameter. + + + +### How to Check All Nodes + +There are a few ways to iterate over or display a linked list. + + + +#### forEach(callback: ListIteratorFunction\): void + +Runs a callback function on all nodes in a linked list from head to tail: + +```js +list.addTail('a'); +list.addTail('b'); +list.addTail('c'); + +// "a" <-> "b" <-> "c" + +list.forEach((node, index) => console.log(node.value + index)); + +// 'a0' +// 'b1' +// 'c2' +``` + + + +#### \*\[Symbol.iterator\]\(\) + +A linked list is iterable. In other words, you may use methods like `for...of` on it. + +```js +list.addTail('a'); +list.addTail('b'); +list.addTail('c'); + +// "a" <-> "b" <-> "c" + +for(const node of list) { + console.log(node.value); +} + +// 'a' +// 'b' +// 'c' +``` + + + +#### toArray(): T[] + +Converts a linked list to an array: + +```js +list.addTail('a'); +list.addTail('b'); +list.addTail('c'); + +// "a" <-> "b" <-> "c" + +const arr = list.toArray(); + +/* +arr === ['a', 'b', 'c'] +*/ +``` + + + +#### toString(): string + +Converts a linked list to a string representation of nodes and their relations: + +```js +list.addTail('a'); +list.addTail(2); +list.addTail('c'); +list.addTail({ k: 4, v: 'd' }); + +// "a" <-> 2 <-> "c" <-> {"k":4,"v":"d"} + +const str = list.toString(); + +/* +str === '"a" <-> 2 <-> "c" <-> {"k":4,"v":"d"}' +*/ +``` + + + From 72051e435e6fb480d679ab1dd89493e7fda3959b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Halil=20=C4=B0brahim=20Kalkan?= Date: Wed, 25 Mar 2020 21:08:57 +0300 Subject: [PATCH 26/28] Update Entity-Framework-Core-Migrations document for new entity extension system. --- ...-Application-Modules-Extending-Entities.md | 1 + docs/en/Entity-Framework-Core-Migrations.md | 104 +++++++----------- 2 files changed, 39 insertions(+), 66 deletions(-) diff --git a/docs/en/Customizing-Application-Modules-Extending-Entities.md b/docs/en/Customizing-Application-Modules-Extending-Entities.md index 878f9c671e..be8bda6068 100644 --- a/docs/en/Customizing-Application-Modules-Extending-Entities.md +++ b/docs/en/Customizing-Application-Modules-Extending-Entities.md @@ -174,4 +174,5 @@ public class MyDistributedIdentityUserCreatedEventHandler : ## See Also +* [Migration System for the EF Core](Entity-Framework-Core-Migrations.md) * [Customizing the Existing Modules](Customizing-Application-Modules-Guide.md) \ No newline at end of file diff --git a/docs/en/Entity-Framework-Core-Migrations.md b/docs/en/Entity-Framework-Core-Migrations.md index 23b17293f1..5d2fff011c 100644 --- a/docs/en/Entity-Framework-Core-Migrations.md +++ b/docs/en/Entity-Framework-Core-Migrations.md @@ -93,7 +93,7 @@ From the database point of view, there are three important projects those will b This project has the `DbContext` class (`BookStoreDbContext` for this sample) of your application. -**Every module uses its own `DbContext` class** to access to the database. Likewise, your application has its own `DbContext`. You typically use this `DbContext` in your application code (in your custom [repositories](Repositories.md) if you follow the best practices). It is almost an empty `DbContext` since your application don't have any entities at the beginning, except the pre-defined `AppUser` entity: +**Every module uses its own `DbContext` class** to access to the database. Likewise, your application has its own `DbContext`. You typically use this `DbContext` in your application code (in your [repositories](Repositories.md) if you follow the best practices). It is almost an empty `DbContext` since your application don't have any entities at the beginning, except the pre-defined `AppUser` entity: ````csharp [ConnectionStringName("Default")] @@ -117,15 +117,15 @@ public class BookStoreDbContext : AbpDbContext builder.Entity(b => { - //Sharing the same table "AbpUsers" with the IdentityUser - b.ToTable("AbpUsers"); - - //Configure base properties + //Sharing the same Users table with the IdentityUser + b.ToTable(AbpIdentityDbProperties.DbTablePrefix + "Users"); + b.ConfigureByConvention(); b.ConfigureAbpUser(); - //Moved customization of the "AbpUsers" table to an extension method - b.ConfigureCustomUserProperties(); + /* Configure mappings for your additional properties + * Also see the MyProjectNameEntityExtensions class + */ }); /* Configure your own tables/entities inside the ConfigureBookStore method */ @@ -188,12 +188,6 @@ public class BookStoreMigrationsDbContext : AbpDbContext(b => - { - b.ConfigureCustomUserProperties(); - }); - /* Configure your own tables/entities inside the ConfigureBookStore method */ builder.ConfigureBookStore(); } @@ -274,7 +268,7 @@ In this way, the mapping configuration of a module can be shared between `DbCont You may want to **reuse a table** of a depended module in your application. In this case, you have two options: -1. You can **directly use the entity** defined by the module. +1. You can **directly use the entity** defined by the module (you can still [extend the entity](Customizing-Application-Modules-Extending-Entities.md) in some level). 2. You can **create a new entity** mapping to the same database table. ###### Use the Entity Defined by a Module @@ -376,10 +370,8 @@ protected override void OnModelCreating(ModelBuilder builder) builder.Entity(b => { b.ToTable("AbpRoles"); - b.ConfigureByConvention(); - - b.ConfigureCustomRoleProperties(); + b.Property(x => x.Title).HasMaxLength(128); }); ... @@ -395,69 +387,44 @@ We added the following lines: ````csharp builder.Entity(b => { - b.ToTable("AbpRoles"); - + b.ToTable("AbpRoles"); b.ConfigureByConvention(); - - b.ConfigureCustomRoleProperties(); + b.Property(x => x.Title).HasMaxLength(128); }); ```` * It maps to the same `AbpRoles` table shared with the `IdentityRole` entity. * `ConfigureByConvention()` configures the standard/base properties (like `TenantId`) and recommended to always call it. -`ConfigureCustomRoleProperties()` has not exists yet. Define it inside the `BookStoreDbContextModelCreatingExtensions` class (near to your `DbContext` in the `.EntityFrameworkCore` project): - -````csharp -public static void ConfigureCustomRoleProperties(this EntityTypeBuilder b) - where TRole : class, IEntity -{ - b.Property(nameof(AppRole.Title)).HasMaxLength(128); -} -```` - -* This method only defines the **custom properties** of your entity. -* Unfortunately, we can not utilize the fully **type safety** here (by referencing the `AppRole` entity). The best we can do is to use the `Title` name as type safe. This is because of EF Core migration system can not map two unrelated entity classes to the same database table. +You've configured the custom property for your `DbContext` that is used by your application on the runtime. We also need to configure the `MigrationsDbContext`. -You've configured the custom property for your `DbContext` used by your application on the runtime. We also need to configure the `MigrationsDbContext`. - -Open the `MigrationsDbContext` (`BookStoreMigrationsDbContext` for this example) and change as shown below: +Instead of directly changing the `MigrationsDbContext`, we should use the entity extension system of the ABP Framework. Find the `YourProjectNameEntityExtensions` class in the `.EntityFrameworkCore` project of your solution (`BookStoreEntityExtensions` for this example) and change it as shown below: ````csharp -protected override void OnModelCreating(ModelBuilder builder) +public static class MyProjectNameEntityExtensions { - base.OnModelCreating(builder); + private static readonly OneTimeRunner OneTimeRunner = new OneTimeRunner(); - /* Include modules to your migration db context */ - - ... - - /* Configure customizations for entities from the modules included */ - - //CONFIGURE THE CUSTOM ROLE PROPERTIES - builder.Entity(b => + public static void Configure() { - b.ConfigureCustomRoleProperties(); - }); - - ... - - /* Configure your own tables/entities inside the ConfigureBookStore method */ - - builder.ConfigureBookStore(); + OneTimeRunner.Run(() => + { + EntityExtensionManager.AddProperty( + "Title", + b => { b.HasMaxLength(128); } + ); + }); + } } ```` -Only added the following lines: +> Instead of hard-coded "Title" string, we suggest to use `nameof(AppRole.Title)`. -````csharp -builder.Entity(b => -{ - b.ConfigureCustomRoleProperties(); -}); -```` +`EntityExtensionManager` is used to add properties to existing entities. Since `EntityExtensionManager` is static, we should call it once. `OneTimeRunner` is a simple utility class defined by the ABP Framework. + +See the [EF Core integration documentation](Entity-Framework-Core.md) for more about the entity extension system. -In this way, we re-used the extension method that is used to configure custom property mappings for the role. But, this time, did the same customization for the `IdentityRole` entity. +> We've repeated a similar database mapping code, like `HasMaxLength(128)`, in both classes. Now, you can add a new EF Core database migration using the standard `Add-Migration` command in the Package Manager Console (remember to select `.EntityFrameworkCore.DbMigrations` as the Default Project in the PMC and make sure that the `.Web` project is still the startup project): @@ -536,7 +503,7 @@ Instead of creating a new entity class to add a custom property, you can use the ###### Using the ExtraProperties -All entities derived from the `AggregateRoot ` class can store name-value pairs in their `ExtraProperties` property, which is a `Dictionary` serialized to JSON in the database table. So, you can add values to this dictionary and query again without changing the entity. +All entities derived from the `AggregateRoot ` class can store name-value pairs in their `ExtraProperties` property (because they implement the `IHasExtraProperties` interface), which is a `Dictionary` serialized to JSON in the database table. So, you can add values to this dictionary and query again without changing the entity. For example, you can store query the title Property inside an `IdentityRole` instead of creating a new entity. Example: @@ -553,16 +520,13 @@ public class IdentityRoleExtendingService : ITransientDependency public async Task GetTitleAsync(Guid id) { var role = await _identityRoleRepository.GetAsync(id); - return role.GetProperty("Title"); } public async Task SetTitleAsync(Guid id, string newTitle) { var role = await _identityRoleRepository.GetAsync(id); - role.SetProperty("Title", newTitle); - await _identityRoleRepository.UpdateAsync(role); } } @@ -575,12 +539,20 @@ In this way, you can easily attach any type of value to an entity of a depended * All the extra properties are stored as **a single JSON object** in the database. They are not stored as new table fields, as you may expect. Creating database table indexes and using SQL queries against these properties will be harder compared to simple table fields. * Property names are strings, so they are **not type safe**. It is recommended to define constants for these kind of properties to prevent typo errors. +###### Using the Entity Extensions System + +Entity extension system solves the main problem of the extra properties: It can store an extra property in a **standard table field** in the database. + +All you need to do is to use the `EntityExtensionManager` to define the extra property as explained above, in the `AppRole` example. Then you can continue to use the same `GetProperty` and `SetProperty` methods defined above to get/set the related property on the entity, but this time stored as a separate field in the database. + ###### Creating a New Table Instead of creating a new entity and mapping to the same table, you can also create **your own table** to store your properties. You typically duplicate some values of the original entity. For example, you can add `Name` field to your own table which is a duplication of the `Name` field in the original table. In this case, you don't deal with migration problems, however you need to deal with the problems of data duplication. When the duplicated value changes, you should reflect the same change in your table. You can use local or distributed [event bus](Event-Bus.md) to subscribe to the change events for the original entity. This is the recommended way of depending on a microservice's data from another microservice, especially if they have separate physical databases (you can search on the web on data sharing on a microservice design, it is a wide topic to cover here). +> See the "[extending entities](Customizing-Application-Modules-Extending-Entities.md)" guide for more details on extending entities, including data duplication and synchronization tips. + #### Discussion of an Alternative Scenario: Every Module Manages Its Own Migration Path As mentioned before, `.EntityFrameworkCore.DbMigrations` merges all the database mappings of all the modules (plus your application's mappings) to create a unified migration path. From 18d376dd96eb8b256f237030aa938e7fbe044cd6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Halil=20=C4=B0brahim=20Kalkan?= Date: Wed, 25 Mar 2020 22:45:10 +0300 Subject: [PATCH 27/28] Added "Entity Extension Manager" section to the EF Core document. --- docs/en/Entity-Framework-Core.md | 111 +++++++++++++++++++++++++++++-- 1 file changed, 106 insertions(+), 5 deletions(-) diff --git a/docs/en/Entity-Framework-Core.md b/docs/en/Entity-Framework-Core.md index 788f3234ed..d908ce03f5 100644 --- a/docs/en/Entity-Framework-Core.md +++ b/docs/en/Entity-Framework-Core.md @@ -58,6 +58,53 @@ namespace MyCompany.MyProject } ```` +### About the EF Core Fluent Mapping + +The [application startup template](Startup-Templates/Application.md) has been configured to use the [EF Core fluent configuration API](https://docs.microsoft.com/en-us/ef/core/modeling/) to map your entities to your database tables. + +You can still use the **data annotation attributes** (like `[Required]`) on the properties of your entity while the ABP documentation generally follows the **fluent mapping API** approach. It is up to you. + +ABP Framework has some **base entity classes** and **conventions** (see the [entities document](Entities.md)) and it provides some useful **extension methods** to configure the properties inherited from the base entity classes. + +#### ConfigureByConvention Method + +`ConfigureByConvention()` is the main extension method that **configures all the base properties** and conventions for your entities. So, it is a **best practice** to call this method for all your entities, in your fluent mapping code. + +**Example**: Assume that you've a `Book` entity derived from `AggregateRoot` base class: + +````csharp +public class Book : AuditedAggregateRoot +{ + public string Name { get; set; } +} +```` + +You can override the `OnModelCreating` method in your `DbContext` and configure the mapping as shown below: + +````csharp +protected override void OnModelCreating(ModelBuilder builder) +{ + //Always call the base method + base.OnModelCreating(builder); + + builder.Entity(b => + { + b.ToTable("Books"); + + //Configure the base properties + b.ConfigureByConvention(); + + //Configure other properties (if you are using the fluent API) + b.Property(x => x.Name).IsRequired().HasMaxLength(128); + }); +} +```` + +* Calling `b.ConfigureByConvention()` is important here to properly **configure the base properties**. +* You can configure the `Name` property here or you can use the **data annotation attributes** (see the [EF Core document](https://docs.microsoft.com/en-us/ef/core/modeling/entity-properties)). + +> While there are many extension methods to configure your base properties, `ConfigureByConvention()` internally calls them if necessary. So, it is enough to call it. + ### Configure the Connection String Selection If you have multiple databases in your application, you can configure the connection string name for your DbContext using the `[ConnectionStringName]` attribute. Example: @@ -225,7 +272,7 @@ public override async Task DeleteAsync( } ```` -### Access to the EF Core API +## Access to the EF Core API In most cases, you want to hide EF Core APIs behind a repository (this is the main purpose of the repository pattern). However, if you want to access the `DbContext` instance over the repository, you can use `GetDbContext()` or `GetDbSet()` extension methods. Example: @@ -251,9 +298,59 @@ public class BookService > Important: You must reference to the `Volo.Abp.EntityFrameworkCore` package from the project you want to access to the DbContext. This breaks encapsulation, but this is what you want in that case. -### Advanced Topics +## Extra Properties & Entity Extension Manager + +Extra Properties system allows you to set/get dynamic properties to entities those implement the `IHasExtraProperties` interface. It is especially useful when you want to add custom properties to the entities defined in an [application module](Modules/Index.md), when you use the module as package reference. + +By default, all the extra properties of an entity are stored as a single `JSON` object in the database. Entity extension system allows you to to store desired extra properties in separate fields in the related database table. -#### Set Default Repository Classes +For more information about the extra properties & the entity extension system, see the following documents: + +* [Customizing the Application Modules: Extending Entities](Customizing-Application-Modules-Extending-Entities.md) +* [Entities](Entities.md) + +This section only explains the `EntityExtensionManager` and its usage. + +### AddProperty Method + +`AddProperty` method of the `EntityExtensionManager` allows you to define additional properties for an entity type. + +**Example**: Add `Title` property (database field) to the `IdentityRole` entity: + +````csharp +EntityExtensionManager.AddProperty( + "Title", + b => { b.HasMaxLength(128); } +); +```` + +If the related module has implemented this feature (by using the `ConfigureExtensions` explained below), then the new property is added to the model. Then you need to run the standard `Add-Migration` and `Update-Database` commands to update your database to add the new field. + +>`AddProperty` method must be called before using the related `DbContext`. It is a static method. The best way is to use it in your application as earlier as possible. The application startup template has a `YourProjectNameEntityExtensions` class that is safe to use this method inside. + +### ConfigureExtensions + +If you are building a reusable module and want to allow application developers to add properties to your entities, you can use the `ConfigureExtensions` extension method in your entity mapping: + +````csharp +builder.Entity(b => +{ + b.ConfigureExtensions(); + //... +}); +```` + +If you call `ConfigureByConvention()` extension method (like `b.ConfigureByConvention()` in this example), ABP Framework internally calls the `ConfigureExtensions` method. It is a **best practice** to use the `ConfigureByConvention()` method since it also configures database mapping for base properties by convention. + +See the "*ConfigureByConvention Method*" section above for more information. + +### GetPropertyNames + +`EntityExtensionManager.GetPropertyNames` static method can be used the names of the extension properties defined for this entity. It is normally not needed by an application code, but used by the ABP Framework internally. + +## Advanced Topics + +### Set Default Repository Classes Default generic repositories are implemented by `EfCoreRepository` class by default. You can create your own implementation and use it for all the default repository implementations. @@ -299,7 +396,7 @@ context.Services.AddAbpDbContext(options => }); ``` -#### Set Base DbContext Class or Interface for Default Repositories +### Set Base DbContext Class or Interface for Default Repositories If your DbContext inherits from another DbContext or implements an interface, you can use that base class or interface as DbContext for default repositories. Example: @@ -331,7 +428,7 @@ public class BookRepository : EfCoreRepository, One advantage of using an interface for a DbContext is then it will be replaceable by another implementation. -#### Replace Other DbContextes +### Replace Other DbContextes Once you properly define and use an interface for DbContext, then any other implementation can replace it using the `ReplaceDbContext` option: @@ -344,3 +441,7 @@ context.Services.AddAbpDbContext(options => ```` In this example, `OtherDbContext` implements `IBookStoreDbContext`. This feature allows you to have multiple DbContext (one per module) on development, but single DbContext (implements all interfaces of all DbContexts) on runtime. + +## See Also + +* [Entities](Entities.md) \ No newline at end of file From 498e96703fb62bfc8e90abc0b137793aa9b82d19 Mon Sep 17 00:00:00 2001 From: mehmet-erim Date: Thu, 26 Mar 2020 15:06:55 +0300 Subject: [PATCH 28/28] docs: add Linked List to docs-nav.json --- docs/en/UI/Angular/Track-By-Service.md | 3 +++ docs/en/docs-nav.json | 4 ++++ 2 files changed, 7 insertions(+) diff --git a/docs/en/UI/Angular/Track-By-Service.md b/docs/en/UI/Angular/Track-By-Service.md index 4506f77e47..71a98fda9d 100644 --- a/docs/en/UI/Angular/Track-By-Service.md +++ b/docs/en/UI/Angular/Track-By-Service.md @@ -112,3 +112,6 @@ class DemoComponent { } ``` +## What's Next? + +* [Linked List (Doubly)](./Linked-List.md) \ No newline at end of file diff --git a/docs/en/docs-nav.json b/docs/en/docs-nav.json index 425b8c526a..3845c979c5 100644 --- a/docs/en/docs-nav.json +++ b/docs/en/docs-nav.json @@ -339,6 +339,10 @@ { "text": "TrackByService", "path": "UI/Angular/Track-By-Service.md" + }, + { + "text": "Linked List (Doubly)", + "path": "UI/Angular/Linked-List.md" } ] }