diff --git a/docs/en/Object-Extensions.md b/docs/en/Object-Extensions.md index bee229dc27..40a7ddd5ea 100644 --- a/docs/en/Object-Extensions.md +++ b/docs/en/Object-Extensions.md @@ -174,6 +174,44 @@ ObjectExtensionManager.Instance The following sections explain the fundamental property configuration options. +#### Default Value + +A default value is automatically set for the new property, which is the natural default value for the property type, like `null` for `string`, `false` for `bool` or `0` for `int`. + +There are two ways to override the default value: + +##### DefaultValue Option + +`DefaultValue` option can be set to any value: + +````csharp +ObjectExtensionManager.Instance + .AddOrUpdateProperty( + "MyIntProperty", + options => + { + options.DefaultValue = 42; + }); +```` + +##### DefaultValueFactory Options + +`DefaultValueFactory` can be set to a function that returns the default value: + +````csharp +ObjectExtensionManager.Instance + .AddOrUpdateProperty( + "MyIntProperty", + options => + { + options.DefaultValueFactory = () => 42; + }); +```` + +`options.DefaultValueFactory` has a higher priority than the `options.DefaultValue` . + +> Tip: Use `DefaultValueFactory` option only if the default value may change over the time. If it is a constant value, then use the `DefaultValue` option. + #### CheckPairDefinitionOnMapping Controls how to check property definitions while mapping two extensible objects. See the "Object to Object Mapping" section to understand the `CheckPairDefinitionOnMapping` option better. @@ -208,6 +246,15 @@ ObjectExtensionManager.Instance With this configuration, `IdentityUserCreateDto` objects will be invalid without a valid `SocialSecurityNumber` value provided. +#### Default Validation Attributes + +There are some attributes **automatically added** when you create certain type of properties; + +* `RequiredAttribute` is added for non nullable primitive property types (e.g. `int`, `bool`, `DateTime`...) and `enum` types. +* `EnumDataTypeAttribute` is added for enum types, to prevent to set invalid enum values. + +Use `options.Attributes.Clear();` if you don't want these attributes. + ### Custom Validation If you need, you can add a custom action that is executed to validate the extra properties. Example: diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.Contracts/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/ObjectExtending/ExtensionPropertyDto.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.Contracts/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/ObjectExtending/ExtensionPropertyDto.cs index 74214914b9..b1ba6f7ea9 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc.Contracts/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/ObjectExtending/ExtensionPropertyDto.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.Contracts/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/ObjectExtending/ExtensionPropertyDto.cs @@ -21,5 +21,7 @@ namespace Volo.Abp.AspNetCore.Mvc.ApplicationConfigurations.ObjectExtending public List Attributes { get; set; } public Dictionary Configuration { get; set; } + + public object DefaultValue { get; set; } } } \ No newline at end of file diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/AbpTagHelperLocalizer.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/AbpTagHelperLocalizer.cs index 9f047c5b3c..700a5f4ee9 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/AbpTagHelperLocalizer.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/AbpTagHelperLocalizer.cs @@ -21,38 +21,30 @@ namespace Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers public string GetLocalizedText(string text, ModelExplorer explorer) { - var resourceType = GetResourceTypeFromModelExplorer(explorer); - var localizer = GetStringLocalizer(resourceType); - - return localizer == null ? text : localizer[text].Value; - } - - public IStringLocalizer GetLocalizer(ModelExplorer explorer) - { - var resourceType = GetResourceTypeFromModelExplorer(explorer); - return GetStringLocalizer(resourceType); - } - - public IStringLocalizer GetLocalizer(Assembly assembly) - { - var resourceType = _options.AssemblyResources.GetOrDefault(assembly); - return GetStringLocalizer(resourceType); + var localizer = GetLocalizerOrNull(explorer); + return localizer == null + ? text + : localizer[text].Value; } - public IStringLocalizer GetLocalizer(Type resourceType) + public IStringLocalizer GetLocalizerOrNull(ModelExplorer explorer) { - return GetStringLocalizer(resourceType); + return GetLocalizerOrNull(explorer.Container.ModelType.Assembly); } - private IStringLocalizer GetStringLocalizer(Type resourceType) + public IStringLocalizer GetLocalizerOrNull(Assembly assembly) { - return resourceType == null ? null : _stringLocalizerFactory.Create(resourceType); + var resourceType = GetResourceType(assembly); + return resourceType == null + ? _stringLocalizerFactory.CreateDefaultOrNull() + : _stringLocalizerFactory.Create(resourceType); } - private Type GetResourceTypeFromModelExplorer(ModelExplorer explorer) + private Type GetResourceType(Assembly assembly) { - var assembly = explorer.Container.ModelType.Assembly; - return _options.AssemblyResources.GetOrDefault(assembly); + return _options + .AssemblyResources + .GetOrDefault(assembly); } } } diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Form/AbpRadioInputTagHelperService.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Form/AbpRadioInputTagHelperService.cs index 010cc1d075..bcfa5bd7d1 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Form/AbpRadioInputTagHelperService.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Form/AbpRadioInputTagHelperService.cs @@ -90,7 +90,7 @@ namespace Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Form protected virtual List GetSelectItemsFromEnum(TagHelperContext context, TagHelperOutput output, ModelExplorer explorer) { - var localizer = _tagHelperLocalizer.GetLocalizer(explorer); + var localizer = _tagHelperLocalizer.GetLocalizerOrNull(explorer); var selectItems = explorer.Metadata.IsEnum ? explorer.ModelType.GetTypeInfo().GetMembers(BindingFlags.Public | BindingFlags.Static) .Select((t, i) => new SelectListItem { Value = i.ToString(), Text = GetLocalizedPropertyName(localizer, explorer.ModelType, t.Name) }).ToList() : null; diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Form/AbpSelectTagHelperService.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Form/AbpSelectTagHelperService.cs index 3ed4f63f82..bb79740a41 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Form/AbpSelectTagHelperService.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Form/AbpSelectTagHelperService.cs @@ -2,7 +2,6 @@ using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Linq; -using System.Reflection; using System.Text.Encodings.Web; using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc.Rendering; @@ -12,6 +11,8 @@ using Microsoft.AspNetCore.Razor.TagHelpers; using Microsoft.Extensions.Localization; using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.Microsoft.AspNetCore.Razor.TagHelpers; using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Extensions; +using Volo.Abp.DynamicProxy; +using Volo.Abp.Reflection; namespace Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Form { @@ -102,7 +103,7 @@ namespace Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Form return TagHelper.AspItems.ToList(); } - if (TagHelper.AspFor.ModelExplorer.Metadata.IsEnum) + if (IsEnum()) { return GetSelectItemsFromEnum(context, output, TagHelper.AspFor.ModelExplorer); } @@ -116,6 +117,17 @@ namespace Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Form throw new Exception("No items provided for select attribute."); } + private bool IsEnum() + { + var value = TagHelper.AspFor.Model; + if (value != null && value.GetType().IsEnum) + { + return true; + } + + return TagHelper.AspFor.ModelExplorer.Metadata.IsEnum; + } + protected virtual async Task GetLabelAsHtmlAsync(TagHelperContext context, TagHelperOutput output, TagHelperOutput selectTag) { if (!string.IsNullOrEmpty(TagHelper.Label)) @@ -186,8 +198,6 @@ namespace Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Form protected virtual List GetSelectItemsFromEnum(TagHelperContext context, TagHelperOutput output, ModelExplorer explorer) { - var localizer = _tagHelperLocalizer.GetLocalizer(explorer); - var selectItems = new List(); var isNullableType = Nullable.GetUnderlyingType(explorer.ModelType) != null; var enumType = explorer.ModelType; @@ -198,26 +208,75 @@ namespace Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Form selectItems.Add(new SelectListItem()); } + var containerLocalizer = _tagHelperLocalizer.GetLocalizerOrNull(explorer.Container.ModelType.Assembly); + IStringLocalizer modelObjectLocalizer = null; + if (explorer.Model != null) + { + modelObjectLocalizer = _tagHelperLocalizer.GetLocalizerOrNull(ProxyHelper.UnProxy(explorer.Model).GetType().Assembly); + } + selectItems.AddRange(enumType.GetEnumNames() .Select(enumName => new SelectListItem { Value = Convert.ToUInt64(Enum.Parse(enumType, enumName)).ToString(), - Text = GetLocalizedPropertyName(localizer, enumType, enumName) + Text = GetLocalizedEnumFieldName(containerLocalizer, modelObjectLocalizer, enumType, enumName) })); return selectItems; } - protected virtual string GetLocalizedPropertyName(IStringLocalizer localizer, Type enumType, string propertyName) + protected virtual string GetLocalizedEnumFieldName( + IStringLocalizer containerLocalizer, + IStringLocalizer modelObjectLocalizer, + Type enumType, + string fieldName) { - if (localizer == null) + LocalizedString localizedString; + + //Look for the enum name + enum field name + + var localizationKey = enumType.Name + "." + fieldName; + if (containerLocalizer != null) + { + localizedString = containerLocalizer[localizationKey]; + if (!localizedString.ResourceNotFound) + { + return localizedString.Value; + } + } + + if (modelObjectLocalizer != null) { - return propertyName; + localizedString = modelObjectLocalizer[localizationKey]; + if (!localizedString.ResourceNotFound) + { + return localizedString.Value; + } } - var localizedString = localizer[enumType.Name + "." + propertyName]; + //Look for the enum field name + + localizationKey = fieldName; + + if (containerLocalizer != null) + { + localizedString = containerLocalizer[localizationKey]; + if (!localizedString.ResourceNotFound) + { + return localizedString.Value; + } + } + + if (modelObjectLocalizer != null) + { + localizedString = modelObjectLocalizer[localizationKey]; + if (!localizedString.ResourceNotFound) + { + return localizedString.Value; + } + } - return !localizedString.ResourceNotFound ? localizedString.Value : localizer[propertyName].Value; + return fieldName; } protected virtual List GetSelectItemsFromAttribute( diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/IAbpTagHelperLocalizer.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/IAbpTagHelperLocalizer.cs index 8351e47082..2a73777b00 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/IAbpTagHelperLocalizer.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/IAbpTagHelperLocalizer.cs @@ -10,10 +10,8 @@ namespace Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers { string GetLocalizedText(string text, ModelExplorer explorer); - IStringLocalizer GetLocalizer(ModelExplorer explorer); + IStringLocalizer GetLocalizerOrNull(ModelExplorer explorer); - IStringLocalizer GetLocalizer(Assembly assembly); - - IStringLocalizer GetLocalizer(Type resourceType); + IStringLocalizer GetLocalizerOrNull(Assembly assembly); } } diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Pagination/AbpPaginationTagHelperService.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Pagination/AbpPaginationTagHelperService.cs index 76bfcb3ef1..9c37e77767 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Pagination/AbpPaginationTagHelperService.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Pagination/AbpPaginationTagHelperService.cs @@ -7,6 +7,7 @@ using Localization.Resources.AbpUi; using Microsoft.AspNetCore.Mvc.TagHelpers; using Microsoft.AspNetCore.Mvc.ViewFeatures; using Microsoft.AspNetCore.Razor.TagHelpers; +using Microsoft.Extensions.Localization; using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.Microsoft.AspNetCore.Razor.TagHelpers; using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Extensions; @@ -17,12 +18,18 @@ namespace Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Pagination private readonly IHtmlGenerator _generator; private readonly HtmlEncoder _encoder; private readonly IAbpTagHelperLocalizer _tagHelperLocalizer; + private readonly IStringLocalizerFactory _stringLocalizerFactory; - public AbpPaginationTagHelperService(IHtmlGenerator generator, HtmlEncoder encoder, IAbpTagHelperLocalizer tagHelperLocalizer) + public AbpPaginationTagHelperService( + IHtmlGenerator generator, + HtmlEncoder encoder, + IAbpTagHelperLocalizer tagHelperLocalizer, + IStringLocalizerFactory stringLocalizerFactory) { _generator = generator; _encoder = encoder; _tagHelperLocalizer = tagHelperLocalizer; + _stringLocalizerFactory = stringLocalizerFactory; } public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output) @@ -119,7 +126,7 @@ namespace Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Pagination protected virtual async Task RenderAnchorTagHelperLinkHtmlAsync(TagHelperContext context, TagHelperOutput output, string currentPage, string localizationKey) { - var localizer = _tagHelperLocalizer.GetLocalizer(typeof(AbpUiResource)); + var localizer = _stringLocalizerFactory.Create(typeof(AbpUiResource)); var anchorTagHelper = GetAnchorTagHelper(currentPage, out var attributeList); @@ -156,7 +163,7 @@ namespace Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Pagination protected virtual string GetOpeningTags(TagHelperContext context, TagHelperOutput output) { - var localizer = _tagHelperLocalizer.GetLocalizer(typeof(AbpUiResource)); + var localizer = _stringLocalizerFactory.Create(typeof(AbpUiResource)); var pagerInfo = (TagHelper.ShowInfo ?? false) ? "
" + localizer["PagerInfo{0}{1}{2}", TagHelper.Model.ShowingFrom, TagHelper.Model.ShowingTo, TagHelper.Model.TotalItemsCount] + "
\r\n" diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/ObjectExtending/CachedObjectExtensionsDtoService.cs b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/ObjectExtending/CachedObjectExtensionsDtoService.cs index 6e72a6863e..aab28b3cb9 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/ObjectExtending/CachedObjectExtensionsDtoService.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/ObjectExtending/CachedObjectExtensionsDtoService.cs @@ -104,10 +104,13 @@ namespace Volo.Abp.AspNetCore.Mvc.ApplicationConfigurations.ObjectExtending var extensionPropertyDto = new ExtensionPropertyDto { Type = TypeHelper.GetFullNameHandlingNullableAndGenerics(propertyConfig.Type), - TypeSimple = TypeHelper.GetSimplifiedName(propertyConfig.Type), + TypeSimple = propertyConfig.Type.IsEnum + ? "enum" + : TypeHelper.GetSimplifiedName(propertyConfig.Type), Attributes = new List(), DisplayName = CreateDisplayNameDto(propertyConfig), Configuration = new Dictionary(), + DefaultValue = propertyConfig.GetDefaultValue(), Api = new ExtensionPropertyApiDto { OnGet = new ExtensionPropertyApiGetDto diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/Reflection/TypeHelper.cs b/framework/src/Volo.Abp.Core/Volo/Abp/Reflection/TypeHelper.cs index 63b3538c3a..b388ef791d 100644 --- a/framework/src/Volo.Abp.Core/Volo/Abp/Reflection/TypeHelper.cs +++ b/framework/src/Volo.Abp.Core/Volo/Abp/Reflection/TypeHelper.cs @@ -10,6 +10,30 @@ namespace Volo.Abp.Reflection { public static class TypeHelper { + private static readonly HashSet NonNullablePrimitiveTypes = new HashSet + { + typeof(byte), + typeof(short), + typeof(int), + typeof(long), + typeof(sbyte), + typeof(ushort), + typeof(uint), + typeof(ulong), + typeof(bool), + typeof(float), + typeof(decimal), + typeof(DateTime), + typeof(DateTimeOffset), + typeof(TimeSpan), + typeof(Guid) + }; + + public static bool IsNonNullablePrimitiveType(Type type) + { + return NonNullablePrimitiveTypes.Contains(type); + } + public static bool IsFunc(object obj) { if (obj == null) @@ -277,5 +301,17 @@ namespace Volo.Abp.Reflection .GetConverter(targetType) .ConvertFromString(value); } + + public static object ConvertFrom(object value) + { + return ConvertFrom(typeof(TTargetType), value); + } + + public static object ConvertFrom(Type targetType, object value) + { + return TypeDescriptor + .GetConverter(targetType) + .ConvertFrom(value); + } } } 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 7f914fb1a0..2b35523916 100644 --- a/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/AbpDbContext.cs +++ b/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/AbpDbContext.cs @@ -194,6 +194,7 @@ namespace Volo.Abp.EntityFrameworkCore { continue; } + /* 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. 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 ddcdb942ba..c1c8491dd6 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 @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.ComponentModel; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; using Newtonsoft.Json; using Volo.Abp.ObjectExtending; @@ -11,7 +12,7 @@ namespace Volo.Abp.EntityFrameworkCore.ValueConverters public ExtraPropertiesValueConverter(Type entityType) : base( d => SerializeObject(d, entityType), - s => DeserializeObject(s)) + s => DeserializeObject(s, entityType)) { } @@ -38,9 +39,47 @@ namespace Volo.Abp.EntityFrameworkCore.ValueConverters return JsonConvert.SerializeObject(copyDictionary, Formatting.None); } - private static Dictionary DeserializeObject(string extraPropertiesAsJson) + private static Dictionary DeserializeObject(string extraPropertiesAsJson, Type entityType) { - return JsonConvert.DeserializeObject>(extraPropertiesAsJson); + var dictionary = JsonConvert.DeserializeObject>(extraPropertiesAsJson); + + if (entityType != null) + { + var objectExtension = ObjectExtensionManager.Instance.GetOrNull(entityType); + if (objectExtension != null) + { + foreach (var property in objectExtension.GetProperties()) + { + dictionary[property.Name] = GetNormalizedValue(dictionary, property); + } + } + } + + return dictionary; + } + + private static object GetNormalizedValue(Dictionary dictionary, ObjectExtensionPropertyInfo property) + { + var value = dictionary.GetOrDefault(property.Name); + if (value == null) + { + return null; + } + + try + { + if (property.Type.IsEnum) + { + return Enum.Parse(property.Type, value.ToString(), true); + } + + //return Convert.ChangeType(value, property.Type); + return value; + } + catch + { + return value; + } } } } \ No newline at end of file diff --git a/framework/src/Volo.Abp.Localization/Volo/Abp/Localization/AbpDictionaryBasedStringLocalizer.cs b/framework/src/Volo.Abp.Localization/Volo/Abp/Localization/AbpDictionaryBasedStringLocalizer.cs index 53a2d7714e..7d9056ca4d 100644 --- a/framework/src/Volo.Abp.Localization/Volo/Abp/Localization/AbpDictionaryBasedStringLocalizer.cs +++ b/framework/src/Volo.Abp.Localization/Volo/Abp/Localization/AbpDictionaryBasedStringLocalizer.cs @@ -177,9 +177,7 @@ namespace Volo.Abp.Localization return allStrings.Values.ToImmutableList(); } - - public class CultureWrapperStringLocalizer : IStringLocalizer, IStringLocalizerSupportsInheritance { private readonly string _cultureName; diff --git a/framework/src/Volo.Abp.ObjectExtending/Volo/Abp/Data/HasExtraPropertiesExtensions.cs b/framework/src/Volo.Abp.ObjectExtending/Volo/Abp/Data/HasExtraPropertiesExtensions.cs index ca9cc0a4c1..6abb350328 100644 --- a/framework/src/Volo.Abp.ObjectExtending/Volo/Abp/Data/HasExtraPropertiesExtensions.cs +++ b/framework/src/Volo.Abp.ObjectExtending/Volo/Abp/Data/HasExtraPropertiesExtensions.cs @@ -65,8 +65,13 @@ namespace Volo.Abp.Data public static TSource SetDefaultsForExtraProperties(this TSource source, Type objectType = null) where TSource : IHasExtraProperties { + if (objectType == null) + { + objectType = typeof(TSource); + } + var properties = ObjectExtensionManager.Instance - .GetProperties(objectType ?? typeof(TSource)); + .GetProperties(objectType); foreach (var property in properties) { @@ -75,7 +80,7 @@ namespace Volo.Abp.Data continue; } - source.ExtraProperties[property.Name] = TypeHelper.GetDefaultValue(property.Type); + source.ExtraProperties[property.Name] = property.GetDefaultValue(); } return source; diff --git a/framework/src/Volo.Abp.ObjectExtending/Volo/Abp/ObjectExtending/ExtensionPropertyHelper.cs b/framework/src/Volo.Abp.ObjectExtending/Volo/Abp/ObjectExtending/ExtensionPropertyHelper.cs new file mode 100644 index 0000000000..ee36b397c8 --- /dev/null +++ b/framework/src/Volo.Abp.ObjectExtending/Volo/Abp/ObjectExtending/ExtensionPropertyHelper.cs @@ -0,0 +1,37 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using Volo.Abp.Reflection; + +namespace Volo.Abp.ObjectExtending +{ + internal static class ExtensionPropertyHelper + { + public static IEnumerable GetDefaultAttributes(Type type) + { + if (TypeHelper.IsNonNullablePrimitiveType(type) || type.IsEnum) + { + yield return new RequiredAttribute(); + } + + if (type.IsEnum) + { + yield return new EnumDataTypeAttribute(type); + } + } + + public static object GetDefaultValue( + Type propertyType, + Func defaultValueFactory, + object defaultValue) + { + if (defaultValueFactory != null) + { + return defaultValueFactory(); + } + + return defaultValue ?? + TypeHelper.GetDefaultValue(propertyType); + } + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.ObjectExtending/Volo/Abp/ObjectExtending/Modularity/EntityExtensionConfiguration.cs b/framework/src/Volo.Abp.ObjectExtending/Volo/Abp/ObjectExtending/Modularity/EntityExtensionConfiguration.cs index ce84cb19e5..d03696b642 100644 --- a/framework/src/Volo.Abp.ObjectExtending/Volo/Abp/ObjectExtending/Modularity/EntityExtensionConfiguration.cs +++ b/framework/src/Volo.Abp.ObjectExtending/Volo/Abp/ObjectExtending/Modularity/EntityExtensionConfiguration.cs @@ -50,6 +50,8 @@ namespace Volo.Abp.ObjectExtending.Modularity configureAction?.Invoke(propertyInfo); + NormalizeProperty(propertyInfo); + return this; } @@ -58,5 +60,23 @@ namespace Volo.Abp.ObjectExtending.Modularity { return Properties.Values.ToImmutableList(); } + + private static void NormalizeProperty(ExtensionPropertyConfiguration propertyInfo) + { + if (!propertyInfo.Api.OnGet.IsAvailable) + { + propertyInfo.UI.OnTable.IsVisible = false; + } + + if (!propertyInfo.Api.OnCreate.IsAvailable) + { + propertyInfo.UI.OnCreateForm.IsVisible = false; + } + + if (!propertyInfo.Api.OnUpdate.IsAvailable) + { + propertyInfo.UI.OnEditForm.IsVisible = false; + } + } } } \ No newline at end of file diff --git a/framework/src/Volo.Abp.ObjectExtending/Volo/Abp/ObjectExtending/Modularity/ExtensionPropertyConfiguration.cs b/framework/src/Volo.Abp.ObjectExtending/Volo/Abp/ObjectExtending/Modularity/ExtensionPropertyConfiguration.cs index 0ff3941f4e..6d66e51337 100644 --- a/framework/src/Volo.Abp.ObjectExtending/Volo/Abp/ObjectExtending/Modularity/ExtensionPropertyConfiguration.cs +++ b/framework/src/Volo.Abp.ObjectExtending/Volo/Abp/ObjectExtending/Modularity/ExtensionPropertyConfiguration.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using JetBrains.Annotations; using Volo.Abp.Localization; +using Volo.Abp.Reflection; namespace Volo.Abp.ObjectExtending.Modularity { @@ -45,6 +46,19 @@ namespace Volo.Abp.ObjectExtending.Modularity [NotNull] public ExtensionPropertyApiConfiguration Api { get; } + /// + /// Uses as the default value if was not set. + /// + [CanBeNull] + public object DefaultValue { get; set; } + + /// + /// Used with the first priority to create the default value for the property. + /// Uses to the if this was not set. + /// + [CanBeNull] + public Func DefaultValueFactory { get; set; } + public ExtensionPropertyConfiguration( [NotNull] EntityExtensionConfiguration entityExtensionConfiguration, [NotNull] Type type, @@ -61,6 +75,14 @@ namespace Volo.Abp.ObjectExtending.Modularity Entity = new ExtensionPropertyEntityConfiguration(); UI = new ExtensionPropertyUiConfiguration(); Api = new ExtensionPropertyApiConfiguration(); + + Attributes.AddRange(ExtensionPropertyHelper.GetDefaultAttributes(Type)); + DefaultValue = TypeHelper.GetDefaultValue(Type); + } + + public object GetDefaultValue() + { + return ExtensionPropertyHelper.GetDefaultValue(Type, DefaultValueFactory, DefaultValue); } } } \ No newline at end of file diff --git a/framework/src/Volo.Abp.ObjectExtending/Volo/Abp/ObjectExtending/Modularity/ModuleExtensionConfigurationHelper.cs b/framework/src/Volo.Abp.ObjectExtending/Volo/Abp/ObjectExtending/Modularity/ModuleExtensionConfigurationHelper.cs index 1f1edd757e..cb5be17c1f 100644 --- a/framework/src/Volo.Abp.ObjectExtending/Volo/Abp/ObjectExtending/Modularity/ModuleExtensionConfigurationHelper.cs +++ b/framework/src/Volo.Abp.ObjectExtending/Volo/Abp/ObjectExtending/Modularity/ModuleExtensionConfigurationHelper.cs @@ -148,9 +148,12 @@ namespace Volo.Abp.ObjectExtending.Modularity propertyConfig.Name, property => { + property.Attributes.Clear(); property.Attributes.AddRange(propertyConfig.Attributes); property.DisplayName = propertyConfig.DisplayName; property.Validators.AddRange(propertyConfig.Validators); + property.DefaultValue = propertyConfig.DefaultValue; + property.DefaultValueFactory = propertyConfig.DefaultValueFactory; } ); } diff --git a/framework/src/Volo.Abp.ObjectExtending/Volo/Abp/ObjectExtending/ObjectExtensionPropertyInfo.cs b/framework/src/Volo.Abp.ObjectExtending/Volo/Abp/ObjectExtending/ObjectExtensionPropertyInfo.cs index 5d55325477..12b32ab241 100644 --- a/framework/src/Volo.Abp.ObjectExtending/Volo/Abp/ObjectExtending/ObjectExtensionPropertyInfo.cs +++ b/framework/src/Volo.Abp.ObjectExtending/Volo/Abp/ObjectExtending/ObjectExtensionPropertyInfo.cs @@ -2,8 +2,8 @@ using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using JetBrains.Annotations; -using Microsoft.Extensions.Localization; using Volo.Abp.Localization; +using Volo.Abp.Reflection; namespace Volo.Abp.ObjectExtending { @@ -48,6 +48,19 @@ namespace Volo.Abp.ObjectExtending [NotNull] public Dictionary Configuration { get; } + /// + /// Uses as the default value if was not set. + /// + [CanBeNull] + public object DefaultValue { get; set; } + + /// + /// Used with the first priority to create the default value for the property. + /// Uses to the if this was not set. + /// + [CanBeNull] + public Func DefaultValueFactory { get; set; } + public ObjectExtensionPropertyInfo( [NotNull] ObjectExtensionInfo objectExtension, [NotNull] Type type, @@ -61,6 +74,14 @@ namespace Volo.Abp.ObjectExtending ValidationAttributes = new List(); Attributes = new List(); Validators = new List>(); + + Attributes.AddRange(ExtensionPropertyHelper.GetDefaultAttributes(Type)); + DefaultValue = TypeHelper.GetDefaultValue(Type); + } + + public object GetDefaultValue() + { + return ExtensionPropertyHelper.GetDefaultValue(Type, DefaultValueFactory, DefaultValue); } } } diff --git a/framework/test/Volo.Abp.Core.Tests/Volo/Abp/Reflection/TypeHelper_Tests.cs b/framework/test/Volo.Abp.Core.Tests/Volo/Abp/Reflection/TypeHelper_Tests.cs index 02056aab20..978bf9de54 100644 --- a/framework/test/Volo.Abp.Core.Tests/Volo/Abp/Reflection/TypeHelper_Tests.cs +++ b/framework/test/Volo.Abp.Core.Tests/Volo/Abp/Reflection/TypeHelper_Tests.cs @@ -7,6 +7,13 @@ namespace Volo.Abp.Reflection { public class TypeHelper_Tests { + [Fact] + public void IsNonNullablePrimitiveType() + { + TypeHelper.IsNonNullablePrimitiveType(typeof(int)).ShouldBeTrue(); + TypeHelper.IsNonNullablePrimitiveType(typeof(string)).ShouldBeFalse(); + } + [Fact] public void Should_Generic_Type_From_Nullable() { diff --git a/framework/test/Volo.Abp.ObjectExtending.Tests/Volo/Abp/ObjectExtending/ObjectExtensionManager_Tests.cs b/framework/test/Volo.Abp.ObjectExtending.Tests/Volo/Abp/ObjectExtending/ObjectExtensionManager_Tests.cs index 857e2fd155..6386dc41ff 100644 --- a/framework/test/Volo.Abp.ObjectExtending.Tests/Volo/Abp/ObjectExtending/ObjectExtensionManager_Tests.cs +++ b/framework/test/Volo.Abp.ObjectExtending.Tests/Volo/Abp/ObjectExtending/ObjectExtensionManager_Tests.cs @@ -1,4 +1,5 @@ -using System.Linq; +using System.ComponentModel.DataAnnotations; +using System.Linq; using Shouldly; using Xunit; @@ -22,7 +23,7 @@ namespace Volo.Abp.ObjectExtending var objectExtension = _objectExtensionManager.GetOrNull(); objectExtension.ShouldNotBeNull(); - + var properties = objectExtension.GetProperties(); properties.Count.ShouldBe(1); properties.FirstOrDefault(p => p.Name == "TestProp").ShouldNotBeNull(); @@ -55,9 +56,128 @@ namespace Volo.Abp.ObjectExtending property.Configuration["TestConfig2"].ShouldBe("TestConfig2-Value"); } + [Fact] + public void Should_Automatically_Add_RequiredAttribute_To_Non_Nullable_Types_And_Enums() + { + _objectExtensionManager + .AddOrUpdateProperty("IntProp") + .AddOrUpdateProperty("BoolProp") + .AddOrUpdateProperty("NullableIntProp") + .AddOrUpdateProperty("StringProp") + .AddOrUpdateProperty("EnumProp"); + + _objectExtensionManager + .GetPropertyOrNull("IntProp") + .Attributes + .ShouldContain(x => x is RequiredAttribute); + + _objectExtensionManager + .GetPropertyOrNull("BoolProp") + .Attributes + .ShouldContain(x => x is RequiredAttribute); + + _objectExtensionManager + .GetPropertyOrNull("EnumProp") + .Attributes + .ShouldContain(x => x is RequiredAttribute); + + _objectExtensionManager + .GetPropertyOrNull("NullableIntProp") + .Attributes + .ShouldNotContain(x => x is RequiredAttribute); + + _objectExtensionManager + .GetPropertyOrNull("StringProp") + .Attributes + .ShouldNotContain(x => x is RequiredAttribute); + } + + [Fact] + public void Should_Automatically_Add_EnumDataTypeAttribute_For_Enums() + { + _objectExtensionManager + .AddOrUpdateProperty("EnumProp"); + + _objectExtensionManager + .GetPropertyOrNull("EnumProp") + .Attributes + .ShouldContain(x => x is EnumDataTypeAttribute); + } + + [Fact] + public void Should_Be_Able_To_Clear_Auto_Added_Attributes() + { + _objectExtensionManager + .AddOrUpdateProperty("IntProp", property => + { + property.Attributes.Clear(); + }); + + _objectExtensionManager + .GetPropertyOrNull("IntProp") + .Attributes + .ShouldNotContain(x => x is RequiredAttribute); + } + + [Fact] + public void Should_Set_DefaultValues() + { + _objectExtensionManager + .AddOrUpdateProperty("IntProp") + .AddOrUpdateProperty("IntPropWithCustomDefaultValue", property => + { + property.DefaultValue = 42; + }) + .AddOrUpdateProperty("BoolProp") + .AddOrUpdateProperty("NullableIntProp") + .AddOrUpdateProperty("NullableIntPropWithCustomDefaultValueFactory", property => + { + property.DefaultValueFactory = () => 2; + }) + .AddOrUpdateProperty("StringProp") + .AddOrUpdateProperty("StringPropWithCustomDefaultValue", property => + { + property.DefaultValue = "custom-value"; + }); + + _objectExtensionManager + .GetPropertyOrNull("IntProp") + .DefaultValue.ShouldBe(0); + + _objectExtensionManager + .GetPropertyOrNull("IntPropWithCustomDefaultValue") + .DefaultValue.ShouldBe(42); + + _objectExtensionManager + .GetPropertyOrNull("BoolProp") + .DefaultValue.ShouldBe(false); + + _objectExtensionManager + .GetPropertyOrNull("NullableIntProp") + .DefaultValue.ShouldBeNull(); + + var propWithDefaultValueFactory = _objectExtensionManager + .GetPropertyOrNull("NullableIntPropWithCustomDefaultValueFactory"); + propWithDefaultValueFactory.GetDefaultValue().ShouldBe(2); + + _objectExtensionManager + .GetPropertyOrNull("StringProp") + .DefaultValue.ShouldBeNull(); + + _objectExtensionManager + .GetPropertyOrNull("StringPropWithCustomDefaultValue") + .DefaultValue.ShouldBe("custom-value"); + } + private class MyExtensibleObject : ExtensibleObject { } + + private enum MyTestEnum + { + EnumValue1, + EnumValue2, + } } }