diff --git a/Oqtane.Client/Resources/UI/ModuleInstance.resx b/Oqtane.Client/Resources/UI/ModuleInstance.resx index fc0994f2..4c4a4c63 100644 --- a/Oqtane.Client/Resources/UI/ModuleInstance.resx +++ b/Oqtane.Client/Resources/UI/ModuleInstance.resx @@ -124,6 +124,9 @@ Module Type Is Invalid For {0} - An Unexpected Error Has Occurred + An Unexpected Error Has Occurred + + + Missing service(s): {0}. Please make sure they have been registered correctly. \ No newline at end of file diff --git a/Oqtane.Client/UI/RenderModeBoundary.razor b/Oqtane.Client/UI/RenderModeBoundary.razor index abf96087..b6b5e0af 100644 --- a/Oqtane.Client/UI/RenderModeBoundary.razor +++ b/Oqtane.Client/UI/RenderModeBoundary.razor @@ -1,7 +1,11 @@ @namespace Oqtane.UI +@using System.Reflection +@using Module = Oqtane.Models.Module +@inject IServiceProvider ServiceProvider @inject SiteState ComponentSiteState @inject IStringLocalizer Localizer @inject ILogService LoggingService +@inject NavigationManager NavigationManager @inherits ErrorBoundary @@ -67,37 +71,50 @@ { if (ShouldRender()) { - if (!string.IsNullOrEmpty(ModuleState.ModuleType)) - { - ModuleType = Type.GetType(ModuleState.ModuleType); - if (ModuleType != null) - { - // repopulate the SiteState service based on the values passed in the SiteState parameter (this is how state is marshalled across the render mode boundary) - ComponentSiteState.Hydrate(SiteState); - - DynamicComponent = builder => - { - builder.OpenComponent(0, ModuleType); - builder.AddAttribute(1, "RenderModeBoundary", this); - builder.CloseComponent(); - }; - } - else - { - // module does not exist with typename specified - _messageContent = string.Format(Localizer["Error.Module.InvalidName"], Utilities.GetTypeNameLastSegment(ModuleState.ModuleType, 0)); - _messageType = MessageType.Error; - _messagePosition = "top"; - _messageStyle = MessageStyle.Alert; - } - } - else + if (string.IsNullOrEmpty(ModuleState.ModuleType)) { _messageContent = string.Format(Localizer["Error.Module.InvalidType"], ModuleState.ModuleDefinitionName); _messageType = MessageType.Error; _messagePosition = "top"; _messageStyle = MessageStyle.Alert; + + return; } + + ModuleType = Type.GetType(ModuleState.ModuleType); + var moduleName = Utilities.GetTypeNameLastSegment(ModuleState.ModuleType, 0); + if (ModuleType == null) + { + // module does not exist with typename specified + _messageContent = string.Format(Localizer["Error.Module.InvalidName"], moduleName); + _messageType = MessageType.Error; + _messagePosition = "top"; + _messageStyle = MessageStyle.Alert; + + return; + } + + //only validate the services injection in development environment + if (NavigationManager.BaseUri.Contains("localhost:") && !ValidateModuleTypeInjectedServices(ModuleType, out IList missingServices)) + { + // module type is not valid for instantiation + _messageContent = string.Format(Localizer["Error.Module.InvalidInjectedServices"], string.Join(",", missingServices)); + _messageType = MessageType.Error; + _messagePosition = "top"; + _messageStyle = MessageStyle.Alert; + + return; + } + + // repopulate the SiteState service based on the values passed in the SiteState parameter (this is how state is marshalled across the render mode boundary) + ComponentSiteState.Hydrate(SiteState); + + DynamicComponent = builder => + { + builder.OpenComponent(0, ModuleType); + builder.AddAttribute(1, "RenderModeBoundary", this); + builder.CloseComponent(); + }; } } @@ -165,4 +182,26 @@ _error = ""; base.Recover(); } + + private bool ValidateModuleTypeInjectedServices(Type moduleType, out IList missingServices) + { + missingServices = new List(); + + var properties = Utilities.GetPropertiesIncludingInherited(moduleType, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); + foreach(var property in properties) + { + var injectAttribute = property.GetCustomAttribute(typeof(InjectAttribute)); + if (injectAttribute != null) + { + var serviceType = property.PropertyType; + var service = ServiceProvider.GetService(serviceType); + if (serviceType != null && service == null) + { + missingServices.Add(Utilities.GetTypeNameLastSegment(serviceType.FullName, 0)); + } + } + } + + return !missingServices.Any(); + } } \ No newline at end of file diff --git a/Oqtane.Shared/Shared/Utilities.cs b/Oqtane.Shared/Shared/Utilities.cs index 44cccffc..45888f0c 100644 --- a/Oqtane.Shared/Shared/Utilities.cs +++ b/Oqtane.Shared/Shared/Utilities.cs @@ -4,6 +4,8 @@ using System.Globalization; using System.IO; using System.Linq; using System.Net; +using System.Reflection; +using System.Runtime.InteropServices; using System.Text; using System.Text.RegularExpressions; @@ -750,6 +752,75 @@ namespace Oqtane.Shared } } + public static IEnumerable GetPropertiesIncludingInherited(Type type, BindingFlags bindingFlags) + { + var dictionary = new Dictionary(StringComparer.Ordinal); + + var currentType = type; + while (currentType != null) + { + var properties = currentType.GetProperties(bindingFlags | BindingFlags.DeclaredOnly); + foreach (var property in properties) + { + if (!dictionary.TryGetValue(property.Name, out var others)) + { + dictionary.Add(property.Name, property); + } + else if (!IsInheritedProperty(property, others)) + { + List many; + if (others is PropertyInfo single) + { + many = new List { single }; + dictionary[property.Name] = many; + } + else + { + many = (List)others; + } + many.Add(property); + } + } + + currentType = currentType.BaseType; + } + + foreach (var item in dictionary) + { + if (item.Value is PropertyInfo property) + { + yield return property; + continue; + } + + var list = (List)item.Value; + var count = list.Count; + for (var i = 0; i < count; i++) + { + yield return list[i]; + } + } + } + + private static bool IsInheritedProperty(PropertyInfo property, object others) + { + if (others is PropertyInfo single) + { + return single.GetMethod?.GetBaseDefinition() == property.GetMethod?.GetBaseDefinition(); + } + + var many = (List)others; + foreach (var other in CollectionsMarshal.AsSpan(many)) + { + if (other.GetMethod?.GetBaseDefinition() == property.GetMethod?.GetBaseDefinition()) + { + return true; + } + } + + return false; + } + [Obsolete("ContentUrl(Alias alias, int fileId) is deprecated. Use FileUrl(Alias alias, int fileId) instead.", false)] public static string ContentUrl(Alias alias, int fileId) {