diff --git a/Tests/DbLocalizationProvider.EPiServer.Sample/App_Data/EPiServerDB_3f986989.mdf b/Tests/DbLocalizationProvider.EPiServer.Sample/App_Data/EPiServerDB_3f986989.mdf index 3030e23e..6dbfcae5 100644 Binary files a/Tests/DbLocalizationProvider.EPiServer.Sample/App_Data/EPiServerDB_3f986989.mdf and b/Tests/DbLocalizationProvider.EPiServer.Sample/App_Data/EPiServerDB_3f986989.mdf differ diff --git a/Tests/DbLocalizationProvider.EPiServer.Sample/App_Data/EPiServerDB_3f986989_log.ldf b/Tests/DbLocalizationProvider.EPiServer.Sample/App_Data/EPiServerDB_3f986989_log.ldf index a152965e..c4c4a079 100644 Binary files a/Tests/DbLocalizationProvider.EPiServer.Sample/App_Data/EPiServerDB_3f986989_log.ldf and b/Tests/DbLocalizationProvider.EPiServer.Sample/App_Data/EPiServerDB_3f986989_log.ldf differ diff --git a/Tests/DbLocalizationProvider.MvcSample/App_Data/DbLocalizationProvider.MvcSample.mdf b/Tests/DbLocalizationProvider.MvcSample/App_Data/DbLocalizationProvider.MvcSample.mdf index 833a6c41..2a2c0c1e 100644 Binary files a/Tests/DbLocalizationProvider.MvcSample/App_Data/DbLocalizationProvider.MvcSample.mdf and b/Tests/DbLocalizationProvider.MvcSample/App_Data/DbLocalizationProvider.MvcSample.mdf differ diff --git a/Tests/DbLocalizationProvider.MvcSample/App_Data/DbLocalizationProvider.MvcSample_log.ldf b/Tests/DbLocalizationProvider.MvcSample/App_Data/DbLocalizationProvider.MvcSample_log.ldf index 72b16366..a3a6e69a 100644 Binary files a/Tests/DbLocalizationProvider.MvcSample/App_Data/DbLocalizationProvider.MvcSample_log.ldf and b/Tests/DbLocalizationProvider.MvcSample/App_Data/DbLocalizationProvider.MvcSample_log.ldf differ diff --git a/Tests/DbLocalizationProvider.MvcSample/DbLocalizationProvider.MvcSample.csproj b/Tests/DbLocalizationProvider.MvcSample/DbLocalizationProvider.MvcSample.csproj index 8a11ec7f..75f12a1f 100644 --- a/Tests/DbLocalizationProvider.MvcSample/DbLocalizationProvider.MvcSample.csproj +++ b/Tests/DbLocalizationProvider.MvcSample/DbLocalizationProvider.MvcSample.csproj @@ -134,6 +134,7 @@ + Web.config @@ -141,7 +142,9 @@ Web.config - + + + {8f0618d8-8200-45e9-9d1e-b268e98e0a84} diff --git a/Tests/DbLocalizationProvider.MvcSample/Models/HomeViewModel.cs b/Tests/DbLocalizationProvider.MvcSample/Models/HomeViewModel.cs index 54d5418c..2f060093 100644 --- a/Tests/DbLocalizationProvider.MvcSample/Models/HomeViewModel.cs +++ b/Tests/DbLocalizationProvider.MvcSample/Models/HomeViewModel.cs @@ -10,14 +10,20 @@ public class BaseViewModel [StringLength(100, MinimumLength = 5)] public string Message { get; set; } + [Display(Name = "Base username:", Description = "")] + [StringLength(100, MinimumLength = 5)] + [UIHint("Username")] + public string BaseUsername { get; set; } + public string CustomMessage { get; } = "Resource like property on base view model"; } [LocalizedModel(Inherited = false)] public class HomeViewModel : BaseViewModel { - [Display(Name = "The user name:")] + [Display(Name = "The user name:", Description = "")] [Required] + [UIHint("Username")] public string Username { get; set; } } } diff --git a/Tests/DbLocalizationProvider.MvcSample/Views/Home/Index.cshtml b/Tests/DbLocalizationProvider.MvcSample/Views/Home/Index.cshtml index 237c8fa8..e55eba9c 100644 --- a/Tests/DbLocalizationProvider.MvcSample/Views/Home/Index.cshtml +++ b/Tests/DbLocalizationProvider.MvcSample/Views/Home/Index.cshtml @@ -23,9 +23,10 @@ @Html.ValidationMessageFor(m => m.Message)
- @Html.LabelFor(m => m.Username) @Html.EditorFor(m => m.Username) - @Html.ValidationMessageFor(m => m.Username) +
+
+ @Html.EditorFor(m => m.BaseUsername)
diff --git a/Tests/DbLocalizationProvider.MvcSample/Views/Shared/EditorTemplates/Username.cshtml b/Tests/DbLocalizationProvider.MvcSample/Views/Shared/EditorTemplates/Username.cshtml new file mode 100644 index 00000000..8b68958b --- /dev/null +++ b/Tests/DbLocalizationProvider.MvcSample/Views/Shared/EditorTemplates/Username.cshtml @@ -0,0 +1,7 @@ +@model string + +@Html.LabelFor(m => m) +@Html.TextBoxFor(m => m) +@Html.ValidationMessageFor(m => m) + +[@ViewData.ModelMetadata.Description] \ No newline at end of file diff --git a/Tests/DbLocalizationProvider.Tests/DataAnnotations/BaseViewModel.cs b/Tests/DbLocalizationProvider.Tests/DataAnnotations/BaseViewModel.cs new file mode 100644 index 00000000..32761750 --- /dev/null +++ b/Tests/DbLocalizationProvider.Tests/DataAnnotations/BaseViewModel.cs @@ -0,0 +1,12 @@ +using System.ComponentModel.DataAnnotations; + +namespace DbLocalizationProvider.Tests.DataAnnotations +{ + [LocalizedModel] + public class BaseViewModel + { + [Display(Name = "Base property", Description = "")] + [Required] + public string BaseProperty { get; set; } + } +} diff --git a/Tests/DbLocalizationProvider.Tests/DataAnnotations/SampleViewModelWithBase.cs b/Tests/DbLocalizationProvider.Tests/DataAnnotations/SampleViewModelWithBase.cs new file mode 100644 index 00000000..a232d026 --- /dev/null +++ b/Tests/DbLocalizationProvider.Tests/DataAnnotations/SampleViewModelWithBase.cs @@ -0,0 +1,13 @@ +using System.ComponentModel.DataAnnotations; + +namespace DbLocalizationProvider.Tests.DataAnnotations +{ + [LocalizedModel(Inherited = false)] + public class SampleViewModelWithBase : BaseViewModel + { + [Display(Name = "Child property", Description = "")] + [Required] + [StringLength(100)] + public string ChildProperty { get; set; } + } +} diff --git a/Tests/DbLocalizationProvider.Tests/DataAnnotations/ViewModelWithInheritanceTests.cs b/Tests/DbLocalizationProvider.Tests/DataAnnotations/ViewModelWithInheritanceTests.cs new file mode 100644 index 00000000..8e8b3f80 --- /dev/null +++ b/Tests/DbLocalizationProvider.Tests/DataAnnotations/ViewModelWithInheritanceTests.cs @@ -0,0 +1,22 @@ +using System.Linq; +using DbLocalizationProvider.Sync; +using Xunit; + +namespace DbLocalizationProvider.Tests.DataAnnotations +{ + public class ViewModelWithInheritanceTests + { + [Fact] + public void NotInheritedModel_ContainsOnlyDeclaredProperties() + { + var sut = new TypeDiscoveryHelper(); + var properties = sut.ScanResources(typeof(SampleViewModelWithBase)); + var keys = properties.Select(p => p.Key).ToList(); + + Assert.Contains("DbLocalizationProvider.Tests.DataAnnotations.SampleViewModelWithBase.ChildProperty-Description", keys); + Assert.DoesNotContain("DbLocalizationProvider.Tests.DataAnnotations.SampleViewModelWithBase.BaseProperty", keys); + Assert.DoesNotContain("DbLocalizationProvider.Tests.DataAnnotations.SampleViewModelWithBase.BaseProperty-Required", keys); + Assert.DoesNotContain("DbLocalizationProvider.Tests.DataAnnotations.SampleViewModelWithBase.ChildProperty-Description-Required", keys); + } + } +} diff --git a/Tests/DbLocalizationProvider.Tests/DbLocalizationProvider.Tests.csproj b/Tests/DbLocalizationProvider.Tests/DbLocalizationProvider.Tests.csproj index 2a67679f..26284a2d 100644 --- a/Tests/DbLocalizationProvider.Tests/DbLocalizationProvider.Tests.csproj +++ b/Tests/DbLocalizationProvider.Tests/DbLocalizationProvider.Tests.csproj @@ -71,6 +71,9 @@ + + + @@ -107,6 +110,7 @@ + diff --git a/Tests/DbLocalizationProvider.Tests/DiscoveryTests/ViewModelWithIncludedOnlyTests.cs b/Tests/DbLocalizationProvider.Tests/DiscoveryTests/ViewModelWithIncludedOnlyTests.cs index 8cf246ca..ffc70cb4 100644 --- a/Tests/DbLocalizationProvider.Tests/DiscoveryTests/ViewModelWithIncludedOnlyTests.cs +++ b/Tests/DbLocalizationProvider.Tests/DiscoveryTests/ViewModelWithIncludedOnlyTests.cs @@ -6,12 +6,19 @@ namespace DbLocalizationProvider.Tests.DiscoveryTests { public class ViewModelWithIncludedOnlyTests { + public ViewModelWithIncludedOnlyTests() + { + _sut = new TypeDiscoveryHelper(); + } + + private readonly TypeDiscoveryHelper _sut; + [Fact] public void ModelWithBase_IncludedPorperty_ShouldDiscoverOnlyExplicitProperties() { - var properties = TypeDiscoveryHelper.GetAllProperties(typeof(SampleViewModelWithIncludedOnlyWithBase), contextAwareScanning: false) - .Select(p => p.Key) - .ToList(); + var properties = _sut.ScanResources(typeof(SampleViewModelWithIncludedOnlyWithBase)) + .Select(p => p.Key) + .ToList(); Assert.Contains("DbLocalizationProvider.Tests.DiscoveryTests.SampleViewModelWithIncludedOnlyWithBase.IncludedProperty", properties); Assert.DoesNotContain("DbLocalizationProvider.Tests.DiscoveryTests.SampleViewModelWithIncludedOnlyWithBase.ExcludedProperty", properties); @@ -23,9 +30,9 @@ public void ModelWithBase_IncludedPorperty_ShouldDiscoverOnlyExplicitProperties( [Fact] public void ModelWithIncludedPorperty_ShouldDiscoverOnlyExplicitProperties() { - var properties = TypeDiscoveryHelper.GetAllProperties(typeof(SampleViewModelWithIncludedOnly), contextAwareScanning: false) - .Select(p => p.Key) - .ToList(); + var properties = _sut.ScanResources(typeof(SampleViewModelWithIncludedOnly)) + .Select(p => p.Key) + .ToList(); Assert.Contains("DbLocalizationProvider.Tests.DiscoveryTests.SampleViewModelWithIncludedOnly.IncludedProperty", properties); Assert.DoesNotContain("DbLocalizationProvider.Tests.DiscoveryTests.SampleViewModelWithIncludedOnly.ExcludedProperty", properties); diff --git a/Tests/DbLocalizationProvider.Tests/GenericModels/GenericModelTests.cs b/Tests/DbLocalizationProvider.Tests/GenericModels/GenericModelTests.cs index c5719119..ee7aad81 100644 --- a/Tests/DbLocalizationProvider.Tests/GenericModels/GenericModelTests.cs +++ b/Tests/DbLocalizationProvider.Tests/GenericModels/GenericModelTests.cs @@ -5,10 +5,17 @@ namespace DbLocalizationProvider.Tests.GenericModels { public class GenericModelTests { + public GenericModelTests() + { + _sut = new TypeDiscoveryHelper(); + } + + private readonly TypeDiscoveryHelper _sut; + [Fact] public void TestGenericProperty() { - var properties = TypeDiscoveryHelper.GetAllProperties(typeof(OpenGenericModel<>), contextAwareScanning: false); + var properties = _sut.ScanResources(typeof(OpenGenericModel<>)); Assert.NotEmpty(properties); } @@ -16,9 +23,9 @@ public void TestGenericProperty() [Fact] public void TestGenericProperty_FromChildClass() { - var properties = TypeDiscoveryHelper.GetAllProperties(typeof(ClosedGenericModel), contextAwareScanning: false); + var properties = _sut.ScanResources(typeof(ClosedGenericModel)); Assert.NotEmpty(properties); } } -} \ No newline at end of file +} diff --git a/Tests/DbLocalizationProvider.Tests/InheritedModels/InheritedViewModelExpressionTests.cs b/Tests/DbLocalizationProvider.Tests/InheritedModels/InheritedViewModelExpressionTests.cs index 71a6e6eb..6187dd85 100644 --- a/Tests/DbLocalizationProvider.Tests/InheritedModels/InheritedViewModelExpressionTests.cs +++ b/Tests/DbLocalizationProvider.Tests/InheritedModels/InheritedViewModelExpressionTests.cs @@ -10,10 +10,11 @@ public class InheritedViewModelExpressionTests [Fact] public void Test() { - var properties = - new[] { typeof(SampleViewModelWithBaseNotInherit), typeof(BaseLocalizedViewModel) } - .Select(t => TypeDiscoveryHelper.GetAllProperties(t, contextAwareScanning: false)) - .ToList(); + var sut = new TypeDiscoveryHelper(); + + var properties = new[] { typeof(SampleViewModelWithBaseNotInherit), typeof(BaseLocalizedViewModel) } + .Select(t => sut.ScanResources(t)) + .ToList(); var childModel = new SampleViewModelWithBaseNotInherit(); var basePropertyKey = ExpressionHelper.GetFullMemberName(() => childModel.BaseProperty); diff --git a/Tests/DbLocalizationProvider.Tests/InheritedModels/ViewModelWithBaseTests.cs b/Tests/DbLocalizationProvider.Tests/InheritedModels/ViewModelWithBaseTests.cs index f4bdc3be..a364462a 100644 --- a/Tests/DbLocalizationProvider.Tests/InheritedModels/ViewModelWithBaseTests.cs +++ b/Tests/DbLocalizationProvider.Tests/InheritedModels/ViewModelWithBaseTests.cs @@ -9,12 +9,19 @@ namespace DbLocalizationProvider.Tests.InheritedModels { public class ViewModelWithBaseTests { + public ViewModelWithBaseTests() + { + _sut = new TypeDiscoveryHelper(); + } + + private readonly TypeDiscoveryHelper _sut; + [Fact] public void BaseProperty_HasChildClassResourceKey() { - var properties = TypeDiscoveryHelper.GetAllProperties(typeof(SampleViewModelWithBase), contextAwareScanning: false) - .Select(p => p.Key) - .ToList(); + var properties = _sut.ScanResources(typeof(SampleViewModelWithBase)) + .Select(p => p.Key) + .ToList(); Assert.Contains("DbLocalizationProvider.Tests.InheritedModels.SampleViewModelWithBase.BaseProperty", properties); Assert.Contains("DbLocalizationProvider.Tests.InheritedModels.SampleViewModelWithBase.BaseProperty-Required", properties); @@ -23,9 +30,9 @@ public void BaseProperty_HasChildClassResourceKey() [Fact] public void BaseProperty_HasChildClassResourceKey_DoesNotIncludeInheritedProperties() { - var properties = TypeDiscoveryHelper.GetAllProperties(typeof(SampleViewModelWithBaseNotInherit), contextAwareScanning: false) - .Select(p => p.Key) - .ToList(); + var properties = _sut.ScanResources(typeof(SampleViewModelWithBaseNotInherit)) + .Select(p => p.Key) + .ToList(); Assert.Contains("DbLocalizationProvider.Tests.InheritedModels.SampleViewModelWithBaseNotInherit.ChildProperty", properties); Assert.DoesNotContain("DbLocalizationProvider.Tests.InheritedModels.SampleViewModelWithBaseNotInherit.BaseProperty", properties); @@ -36,7 +43,7 @@ public void BuildResourceKey_ForBaseClassProperty_ExcludedFromChild_ShouldReturn { var properties = new[] { typeof(SampleViewModelWithBaseNotInherit), typeof(BaseLocalizedViewModel) } - .Select(t => TypeDiscoveryHelper.GetAllProperties(t, contextAwareScanning: false)) + .Select(t => _sut.ScanResources(t)) .ToList(); var childPropertyKey = ResourceKeyBuilder.BuildResourceKey(typeof(SampleViewModelWithBaseNotInherit), "ChildProperty"); @@ -53,7 +60,7 @@ public void BuildResourceKey_ForSecondBaseClassProperty_ExcludedFromChild_Should { var properties = new[] { typeof(SampleViewModelWithBaseNotInherit), typeof(BaseLocalizedViewModel), typeof(VeryBaseLocalizedViewModel) } - .Select(t => TypeDiscoveryHelper.GetAllProperties(t, contextAwareScanning: false)) + .Select(t => _sut.ScanResources(t)) .ToList(); var veryBasePropertyKey = ResourceKeyBuilder.BuildResourceKey(typeof(SampleViewModelWithBaseNotInherit), "VeryBaseProperty"); diff --git a/Tests/DbLocalizationProvider.Tests/LocalizedEnumTests.cs b/Tests/DbLocalizationProvider.Tests/LocalizedEnumTests.cs index d2b06f83..ec8b4990 100644 --- a/Tests/DbLocalizationProvider.Tests/LocalizedEnumTests.cs +++ b/Tests/DbLocalizationProvider.Tests/LocalizedEnumTests.cs @@ -10,10 +10,11 @@ public class LocalizedEnumTests public LocalizedEnumTests() { var types = new[] { typeof(DocumentEntity) }; + var sut = new TypeDiscoveryHelper(); Assert.NotEmpty(types); - _properties = types.SelectMany(t => TypeDiscoveryHelper.GetAllProperties(t, contextAwareScanning: false)); + _properties = types.SelectMany(t => sut.ScanResources(t)); } private readonly IEnumerable _properties; diff --git a/Tests/DbLocalizationProvider.Tests/LocalizedEnumsDiscoveryTests.cs b/Tests/DbLocalizationProvider.Tests/LocalizedEnumsDiscoveryTests.cs index 03aeb7d9..b4dea3ff 100644 --- a/Tests/DbLocalizationProvider.Tests/LocalizedEnumsDiscoveryTests.cs +++ b/Tests/DbLocalizationProvider.Tests/LocalizedEnumsDiscoveryTests.cs @@ -19,8 +19,9 @@ public LocalizedEnumsDiscoveryTests() [Fact] public void DiscoverEnumValue_NameAsTranslation() { + var sut = new TypeDiscoveryHelper(); var type = _types.First(t => t.FullName == "DbLocalizationProvider.Tests.SampleStatus"); - var properties = TypeDiscoveryHelper.GetAllProperties(type); + var properties = sut.ScanResources(type); var openStatus = properties.First(p => p.Key == "DbLocalizationProvider.Tests.SampleStatus.Open"); diff --git a/Tests/DbLocalizationProvider.Tests/LocalizedModelsDiscoveryTests.cs b/Tests/DbLocalizationProvider.Tests/LocalizedModelsDiscoveryTests.cs index 64b0cc5a..ea472ef8 100644 --- a/Tests/DbLocalizationProvider.Tests/LocalizedModelsDiscoveryTests.cs +++ b/Tests/DbLocalizationProvider.Tests/LocalizedModelsDiscoveryTests.cs @@ -7,15 +7,26 @@ namespace DbLocalizationProvider.Tests { public class LocalizedModelsDiscoveryTests { - private readonly IEnumerable _properties; - public LocalizedModelsDiscoveryTests() { - var types = new[] { typeof(SampleViewModel), typeof(SubViewModel) };//TypeDiscoveryHelper.GetTypesWithAttribute().ToList(); + var types = new[] { typeof(SampleViewModel), typeof(SubViewModel) }; + var sut = new TypeDiscoveryHelper(); Assert.NotEmpty(types); - _properties = types.SelectMany(t => TypeDiscoveryHelper.GetAllProperties(t, contextAwareScanning: false)); + _properties = types.SelectMany(t => sut.ScanResources(t)); + } + + private readonly IEnumerable _properties; + + [Fact] + public void PropertyWithAttributes_DisplayDescription_Discovered() + { + var resource = _properties.FirstOrDefault(p => p.Key == "DbLocalizationProvider.Tests.SampleViewModel.PropertyWithDescription"); + Assert.NotNull(resource); + + var propertyWithDescriptionResource = _properties.FirstOrDefault(p => p.Key == "DbLocalizationProvider.Tests.SampleViewModel.PropertyWithDescription-Description"); + Assert.NotNull(propertyWithDescriptionResource); } [Fact] @@ -52,16 +63,5 @@ public void SingleLevel_ScalarProperties_NoAttribute() var nullable = _properties.FirstOrDefault(p => p.Key == "DbLocalizationProvider.Tests.SampleViewModel.NullableInt"); Assert.NotNull(nullable); } - - - [Fact] - public void PropertyWithAttributes_DisplayDescription_Discovered() - { - var resource = _properties.FirstOrDefault(p => p.Key == "DbLocalizationProvider.Tests.SampleViewModel.PropertyWithDescription"); - Assert.NotNull(resource); - - var propertyWithDescriptionResource = _properties.FirstOrDefault(p => p.Key == "DbLocalizationProvider.Tests.SampleViewModel.PropertyWithDescription-Description"); - Assert.NotNull(propertyWithDescriptionResource); - } } } diff --git a/Tests/DbLocalizationProvider.Tests/LocalizedResourceDiscoveryTests.cs b/Tests/DbLocalizationProvider.Tests/LocalizedResourceDiscoveryTests.cs index ee3878e2..bd0a68c2 100644 --- a/Tests/DbLocalizationProvider.Tests/LocalizedResourceDiscoveryTests.cs +++ b/Tests/DbLocalizationProvider.Tests/LocalizedResourceDiscoveryTests.cs @@ -9,31 +9,21 @@ namespace DbLocalizationProvider.Tests { public class LocalizedResourceDiscoveryTests { - private readonly List _types; - public LocalizedResourceDiscoveryTests() { + _sut = new TypeDiscoveryHelper(); _types = TypeDiscoveryHelper.GetTypesWithAttribute().ToList(); Assert.NotEmpty(_types); } - [Fact] - public void SingleLevel_ScalarProperties() - { - var type = _types.First(t => t.FullName == "DbLocalizationProvider.Tests.ResourceKeys"); - var properties = TypeDiscoveryHelper.GetAllProperties(type); - - var staticField = properties.First(p => p.Key == "DbLocalizationProvider.Tests.ResourceKeys.ThisIsConstant"); - - Assert.True(TypeDiscoveryHelper.IsStringProperty(staticField.ReturnType)); - Assert.Equal("Default value for constant", staticField.Translation); - } + private readonly List _types; + private readonly TypeDiscoveryHelper _sut; [Fact] public void NestedObject_ScalarProperties() { var type = _types.First(t => t.FullName == "DbLocalizationProvider.Tests.ResourceKeys"); - var properties = TypeDiscoveryHelper.GetAllProperties(type).ToList(); + var properties = _sut.ScanResources(type).ToList(); var complexPropertySubProperty = properties.FirstOrDefault(p => p.Key == "DbLocalizationProvider.Tests.ResourceKeys.SubResource.SubResourceProperty"); @@ -46,7 +36,19 @@ public void NestedObject_ScalarProperties() // need to check that there is no resource discovered for complex properties itself Assert.DoesNotContain("DbLocalizationProvider.Tests.ResourceKeys.SubResource", properties.Select(k => k.Key)); Assert.DoesNotContain("DbLocalizationProvider.Tests.ResourceKeys.SubResource.EvenMoreComplexResource", properties.Select(k => k.Key)); + } + [Fact] + public void NestedType_ScalarProperties() + { + var type = _types.FirstOrDefault(t => t.FullName == "DbLocalizationProvider.Tests.ParentClassForResources+ChildResourceClass"); + + Assert.NotNull(type); + + var property = _sut.ScanResources(type).First(); + var resourceKey = ExpressionHelper.GetFullMemberName(() => ParentClassForResources.ChildResourceClass.HelloMessage); + + Assert.Equal(resourceKey, property.Key); } [Fact] @@ -56,22 +58,22 @@ public void NestedType_ThroughProperty_ScalarProperties() Assert.NotNull(type); - var property = TypeDiscoveryHelper.GetAllProperties(type).FirstOrDefault(p => p.Key == "DbLocalizationProvider.Tests.PageResources.Header.HelloMessage"); + var property = _sut.ScanResources(type).FirstOrDefault(p => p.Key == "DbLocalizationProvider.Tests.PageResources.Header.HelloMessage"); Assert.NotNull(property); } [Fact] - public void NestedType_ScalarProperties() + public void SingleLevel_ScalarProperties() { - var type = _types.FirstOrDefault(t => t.FullName == "DbLocalizationProvider.Tests.ParentClassForResources+ChildResourceClass"); - - Assert.NotNull(type); + var sut = new TypeDiscoveryHelper(); + var type = _types.First(t => t.FullName == "DbLocalizationProvider.Tests.ResourceKeys"); + var properties = sut.ScanResources(type); - var property = TypeDiscoveryHelper.GetAllProperties(type).First(); - var resourceKey = ExpressionHelper.GetFullMemberName(() => ParentClassForResources.ChildResourceClass.HelloMessage); + var staticField = properties.First(p => p.Key == "DbLocalizationProvider.Tests.ResourceKeys.ThisIsConstant"); - Assert.Equal(resourceKey, property.Key); + Assert.True(LocalizedTypeScannerBase.IsStringProperty(staticField.ReturnType)); + Assert.Equal("Default value for constant", staticField.Translation); } } } diff --git a/Tests/DbLocalizationProvider.Tests/NamedResources/NamedModelsTests.cs b/Tests/DbLocalizationProvider.Tests/NamedResources/NamedModelsTests.cs index e72a65fb..a61f1641 100644 --- a/Tests/DbLocalizationProvider.Tests/NamedResources/NamedModelsTests.cs +++ b/Tests/DbLocalizationProvider.Tests/NamedResources/NamedModelsTests.cs @@ -6,45 +6,25 @@ namespace DbLocalizationProvider.Tests.NamedResources { public class NamedModelsTests { - [Fact] - public void MultipleAttributesForSingleProperty_NoPrefix() + public NamedModelsTests() { - var model = TypeDiscoveryHelper.GetTypesWithAttribute() - .Where(t => t.FullName == $"DbLocalizationProvider.Tests.NamedResources.{nameof(ModelWithNamedProperties)}"); - - var properties = model.SelectMany(t => TypeDiscoveryHelper.GetAllProperties(t, contextAwareScanning: false)).ToList(); - - var nonexistingProperty = properties.FirstOrDefault(p => p.Key == "DbLocalizationProvider.Tests.NamedResources.ModelWithNamedProperties.PageHeader"); - Assert.Null(nonexistingProperty); - - var namedProperty = properties.FirstOrDefault(p => p.Key == "/this/is/xpath/key"); - Assert.NotNull(namedProperty); - Assert.Equal("This is page header", namedProperty.Translation); - - var anotherNamedProperty = properties.FirstOrDefault(p => p.Key == "/this/is/another/xpath/key"); - Assert.NotNull(anotherNamedProperty); + _sut = new TypeDiscoveryHelper(); + } - var resourceKeyOnComplexProperty = properties.FirstOrDefault(p => p.Key == "/this/is/complex/type"); - Assert.NotNull(resourceKeyOnComplexProperty); + private readonly TypeDiscoveryHelper _sut; - var propertyWithDisplayName = properties.FirstOrDefault(p => p.Key == "/simple/property/with/display/name"); - Assert.NotNull(propertyWithDisplayName); - Assert.Equal("This is simple property", propertyWithDisplayName.Translation); + [Fact] + public void DuplicateAttributes_DiffProperties_SameKey_ThrowsException() + { + var model = new[] { typeof(BadResourceWithDuplicateKeysWithinClass) }; + Assert.Throws(() => model.SelectMany(t => _sut.ScanResources(t)).ToList()); } [Fact] - public void SingleAttributeForSingleProperty_WithClassPrefix() + public void DuplicateAttributes_SingleProperty_SameKey_ThrowsException() { - var model = TypeDiscoveryHelper.GetTypesWithAttribute() - .Where(t => t.FullName == $"DbLocalizationProvider.Tests.NamedResources.{nameof(ModelWithNamedPropertiesWithPrefix)}"); - - var properties = model.SelectMany(t => TypeDiscoveryHelper.GetAllProperties(t, contextAwareScanning: false)).ToList(); - - var name = "/contenttypes/modelwithnamedpropertieswithprefix/properties/pageheader/name"; - var headerProperty = properties.FirstOrDefault(p => p.Key == name); - - Assert.NotNull(headerProperty); - Assert.Equal("This is page header", headerProperty.Translation); + var model = new[] { typeof(ModelWithDuplicateResourceKeys) }; + Assert.Throws(() => model.SelectMany(t => _sut.ScanResources(t)).ToList()); } [Fact] @@ -53,7 +33,7 @@ public void MultipleAttributeForSingleProperty_WithClassPrefix() var model = TypeDiscoveryHelper.GetTypesWithAttribute() .Where(t => t.FullName == $"DbLocalizationProvider.Tests.NamedResources.{nameof(ModelWithNamedPropertiesWithPrefix)}"); - var properties = model.SelectMany(t => TypeDiscoveryHelper.GetAllProperties(t, contextAwareScanning: false)).ToList(); + var properties = model.SelectMany(t => _sut.ScanResources(t)).ToList(); var firstResource = properties.FirstOrDefault(p => p.Key == "/contenttypes/modelwithnamedpropertieswithprefix/resource1"); @@ -66,13 +46,39 @@ public void MultipleAttributeForSingleProperty_WithClassPrefix() Assert.Equal("2nd resource", secondResource.Translation); } + [Fact] + public void MultipleAttributesForSingleProperty_NoPrefix() + { + var model = TypeDiscoveryHelper.GetTypesWithAttribute() + .Where(t => t.FullName == $"DbLocalizationProvider.Tests.NamedResources.{nameof(ModelWithNamedProperties)}"); + + var properties = model.SelectMany(t => _sut.ScanResources(t)).ToList(); + + var nonexistingProperty = properties.FirstOrDefault(p => p.Key == "DbLocalizationProvider.Tests.NamedResources.ModelWithNamedProperties.PageHeader"); + Assert.Null(nonexistingProperty); + + var namedProperty = properties.FirstOrDefault(p => p.Key == "/this/is/xpath/key"); + Assert.NotNull(namedProperty); + Assert.Equal("This is page header", namedProperty.Translation); + + var anotherNamedProperty = properties.FirstOrDefault(p => p.Key == "/this/is/another/xpath/key"); + Assert.NotNull(anotherNamedProperty); + + var resourceKeyOnComplexProperty = properties.FirstOrDefault(p => p.Key == "/this/is/complex/type"); + Assert.NotNull(resourceKeyOnComplexProperty); + + var propertyWithDisplayName = properties.FirstOrDefault(p => p.Key == "/simple/property/with/display/name"); + Assert.NotNull(propertyWithDisplayName); + Assert.Equal("This is simple property", propertyWithDisplayName.Translation); + } + [Fact] public void ResourceAttributeToClass_WithClassPrefix() { var model = TypeDiscoveryHelper.GetTypesWithAttribute() .Where(t => t.FullName == $"DbLocalizationProvider.Tests.NamedResources.{nameof(ModelWithNamedPropertiesWithPrefixAndKeyOnClass)}"); - var properties = model.SelectMany(t => TypeDiscoveryHelper.GetAllProperties(t, contextAwareScanning: false)).ToList(); + var properties = model.SelectMany(t => _sut.ScanResources(t)).ToList(); var firstResource = properties.FirstOrDefault(p => p.Key == "/contenttypes/modelwithnamedpropertieswithprefixandkeyonclass/name"); Assert.NotNull(firstResource); @@ -85,17 +91,18 @@ public void ResourceAttributeToClass_WithClassPrefix() } [Fact] - public void DuplicateAttributes_SingleProperty_SameKey_ThrowsException() + public void SingleAttributeForSingleProperty_WithClassPrefix() { - var model = new[] { typeof(ModelWithDuplicateResourceKeys) }; - Assert.Throws(() => model.SelectMany(t => TypeDiscoveryHelper.GetAllProperties(t, contextAwareScanning: false)).ToList()); - } + var model = TypeDiscoveryHelper.GetTypesWithAttribute() + .Where(t => t.FullName == $"DbLocalizationProvider.Tests.NamedResources.{nameof(ModelWithNamedPropertiesWithPrefix)}"); - [Fact] - public void DuplicateAttributes_DiffProperties_SameKey_ThrowsException() - { - var model = new[] { typeof(BadResourceWithDuplicateKeysWithinClass) }; - Assert.Throws(() => model.SelectMany(t => TypeDiscoveryHelper.GetAllProperties(t, contextAwareScanning: false)).ToList()); + var properties = model.SelectMany(t => _sut.ScanResources(t)).ToList(); + + var name = "/contenttypes/modelwithnamedpropertieswithprefix/properties/pageheader/name"; + var headerProperty = properties.FirstOrDefault(p => p.Key == name); + + Assert.NotNull(headerProperty); + Assert.Equal("This is page header", headerProperty.Translation); } } } diff --git a/Tests/DbLocalizationProvider.Tests/NamedResources/NamedResourcesTests.cs b/Tests/DbLocalizationProvider.Tests/NamedResources/NamedResourcesTests.cs index 797b2e78..dcfac3e0 100644 --- a/Tests/DbLocalizationProvider.Tests/NamedResources/NamedResourcesTests.cs +++ b/Tests/DbLocalizationProvider.Tests/NamedResources/NamedResourcesTests.cs @@ -7,18 +7,25 @@ namespace DbLocalizationProvider.Tests.NamedResources { public class NamedResourcesTests { + public NamedResourcesTests() + { + _sut = new TypeDiscoveryHelper(); + } + + private readonly TypeDiscoveryHelper _sut; + [Fact] public void DuplicateAttributes_DiffProperties_SameKey_ThrowsException() { var model = new[] { typeof(BadResourceWithDuplicateKeysWithinClass) }; - Assert.Throws(() => model.SelectMany(t => TypeDiscoveryHelper.GetAllProperties(t)).ToList()); + Assert.Throws(() => model.SelectMany(t => _sut.ScanResources(t)).ToList()); } [Fact] public void DuplicateAttributes_SingleProperty_SameKey_ThrowsException() { var model = new[] { typeof(BadResourceWithDuplicateKeys) }; - Assert.Throws(() => model.SelectMany(t => TypeDiscoveryHelper.GetAllProperties(t)).ToList()); + Assert.Throws(() => model.SelectMany(t => _sut.ScanResources(t)).ToList()); } [Fact] @@ -43,7 +50,7 @@ public void MultipleAttributesForSingleProperty_NoPrefix() var model = TypeDiscoveryHelper.GetTypesWithAttribute() .Where(t => t.FullName == $"DbLocalizationProvider.Tests.NamedResources.{nameof(ResourcesWithNamedKeys)}"); - var properties = model.SelectMany(t => TypeDiscoveryHelper.GetAllProperties(t)).ToList(); + var properties = model.SelectMany(t => _sut.ScanResources(t)).ToList(); var namedResource = properties.FirstOrDefault(p => p.Key == "/this/is/xpath/to/resource"); @@ -57,7 +64,7 @@ public void MultipleAttributesForSingleProperty_WithPrefix() var model = TypeDiscoveryHelper.GetTypesWithAttribute() .Where(t => t.FullName == $"DbLocalizationProvider.Tests.NamedResources.{nameof(ResourcesWithNamedKeysWithPrefix)}"); - var properties = model.SelectMany(t => TypeDiscoveryHelper.GetAllProperties(t)).ToList(); + var properties = model.SelectMany(t => _sut.ScanResources(t)).ToList(); var namedResource = properties.FirstOrDefault(p => p.Key == "/this/is/root/resource/and/this/is/header"); diff --git a/Tests/DbLocalizationProvider.Tests/ScannerTests/TypeScannerTests.cs b/Tests/DbLocalizationProvider.Tests/ScannerTests/TypeScannerTests.cs new file mode 100644 index 00000000..1392a797 --- /dev/null +++ b/Tests/DbLocalizationProvider.Tests/ScannerTests/TypeScannerTests.cs @@ -0,0 +1,18 @@ +using DbLocalizationProvider.Sync; +using Xunit; + +namespace DbLocalizationProvider.Tests.ScannerTests +{ + public class TypeScannerTests + { + [Fact] + public void ViewModelType_ShouldSelectModelScanner() + { + var sut = new LocalizedModelTypeScanner(); + + var result = sut.ShouldScan(typeof(SampleViewModel)); + + Assert.True(result); + } + } +} diff --git a/src/DbLocalizationProvider/DataAnnotations/LocalizedMetadataProvider.cs b/src/DbLocalizationProvider/DataAnnotations/LocalizedMetadataProvider.cs index d0dc3208..888dc06b 100644 --- a/src/DbLocalizationProvider/DataAnnotations/LocalizedMetadataProvider.cs +++ b/src/DbLocalizationProvider/DataAnnotations/LocalizedMetadataProvider.cs @@ -41,7 +41,7 @@ protected override ModelMetadata CreateMetadata( } var displayAttribute = theAttributes.OfType().FirstOrDefault(); - if(!string.IsNullOrEmpty(displayAttribute?.Description)) + if(displayAttribute?.Description != null) { data.Description = ModelMetadataLocalizationHelper.GetTranslation(containerType, $"{propertyName}-Description"); } diff --git a/src/DbLocalizationProvider/DbLocalizationProvider.csproj b/src/DbLocalizationProvider/DbLocalizationProvider.csproj index 0166e516..73b1dad1 100644 --- a/src/DbLocalizationProvider/DbLocalizationProvider.csproj +++ b/src/DbLocalizationProvider/DbLocalizationProvider.csproj @@ -150,6 +150,10 @@ + + + + diff --git a/src/DbLocalizationProvider/Properties/AssemblyInfo.cs b/src/DbLocalizationProvider/Properties/AssemblyInfo.cs index be764a5e..8ffc9983 100644 --- a/src/DbLocalizationProvider/Properties/AssemblyInfo.cs +++ b/src/DbLocalizationProvider/Properties/AssemblyInfo.cs @@ -5,9 +5,9 @@ [assembly: AssemblyTitle("DbLocalizationProvider")] [assembly: AssemblyDescription("Database driven localization provider")] [assembly: Guid("17ca5d23-46c3-44b1-8fa6-0f40b2e447ba")] -[assembly: AssemblyVersion("2.1.0.0")] -[assembly: AssemblyFileVersion("2.1.0.0")] -[assembly: AssemblyInformationalVersion("2.1.0")] +[assembly: AssemblyVersion("2.1.1.0")] +[assembly: AssemblyFileVersion("2.1.1.0")] +[assembly: AssemblyInformationalVersion("2.1.1")] [assembly: InternalsVisibleTo("DbLocalizationProvider.EPiServer")] [assembly: InternalsVisibleTo("DbLocalizationProvider.AdminUI")] [assembly: InternalsVisibleTo("DbLocalizationProvider.MigrationTool")] diff --git a/src/DbLocalizationProvider/Sync/IResourceTypeScanner.cs b/src/DbLocalizationProvider/Sync/IResourceTypeScanner.cs new file mode 100644 index 00000000..92155244 --- /dev/null +++ b/src/DbLocalizationProvider/Sync/IResourceTypeScanner.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; + +namespace DbLocalizationProvider.Sync +{ + internal interface IResourceTypeScanner + { + bool ShouldScan(Type target); + string GetResourceKeyPrefix(Type target, string keyPrefix = null); + ICollection GetClassLevelResources(Type target, string resourceKeyPrefix); + ICollection GetResources(Type target, string resourceKeyPrefix); + } +} \ No newline at end of file diff --git a/src/DbLocalizationProvider/Sync/LocalizedModelTypeScanner.cs b/src/DbLocalizationProvider/Sync/LocalizedModelTypeScanner.cs new file mode 100644 index 00000000..4bbf4ab7 --- /dev/null +++ b/src/DbLocalizationProvider/Sync/LocalizedModelTypeScanner.cs @@ -0,0 +1,69 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using DbLocalizationProvider.Internal; + +namespace DbLocalizationProvider.Sync +{ + internal class LocalizedModelTypeScanner : LocalizedTypeScannerBase, IResourceTypeScanner + { + public bool ShouldScan(Type target) + { + return target.GetCustomAttribute() != null; + } + + public string GetResourceKeyPrefix(Type target, string keyPrefix = null) + { + var modelAttribute = target.GetCustomAttribute(); + + return !string.IsNullOrEmpty(modelAttribute?.KeyPrefix) ? modelAttribute.KeyPrefix : target.FullName; + } + + public ICollection GetClassLevelResources(Type target, string resourceKeyPrefix) + { + var result = new List(); + var resourceAttributesOnModelClass = target.GetCustomAttributes().ToList(); + if(!resourceAttributesOnModelClass.Any()) + return result; + + foreach (var resourceKeyAttribute in resourceAttributesOnModelClass) + { + result.Add(new DiscoveredResource(null, + ResourceKeyBuilder.BuildResourceKey(resourceKeyPrefix, resourceKeyAttribute.Key, separator: string.Empty), + null, + resourceKeyAttribute.Value, + target, + typeof(string), + true)); + } + + return result; + } + + public ICollection GetResources(Type target, string resourceKeyPrefix) + { + var resourceSources = GetResourceSources(target); + var attr = target.GetCustomAttribute(); + var isKeyPrefixSpecified = !string.IsNullOrEmpty(attr?.KeyPrefix); + + return resourceSources.SelectMany(pi => DiscoverResourcesFromProperty(pi, resourceKeyPrefix, isKeyPrefixSpecified)).ToList(); + } + + private ICollection GetResourceSources(Type target) + { + var modelAttribute = target.GetCustomAttribute(); + if(modelAttribute == null) + return new List(); + + var flags = BindingFlags.Public | BindingFlags.GetProperty | BindingFlags.Instance | BindingFlags.Static; + + if(!modelAttribute.Inherited) + flags = flags | BindingFlags.DeclaredOnly; + + return target.GetProperties(flags) + .Where(pi => pi.GetCustomAttribute() == null) + .Where(pi => !modelAttribute.OnlyIncluded || pi.GetCustomAttribute() != null).ToList(); + } + } +} \ No newline at end of file diff --git a/src/DbLocalizationProvider/Sync/LocalizedResourceTypeScanner.cs b/src/DbLocalizationProvider/Sync/LocalizedResourceTypeScanner.cs new file mode 100644 index 00000000..3384e1c1 --- /dev/null +++ b/src/DbLocalizationProvider/Sync/LocalizedResourceTypeScanner.cs @@ -0,0 +1,59 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using DbLocalizationProvider.Internal; + +namespace DbLocalizationProvider.Sync +{ + internal class LocalizedResourceTypeScanner : LocalizedTypeScannerBase, IResourceTypeScanner + { + public bool ShouldScan(Type target) + { + return target.GetCustomAttribute() != null; + } + + public string GetResourceKeyPrefix(Type target, string keyPrefix = null) + { + var resourceAttribute = target.GetCustomAttribute(); + + return !string.IsNullOrEmpty(resourceAttribute?.KeyPrefix) + ? resourceAttribute.KeyPrefix + : (string.IsNullOrEmpty(keyPrefix) ? target.FullName : keyPrefix); + } + + public ICollection GetClassLevelResources(Type target, string resourceKeyPrefix) + { + return new List(); + } + + public ICollection GetResources(Type target, string resourceKeyPrefix) + { + if(target.BaseType == typeof(Enum)) + { + return target.GetMembers(BindingFlags.Public | BindingFlags.Static) + .Select(mi => new DiscoveredResource(mi, + ResourceKeyBuilder.BuildResourceKey(resourceKeyPrefix, mi), + mi.Name, + mi.Name, + target, + Enum.GetUnderlyingType(target), + Enum.GetUnderlyingType(target).IsSimpleType())).ToList(); + } + + var resourceSources = GetResourceSources(target); + var attr = target.GetCustomAttribute(); + var isKeyPrefixSpecified = !string.IsNullOrEmpty(attr?.KeyPrefix); + + return resourceSources.SelectMany(pi => DiscoverResourcesFromProperty(pi, resourceKeyPrefix, isKeyPrefixSpecified)).ToList(); + } + + private ICollection GetResourceSources(Type target) + { + var flags = BindingFlags.Public | BindingFlags.GetProperty | BindingFlags.Instance | BindingFlags.Static; + + return target.GetProperties(flags) + .Where(pi => pi.GetCustomAttribute() == null).ToList(); + } + } +} \ No newline at end of file diff --git a/src/DbLocalizationProvider/Sync/LocalizedTypeScannerBase.cs b/src/DbLocalizationProvider/Sync/LocalizedTypeScannerBase.cs new file mode 100644 index 00000000..160ce2ea --- /dev/null +++ b/src/DbLocalizationProvider/Sync/LocalizedTypeScannerBase.cs @@ -0,0 +1,127 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Reflection; +using DbLocalizationProvider.Internal; + +namespace DbLocalizationProvider.Sync +{ + internal abstract class LocalizedTypeScannerBase + { + protected IEnumerable DiscoverResourcesFromProperty(PropertyInfo pi, string resourceKeyPrefix, bool typeKeyPrefixSpecified) + { + // check if there are [ResourceKey] attributes + var keyAttributes = pi.GetCustomAttributes().ToList(); + var resourceKey = ResourceKeyBuilder.BuildResourceKey(resourceKeyPrefix, pi); + var translation = GetResourceValue(pi); + + if(!keyAttributes.Any()) + { + yield return new DiscoveredResource(pi, + resourceKey, + translation, + pi.Name, + pi.PropertyType, + pi.GetMethod.ReturnType, + pi.GetMethod.ReturnType.IsSimpleType()); + + // try to fetch also [Display()] attribute to generate new "...-Description" resource => usually used for help text labels + var displayAttribute = pi.GetCustomAttribute(); + if(displayAttribute?.Description != null) + { + yield return new DiscoveredResource(pi, + $"{resourceKey}-Description", + displayAttribute.Description, + $"{pi.Name}-Description", + pi.PropertyType, + pi.GetMethod.ReturnType, + pi.GetMethod.ReturnType.IsSimpleType()); + } + + var validationAttributes = pi.GetCustomAttributes(); + foreach (var validationAttribute in validationAttributes) + { + var validationResourceKey = ResourceKeyBuilder.BuildResourceKey(resourceKey, validationAttribute); + var propertyName = validationResourceKey.Split('.').Last(); + yield return new DiscoveredResource(pi, + validationResourceKey, + string.IsNullOrEmpty(validationAttribute.ErrorMessage) ? propertyName : validationAttribute.ErrorMessage, + propertyName, + pi.PropertyType, + pi.GetMethod.ReturnType, + pi.GetMethod.ReturnType.IsSimpleType()); + } + } + + foreach (var resourceKeyAttribute in keyAttributes) + { + yield return new DiscoveredResource(pi, + ResourceKeyBuilder.BuildResourceKey(typeKeyPrefixSpecified ? resourceKeyPrefix : null, + resourceKeyAttribute.Key, + separator: string.Empty), + string.IsNullOrEmpty(resourceKeyAttribute.Value) ? translation : resourceKeyAttribute.Value, + null, + pi.PropertyType, + pi.GetMethod.ReturnType, + true); + } + } + + private static string GetResourceValue(PropertyInfo pi) + { + var result = pi.Name; + + // try to extract resource value + var methodInfo = pi.GetGetMethod(); + if(IsStringProperty(methodInfo.ReturnType)) + { + try + { + if(methodInfo.IsStatic) + { + result = methodInfo.Invoke(null, null) as string; + } + else + { + if(pi.DeclaringType != null) + { + var targetInstance = Activator.CreateInstance(pi.DeclaringType); + var propertyReturnValue = methodInfo.Invoke(targetInstance, null) as string; + if(propertyReturnValue != null) + { + result = propertyReturnValue; + } + } + } + } + catch + { + // if we fail to retrieve value for the resource - fair enough + } + } + + var attributes = pi.GetCustomAttributes(true); + var displayAttribute = attributes.OfType().FirstOrDefault(); + + if(!string.IsNullOrEmpty(displayAttribute?.GetName())) + { + result = displayAttribute.GetName(); + } + + var displayNameAttribute = attributes.OfType().FirstOrDefault(); + if(!string.IsNullOrEmpty(displayNameAttribute?.DisplayName)) + { + result = displayNameAttribute.DisplayName; + } + + return result; + } + + internal static bool IsStringProperty(Type returnType) + { + return returnType == typeof(string); + } + } +} \ No newline at end of file diff --git a/src/DbLocalizationProvider/Sync/ResourceSynchronizer.cs b/src/DbLocalizationProvider/Sync/ResourceSynchronizer.cs index 81e7329d..e403ab61 100644 --- a/src/DbLocalizationProvider/Sync/ResourceSynchronizer.cs +++ b/src/DbLocalizationProvider/Sync/ResourceSynchronizer.cs @@ -81,7 +81,8 @@ private void ResetSyncStatus(DbContext db) private void RegisterDiscoveredModels(LanguageEntities db, IEnumerable types) { - var properties = types.SelectMany(type => TypeDiscoveryHelper.GetAllProperties(type, contextAwareScanning: false)); + var sut = new TypeDiscoveryHelper(); + var properties = types.SelectMany(type => sut.ScanResources(type)); foreach (var property in properties) { @@ -92,7 +93,8 @@ private void RegisterDiscoveredModels(LanguageEntities db, IEnumerable typ private void RegisterDiscoveredResources(LanguageEntities db, IEnumerable types) { - var properties = types.SelectMany(type => TypeDiscoveryHelper.GetAllProperties(type)); + var sut = new TypeDiscoveryHelper(); + var properties = types.SelectMany(type => sut.ScanResources(type)); foreach (var property in properties) { diff --git a/src/DbLocalizationProvider/Sync/TypeDiscoveryHelper.cs b/src/DbLocalizationProvider/Sync/TypeDiscoveryHelper.cs index 5ea73fb5..d182258a 100644 --- a/src/DbLocalizationProvider/Sync/TypeDiscoveryHelper.cs +++ b/src/DbLocalizationProvider/Sync/TypeDiscoveryHelper.cs @@ -1,17 +1,60 @@ using System; using System.Collections.Concurrent; using System.Collections.Generic; -using System.ComponentModel; -using System.ComponentModel.DataAnnotations; using System.Linq; using System.Reflection; -using DbLocalizationProvider.Internal; namespace DbLocalizationProvider.Sync { internal class TypeDiscoveryHelper { internal static ConcurrentDictionary> DiscoveredResourceCache = new ConcurrentDictionary>(); + private readonly List _scanners = new List(); + + public TypeDiscoveryHelper() + { + _scanners.Add(new LocalizedModelTypeScanner()); + _scanners.Add(new LocalizedResourceTypeScanner()); + } + + public IEnumerable ScanResources(Type target, string keyPrefix = null, IResourceTypeScanner scanner = null) + { + var typeScanner = scanner; + + if(scanner == null) + typeScanner = _scanners.FirstOrDefault(s => s.ShouldScan(target)); + + if(typeScanner == null) + return new List(); + + if (target.IsGenericParameter) + return new List(); + + var resourceKeyPrefix = typeScanner.GetResourceKeyPrefix(target, keyPrefix); + + var buffer = new List(); + buffer.AddRange(typeScanner.GetClassLevelResources(target, resourceKeyPrefix)); + + buffer.AddRange(typeScanner.GetResources(target, resourceKeyPrefix)); + + var result = buffer.Where(t => t.IsSimpleType || t.Info == null || t.Info.GetCustomAttribute() != null) + .ToList(); + + foreach (var property in buffer.Where(t => !t.IsSimpleType)) + { + if(!property.IsSimpleType) + result.AddRange(ScanResources(property.DeclaringType, property.Key, typeScanner)); + } + + var duplicateKeys = result.GroupBy(r => r.Key).Where(g => g.Count() > 1).ToList(); + if(duplicateKeys.Any()) + throw new DuplicateResourceKeyException($"Duplicate keys: [{string.Join(", ", duplicateKeys.Select(g => g.Key))}]"); + + // add scanned resources to the cache + DiscoveredResourceCache.TryAdd(target.FullName, result.Where(r => !string.IsNullOrEmpty(r.PropertyName)).Select(r => r.PropertyName).ToList()); + + return result; + } internal static List> GetTypes(params Func[] filters) { @@ -62,126 +105,122 @@ internal static IEnumerable GetTypesChildOf() return allTypes; } - internal static IEnumerable GetAllProperties(Type type, string keyPrefix = null, bool contextAwareScanning = true) - { - var resourceKeyPrefix = type.FullName; - var typeKeyPrefixSpecified = false; - var properties = new List(); - var modelAttribute = type.GetCustomAttribute(); - - if(contextAwareScanning) - { - // this is resource class scanning - try to fetch resource key prefix attribute if set there - var resourceAttribute = type.GetCustomAttribute(); - if(!string.IsNullOrEmpty(resourceAttribute?.KeyPrefix)) - { - resourceKeyPrefix = resourceAttribute.KeyPrefix; - typeKeyPrefixSpecified = true; - } - else - { - resourceKeyPrefix = string.IsNullOrEmpty(keyPrefix) ? type.FullName : keyPrefix; - } - } - else - { - // this is model scanning - try to fetch resource key prefix attribute if set there - if(!string.IsNullOrEmpty(modelAttribute?.KeyPrefix)) - { - resourceKeyPrefix = modelAttribute.KeyPrefix; - typeKeyPrefixSpecified = true; - } - - var resourceAttributesOnModelClass = type.GetCustomAttributes().ToList(); - if(resourceAttributesOnModelClass.Any()) - { - foreach (var resourceKeyAttribute in resourceAttributesOnModelClass) - { - properties.Add(new DiscoveredResource(null, - ResourceKeyBuilder.BuildResourceKey(resourceKeyPrefix, resourceKeyAttribute.Key, separator: string.Empty), - null, - resourceKeyAttribute.Value, - type, - typeof(string), - true)); - } - } - } - - if(type.BaseType == typeof(Enum)) - { - properties.AddRange(type.GetMembers(BindingFlags.Public | BindingFlags.Static) - .Select(mi => new DiscoveredResource(mi, - ResourceKeyBuilder.BuildResourceKey(resourceKeyPrefix, mi), - mi.Name, - mi.Name, - type, - Enum.GetUnderlyingType(type), - Enum.GetUnderlyingType(type).IsSimpleType())).ToList()); - } - else - { - var flags = BindingFlags.Public | BindingFlags.GetProperty | BindingFlags.Instance | BindingFlags.Static; - if(modelAttribute != null && !modelAttribute.Inherited) - flags = flags | BindingFlags.DeclaredOnly; - - properties.AddRange(type.GetProperties(flags) - .Where(pi => pi.GetCustomAttribute() == null) - .Where(pi => modelAttribute == null || !modelAttribute.OnlyIncluded || pi.GetCustomAttribute() != null) - .SelectMany(pi => DiscoverResourcesFromProperty(pi, resourceKeyPrefix, typeKeyPrefixSpecified)).ToList()); - } - - var duplicateKeys = properties.GroupBy(r => r.Key).Where(g => g.Count() > 1).ToList(); - if(duplicateKeys.Any()) - { - throw new DuplicateResourceKeyException($"Duplicate keys: [{string.Join(", ", duplicateKeys.Select(g => g.Key))}]"); - } - - // first we can filter out all simple and/or complex included properties from the type as starting list of discovered resources - var results = new List(properties.Where(t => t.IsSimpleType || t.Info == null || t.Info.GetCustomAttribute() != null)); - - foreach (var property in properties) - { - var pi = property.Info; - var deeperModelType = property.ReturnType; - - if(!property.IsSimpleType) - { - // if this is not a simple type - we need to scan deeper only if deeper model has attribute annotation - if(contextAwareScanning || deeperModelType.GetCustomAttribute() != null) - { - results.AddRange(GetAllProperties(property.DeclaringType, property.Key, contextAwareScanning)); - } - } - - if(pi == null) - continue; - - var validationAttributes = pi.GetCustomAttributes(); - foreach (var validationAttribute in validationAttributes) - { - var resourceKey = ResourceKeyBuilder.BuildResourceKey(property.Key, validationAttribute); - var propertyName = resourceKey.Split('.').Last(); - results.Add(new DiscoveredResource(pi, - resourceKey, - string.IsNullOrEmpty(validationAttribute.ErrorMessage) ? propertyName : validationAttribute.ErrorMessage, - propertyName, - property.DeclaringType, - property.ReturnType, - property.ReturnType.IsSimpleType())); - } - } - - // add scanned resources to the cache - DiscoveredResourceCache.TryAdd(type.FullName, results.Where(r => !string.IsNullOrEmpty(r.PropertyName)).Select(r => r.PropertyName).ToList()); - - return results; - } - - internal static bool IsStringProperty(Type returnType) - { - return returnType == typeof(string); - } + //internal static IEnumerable GetAllProperties(Type type, string keyPrefix = null, bool contextAwareScanning = true) + //{ + // var resourceKeyPrefix = type.FullName; + // var typeKeyPrefixSpecified = false; + // var properties = new List(); + // var modelAttribute = type.GetCustomAttribute(); + + // //if(contextAwareScanning) + // //{ + // // this is resource class scanning - try to fetch resource key prefix attribute if set there + // //var resourceAttribute = type.GetCustomAttribute(); + // //if(!string.IsNullOrEmpty(resourceAttribute?.KeyPrefix)) + // //{ + // // resourceKeyPrefix = resourceAttribute.KeyPrefix; + // // typeKeyPrefixSpecified = true; + // //} + // //else + // //{ + // // resourceKeyPrefix = string.IsNullOrEmpty(keyPrefix) ? type.FullName : keyPrefix; + // //} + // //} + // //else + // //{ + // // this is model scanning - try to fetch resource key prefix attribute if set there + // //if(!string.IsNullOrEmpty(modelAttribute?.KeyPrefix)) + // //{ + // // resourceKeyPrefix = modelAttribute.KeyPrefix; + // // typeKeyPrefixSpecified = true; + // //} + + // //var resourceAttributesOnModelClass = type.GetCustomAttributes().ToList(); + // //if(resourceAttributesOnModelClass.Any()) + // //{ + // // foreach (var resourceKeyAttribute in resourceAttributesOnModelClass) + // // { + // // properties.Add(new DiscoveredResource(null, + // // ResourceKeyBuilder.BuildResourceKey(resourceKeyPrefix, resourceKeyAttribute.Key, separator: string.Empty), + // // null, + // // resourceKeyAttribute.Value, + // // type, + // // typeof(string), + // // true)); + // // } + // //} + // //} + + // //if(type.BaseType == typeof(Enum)) + // //{ + // // properties.AddRange(type.GetMembers(BindingFlags.Public | BindingFlags.Static) + // // .Select(mi => new DiscoveredResource(mi, + // // ResourceKeyBuilder.BuildResourceKey(resourceKeyPrefix, mi), + // // mi.Name, + // // mi.Name, + // // type, + // // Enum.GetUnderlyingType(type), + // // Enum.GetUnderlyingType(type).IsSimpleType())).ToList()); + // //} + // //else + // //{ + // // var flags = BindingFlags.Public | BindingFlags.GetProperty | BindingFlags.Instance | BindingFlags.Static; + // // if (modelAttribute != null && !modelAttribute.Inherited) + // // flags = flags | BindingFlags.DeclaredOnly; + + // // properties.AddRange(type.GetProperties(flags) + // // .Where(pi => pi.GetCustomAttribute() == null) + // // .Where(pi => modelAttribute == null || !modelAttribute.OnlyIncluded || pi.GetCustomAttribute() != null) + // // .SelectMany(pi => DiscoverResourcesFromProperty(pi, resourceKeyPrefix, typeKeyPrefixSpecified)).ToList()); + // //} + + // //var duplicateKeys = properties.GroupBy(r => r.Key).Where(g => g.Count() > 1).ToList(); + // //if(duplicateKeys.Any()) + // //{ + // // throw new DuplicateResourceKeyException($"Duplicate keys: [{string.Join(", ", duplicateKeys.Select(g => g.Key))}]"); + // //} + + // // first we can filter out all simple and/or complex included properties from the type as starting list of discovered resources + + // var results = new List(properties.Where(t => t.IsSimpleType || t.Info == null || t.Info.GetCustomAttribute() != null)); + + // //foreach (var property in properties) + // //{ + // // var pi = property.Info; + // // var deeperModelType = property.ReturnType; + + // // if(!property.IsSimpleType) + // // { + // // // if this is not a simple type - we need to scan deeper only if deeper model has attribute annotation + // // if(contextAwareScanning || deeperModelType.GetCustomAttribute() != null) + // // { + // // results.AddRange(GetAllProperties(property.DeclaringType, property.Key, contextAwareScanning)); + // // } + // // } + + // // if(pi == null) + // // continue; + + // // var validationAttributes = pi.GetCustomAttributes(); + // // foreach (var validationAttribute in validationAttributes) + // // { + // // var resourceKey = ResourceKeyBuilder.BuildResourceKey(property.Key, validationAttribute); + // // var propertyName = resourceKey.Split('.').Last(); + // // results.Add(new DiscoveredResource(pi, + // // resourceKey, + // // string.IsNullOrEmpty(validationAttribute.ErrorMessage) ? propertyName : validationAttribute.ErrorMessage, + // // propertyName, + // // property.DeclaringType, + // // property.ReturnType, + // // property.ReturnType.IsSimpleType())); + // // } + // //} + + // // add scanned resources to the cache + // //DiscoveredResourceCache.TryAdd(type.FullName, results.Where(r => !string.IsNullOrEmpty(r.PropertyName)).Select(r => r.PropertyName).ToList()); + + // return results; + //} private static IEnumerable GetAssemblies() { @@ -208,99 +247,5 @@ private static IEnumerable SelectTypes(Assembly assembly, Func return new List(); } } - - private static IEnumerable DiscoverResourcesFromProperty(PropertyInfo pi, string resourceKeyPrefix, bool typeKeyPrefixSpecified) - { - // check if there are [ResourceKey] attributes - var keyAttributes = pi.GetCustomAttributes().ToList(); - var translation = GetResourceValue(pi); - - if(!keyAttributes.Any()) - { - yield return new DiscoveredResource(pi, - ResourceKeyBuilder.BuildResourceKey(resourceKeyPrefix, pi), - translation, - pi.Name, - pi.PropertyType, - pi.GetMethod.ReturnType, - pi.GetMethod.ReturnType.IsSimpleType()); - - // try to fetch also [Display()] attribute to generate new "...-Description" resource => usually used for help text labels - var displayAttribute = pi.GetCustomAttribute(); - if(!string.IsNullOrEmpty(displayAttribute?.Description)) - { - yield return new DiscoveredResource(pi, - $"{ResourceKeyBuilder.BuildResourceKey(resourceKeyPrefix, pi)}-Description", - $"{pi.Name}-Description", - displayAttribute.Description, - pi.PropertyType, - pi.GetMethod.ReturnType, - pi.GetMethod.ReturnType.IsSimpleType()); - } - } - - foreach (var resourceKeyAttribute in keyAttributes) - { - yield return new DiscoveredResource(pi, - ResourceKeyBuilder.BuildResourceKey(typeKeyPrefixSpecified ? resourceKeyPrefix : null, - resourceKeyAttribute.Key, - separator: string.Empty), - string.IsNullOrEmpty(resourceKeyAttribute.Value) ? translation : resourceKeyAttribute.Value, - null, - pi.PropertyType, - pi.GetMethod.ReturnType, - true); - } - } - - private static string GetResourceValue(PropertyInfo pi) - { - var result = pi.Name; - - // try to extract resource value - var methodInfo = pi.GetGetMethod(); - if(IsStringProperty(methodInfo.ReturnType)) - { - try - { - if(methodInfo.IsStatic) - { - result = methodInfo.Invoke(null, null) as string; - } - else - { - if(pi.DeclaringType != null) - { - var targetInstance = Activator.CreateInstance(pi.DeclaringType); - var propertyReturnValue = methodInfo.Invoke(targetInstance, null) as string; - if(propertyReturnValue != null) - { - result = propertyReturnValue; - } - } - } - } - catch - { - // if we fail to retrieve value for the resource - fair enough - } - } - - var attributes = pi.GetCustomAttributes(true); - var displayAttribute = attributes.OfType().FirstOrDefault(); - - if(!string.IsNullOrEmpty(displayAttribute?.GetName())) - { - result = displayAttribute.GetName(); - } - - var displayNameAttribute = attributes.OfType().FirstOrDefault(); - if(!string.IsNullOrEmpty(displayNameAttribute?.DisplayName)) - { - result = displayNameAttribute.DisplayName; - } - - return result; - } } }