diff --git a/Directory.Build.props b/Directory.Build.props index 1f1a2d04..594008b7 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,16 +1,17 @@  - 9.0 + 10.0 enable latest AllEnabledByDefault + enable Ignia OnTopic - ©2021 Ignia, LLC + ©2022 Ignia, LLC Ignia https://github.com/Ignia/Topics-Library true diff --git a/OnTopic.All/OnTopic.All.csproj b/OnTopic.All/OnTopic.All.csproj index 23006a24..aebc44b1 100644 --- a/OnTopic.All/OnTopic.All.csproj +++ b/OnTopic.All/OnTopic.All.csproj @@ -11,7 +11,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/OnTopic.All/Properties/AssemblyInfo.cs b/OnTopic.All/Properties/AssemblyInfo.cs index bcdd7ce1..171b1811 100644 --- a/OnTopic.All/Properties/AssemblyInfo.cs +++ b/OnTopic.All/Properties/AssemblyInfo.cs @@ -3,7 +3,6 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ -using System; using System.Runtime.InteropServices; /*============================================================================================================================== diff --git a/OnTopic.AspNetCore.Mvc.Host/OnTopic.AspNetCore.Mvc.Host.csproj b/OnTopic.AspNetCore.Mvc.Host/OnTopic.AspNetCore.Mvc.Host.csproj index 7b32c7a3..57210271 100644 --- a/OnTopic.AspNetCore.Mvc.Host/OnTopic.AspNetCore.Mvc.Host.csproj +++ b/OnTopic.AspNetCore.Mvc.Host/OnTopic.AspNetCore.Mvc.Host.csproj @@ -1,13 +1,13 @@  - net5.0 + net6.0 62eb85bf-f802-4afd-8bec-3d344e1cfc79 false - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/OnTopic.AspNetCore.Mvc.Host/Program.cs b/OnTopic.AspNetCore.Mvc.Host/Program.cs index ee3d3f0c..6d8c7786 100644 --- a/OnTopic.AspNetCore.Mvc.Host/Program.cs +++ b/OnTopic.AspNetCore.Mvc.Host/Program.cs @@ -3,43 +3,91 @@ | Client Ignia, LLC | Project Sample OnTopic Site \=============================================================================================================================*/ -using System; -using System.Diagnostics.CodeAnalysis; -using Microsoft.AspNetCore.Hosting; -using Microsoft.Extensions.Hosting; - -namespace OnTopic.AspNetCore.Mvc.Host { - - /*============================================================================================================================ - | CLASS: PROGRAM - \---------------------------------------------------------------------------------------------------------------------------*/ - /// - /// The class—and it's method—represent the entry point into the - /// ASP.NET Core web application. - /// - [ExcludeFromCodeCoverage] - public static class Program { - - /*========================================================================================================================== - | METHOD: MAIN - \-------------------------------------------------------------------------------------------------------------------------*/ - /// - /// Responsible for bootstrapping the web application. - /// - public static void Main(string[] args) => CreateHostBuilder(args).Build().Run(); - - /*========================================================================================================================== - | METHOD: CREATE WEB HOST BUILDER - \-------------------------------------------------------------------------------------------------------------------------*/ - /// - /// Configures a new with the default options. - /// - public static IHostBuilder CreateHostBuilder(string[] args) => - Microsoft.Extensions.Hosting.Host - .CreateDefaultBuilder(args) - .ConfigureWebHostDefaults(webBuilder => { - webBuilder.UseStartup(); - }); - - } //Class -} //Namespace \ No newline at end of file +using Microsoft.AspNetCore.Mvc.Controllers; +using Microsoft.AspNetCore.Mvc.ViewComponents; +using OnTopic.AspNetCore.Mvc; +using OnTopic.AspNetCore.Mvc.Host; + +#pragma warning disable CA1812 // Avoid uninstantiated internal classes + +/*============================================================================================================================== +| CONFIGURE SERVICES +\-----------------------------------------------------------------------------------------------------------------------------*/ +var builder = WebApplication.CreateBuilder(args); + +/*------------------------------------------------------------------------------------------------------------------------------ +| Configure: Cookie Policy +\-----------------------------------------------------------------------------------------------------------------------------*/ +builder.Services.Configure(options => { + // This lambda determines whether user consent for non-essential cookies is needed for a given request. + options.CheckConsentNeeded = context => true; + options.MinimumSameSitePolicy = SameSiteMode.None; +}); + +/*------------------------------------------------------------------------------------------------------------------------------ +| Configure: Output Caching +\-----------------------------------------------------------------------------------------------------------------------------*/ +builder.Services.AddResponseCaching(); + +/*------------------------------------------------------------------------------------------------------------------------------ +| Configure: MVC +\-----------------------------------------------------------------------------------------------------------------------------*/ +builder.Services.AddControllersWithViews() + + //Add OnTopic support + .AddTopicSupport(); + +/*------------------------------------------------------------------------------------------------------------------------------ +| Register: Activators +\-----------------------------------------------------------------------------------------------------------------------------*/ +var activator = new SampleActivator(builder.Configuration.GetConnectionString("OnTopic")); + +builder.Services.AddSingleton(activator); +builder.Services.AddSingleton(activator); + +/*============================================================================================================================== +| CONFIGURE APPLICATION +\-----------------------------------------------------------------------------------------------------------------------------*/ +var app = builder.Build(); + +/*------------------------------------------------------------------------------------------------------------------------------ +| Configure: Error Pages +\-----------------------------------------------------------------------------------------------------------------------------*/ + app.UseStatusCodePagesWithReExecute("/Error/{0}/"); +if (!app.Environment.IsDevelopment()) { + app.UseExceptionHandler("/Error/500/"); + // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts. + app.UseHsts(); +} + +/*------------------------------------------------------------------------------------------------------------------------------ +| Configure: Server defaults +\-----------------------------------------------------------------------------------------------------------------------------*/ +app.UseHttpsRedirection(); +app.UseStaticFiles(); +app.UseCookiePolicy(); +app.UseRouting(); +app.UseCors("default"); +app.UseResponseCaching(); + +/*------------------------------------------------------------------------------------------------------------------------------ +| Configure: MVC +\-----------------------------------------------------------------------------------------------------------------------------*/ +app.MapImplicitAreaControllerRoute(); // {area:exists}/{action=Index} +app.MapDefaultAreaControllerRoute(); // {area:exists}/{controller}/{action=Index}/{id?} +app.MapTopicAreaRoute(); // {area:exists}/{**path} + +app.MapTopicErrors(includeStaticFiles: false); // Error/{statusCode} +app.MapDefaultControllerRoute(); // {controller=Home}/{action=Index}/{id?} +app.MapTopicRoute(rootTopic: "Web"); // Web/{**path} +app.MapTopicRoute(rootTopic: "Error"); // Error/{**path} +app.MapTopicSitemap(); // Sitemap +app.MapTopicRedirect(); // Topic/{topicId} +app.MapControllers(); + +/*------------------------------------------------------------------------------------------------------------------------------ +| Run application +\-----------------------------------------------------------------------------------------------------------------------------*/ +app.Run(); + +#pragma warning restore CA1812 // Avoid uninstantiated internal classes \ No newline at end of file diff --git a/OnTopic.AspNetCore.Mvc.Host/Properties/AssemblyInfo.cs b/OnTopic.AspNetCore.Mvc.Host/Properties/AssemblyInfo.cs index bcdd7ce1..171b1811 100644 --- a/OnTopic.AspNetCore.Mvc.Host/Properties/AssemblyInfo.cs +++ b/OnTopic.AspNetCore.Mvc.Host/Properties/AssemblyInfo.cs @@ -3,7 +3,6 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ -using System; using System.Runtime.InteropServices; /*============================================================================================================================== diff --git a/OnTopic.AspNetCore.Mvc.Host/SampleActivator.cs b/OnTopic.AspNetCore.Mvc.Host/SampleActivator.cs index f50d1845..f0137008 100644 --- a/OnTopic.AspNetCore.Mvc.Host/SampleActivator.cs +++ b/OnTopic.AspNetCore.Mvc.Host/SampleActivator.cs @@ -3,7 +3,6 @@ | Client Ignia, LLC | Project Sample OnTopic Site \=============================================================================================================================*/ -using System; using System.Diagnostics.CodeAnalysis; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Controllers; @@ -118,6 +117,8 @@ public object Create(ControllerContext context) { return type.Name switch { nameof(TopicController) => new TopicController(_topicRepository, _topicMappingService), + nameof(ErrorController) => + new ErrorController(_topicRepository, _topicMappingService), nameof(SitemapController) => new SitemapController(_topicRepository), nameof(RedirectController) => diff --git a/OnTopic.AspNetCore.Mvc.Host/Startup.cs b/OnTopic.AspNetCore.Mvc.Host/Startup.cs deleted file mode 100644 index c480c764..00000000 --- a/OnTopic.AspNetCore.Mvc.Host/Startup.cs +++ /dev/null @@ -1,126 +0,0 @@ -/*============================================================================================================================== -| Author Ignia, LLC -| Client Ignia, LLC -| Project Sample OnTopic Site -\=============================================================================================================================*/ -using System.Diagnostics.CodeAnalysis; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Hosting; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc.Controllers; -using Microsoft.AspNetCore.Mvc.ViewComponents; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; - -namespace OnTopic.AspNetCore.Mvc.Host { - - /*============================================================================================================================ - | CLASS: STARTUP - \---------------------------------------------------------------------------------------------------------------------------*/ - /// - /// Configures the application and sets up dependencies. - /// - [ExcludeFromCodeCoverage] - public class Startup { - - /*========================================================================================================================== - | CONSTRUCTOR - \-------------------------------------------------------------------------------------------------------------------------*/ - /// - /// Constructs a new instances of the class. Accepts an . - /// - /// - /// The shared dependency. - /// - public Startup(IConfiguration configuration) { - Configuration = configuration; - } - - /*========================================================================================================================== - | PROPERTY: CONFIGURATION - \-------------------------------------------------------------------------------------------------------------------------*/ - /// - /// Provides a (public) reference to the application's service. - /// - public IConfiguration Configuration { get; } - - /*========================================================================================================================== - | METHOD: CONFIGURE SERVICES - \-------------------------------------------------------------------------------------------------------------------------*/ - /// - /// Provides configuration of services. This method is called by the runtime to bootstrap the server configuration. - /// - public void ConfigureServices(IServiceCollection services) { - - /*------------------------------------------------------------------------------------------------------------------------ - | Configure: Cookie Policy - \-----------------------------------------------------------------------------------------------------------------------*/ - services.Configure(options => { - // This lambda determines whether user consent for non-essential cookies is needed for a given request. - options.CheckConsentNeeded = context => true; - options.MinimumSameSitePolicy = SameSiteMode.None; - }); - - /*------------------------------------------------------------------------------------------------------------------------ - | Configure: MVC - \-----------------------------------------------------------------------------------------------------------------------*/ - services.AddControllersWithViews() - - //Add OnTopic support - .AddTopicSupport(); - - /*------------------------------------------------------------------------------------------------------------------------ - | Register: Activators - \-----------------------------------------------------------------------------------------------------------------------*/ - var activator = new SampleActivator(Configuration.GetConnectionString("OnTopic")); - - services.AddSingleton(activator); - services.AddSingleton(activator); - - } - - /*========================================================================================================================== - | METHOD: CONFIGURE (APPLICATION) - \-------------------------------------------------------------------------------------------------------------------------*/ - /// - /// Provides configuration the application. This method is called by the runtime to bootstrap the application - /// configuration, including the HTTP pipeline. - /// - public static void Configure(IApplicationBuilder app, IWebHostEnvironment env) { - - /*------------------------------------------------------------------------------------------------------------------------ - | Configure: Error Pages - \-----------------------------------------------------------------------------------------------------------------------*/ - if (env.IsDevelopment()) { - app.UseDeveloperExceptionPage(); - } - else { - app.UseExceptionHandler("/Home/Error"); - // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts. - app.UseHsts(); - } - - /*------------------------------------------------------------------------------------------------------------------------ - | Configure: Server defaults - \-----------------------------------------------------------------------------------------------------------------------*/ - app.UseHttpsRedirection(); - app.UseStaticFiles(); - app.UseCookiePolicy(); - app.UseRouting(); - app.UseCors("default"); - - /*------------------------------------------------------------------------------------------------------------------------ - | Configure: MVC - \-----------------------------------------------------------------------------------------------------------------------*/ - app.UseEndpoints(endpoints => { - endpoints.MapTopicRoute("Web"); - endpoints.MapTopicSitemap(); - endpoints.MapTopicRedirect(); - endpoints.MapControllers(); - }); - - } - - } //Class -} //Namespace \ No newline at end of file diff --git a/OnTopic.AspNetCore.Mvc.IntegrationTests.Host/Areas/Area/Controllers/AreaController.cs b/OnTopic.AspNetCore.Mvc.IntegrationTests.Host/Areas/Area/Controllers/AreaController.cs index 747a4f9b..218c88e1 100644 --- a/OnTopic.AspNetCore.Mvc.IntegrationTests.Host/Areas/Area/Controllers/AreaController.cs +++ b/OnTopic.AspNetCore.Mvc.IntegrationTests.Host/Areas/Area/Controllers/AreaController.cs @@ -3,12 +3,9 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ -using System.Diagnostics.CodeAnalysis; using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Routing; using OnTopic.AspNetCore.Mvc.Controllers; using OnTopic.Mapping; -using OnTopic.Repositories; namespace OnTopic.AspNetCore.Mvc.IntegrationTests.Areas.Area.Controllers { diff --git a/OnTopic.AspNetCore.Mvc.IntegrationTests.Host/OnTopic.AspNetCore.Mvc.IntegrationTests.Host.csproj b/OnTopic.AspNetCore.Mvc.IntegrationTests.Host/OnTopic.AspNetCore.Mvc.IntegrationTests.Host.csproj index de304b61..f5d926dc 100644 --- a/OnTopic.AspNetCore.Mvc.IntegrationTests.Host/OnTopic.AspNetCore.Mvc.IntegrationTests.Host.csproj +++ b/OnTopic.AspNetCore.Mvc.IntegrationTests.Host/OnTopic.AspNetCore.Mvc.IntegrationTests.Host.csproj @@ -1,12 +1,12 @@  - net5.0 + net6.0 false - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/OnTopic.AspNetCore.Mvc.IntegrationTests.Host/Program.cs b/OnTopic.AspNetCore.Mvc.IntegrationTests.Host/Program.cs index 99fd880a..ec3f48ca 100644 --- a/OnTopic.AspNetCore.Mvc.IntegrationTests.Host/Program.cs +++ b/OnTopic.AspNetCore.Mvc.IntegrationTests.Host/Program.cs @@ -3,10 +3,6 @@ | Client Ignia, LLC | Project Integration Tests Host \=============================================================================================================================*/ -using System; -using System.Diagnostics.CodeAnalysis; -using Microsoft.AspNetCore.Hosting; -using Microsoft.Extensions.Hosting; namespace OnTopic.AspNetCore.Mvc.IntegrationTests.Host { diff --git a/OnTopic.AspNetCore.Mvc.IntegrationTests.Host/Properties/AssemblyInfo.cs b/OnTopic.AspNetCore.Mvc.IntegrationTests.Host/Properties/AssemblyInfo.cs index 42d8b9b8..caf0a96a 100644 --- a/OnTopic.AspNetCore.Mvc.IntegrationTests.Host/Properties/AssemblyInfo.cs +++ b/OnTopic.AspNetCore.Mvc.IntegrationTests.Host/Properties/AssemblyInfo.cs @@ -3,8 +3,17 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ -using System; -using System.Diagnostics.CodeAnalysis; + +/*============================================================================================================================== +| USING DIRECTIVES (GLOBAL) +\-----------------------------------------------------------------------------------------------------------------------------*/ +global using System.Diagnostics.CodeAnalysis; +global using OnTopic.Internal.Diagnostics; +global using OnTopic.Repositories; + +/*============================================================================================================================== +| USING DIRECTIVES (LOCAL) +\-----------------------------------------------------------------------------------------------------------------------------*/ using System.Runtime.InteropServices; /*============================================================================================================================== diff --git a/OnTopic.AspNetCore.Mvc.IntegrationTests.Host/Repositories/StubTopicRepository.cs b/OnTopic.AspNetCore.Mvc.IntegrationTests.Host/Repositories/StubTopicRepository.cs index c8727c8d..17906860 100644 --- a/OnTopic.AspNetCore.Mvc.IntegrationTests.Host/Repositories/StubTopicRepository.cs +++ b/OnTopic.AspNetCore.Mvc.IntegrationTests.Host/Repositories/StubTopicRepository.cs @@ -3,11 +3,7 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ -using System; -using System.Diagnostics.CodeAnalysis; -using OnTopic.Internal.Diagnostics; using OnTopic.Querying; -using OnTopic.Repositories; namespace OnTopic.AspNetCore.Mvc.IntegrationTests.Host.Repositories { @@ -152,6 +148,7 @@ private static Topic CreateFakeData() { var web = new Topic("Web", "Page", rootTopic, currentAttributeId++); _ = new Topic("ContentList", "ContentList", web, currentAttributeId++); _ = new Topic("MissingView", "Missing", web, currentAttributeId++); + _ = new Topic("Container", "Container", web, currentAttributeId++); /*------------------------------------------------------------------------------------------------------------------------ | Establish area topics @@ -165,6 +162,29 @@ private static Topic CreateFakeData() { View = "Accordion" }; + /*------------------------------------------------------------------------------------------------------------------------ + | Establish error topics + \-----------------------------------------------------------------------------------------------------------------------*/ + var error = new Topic("Error", "Page", rootTopic, currentAttributeId++); + + _ = new Topic("400", "Page", error, currentAttributeId++); + _ = new Topic("Unauthorized", "Page", error, currentAttributeId++); + + /*------------------------------------------------------------------------------------------------------------------------ + | Establish caching tests + \-----------------------------------------------------------------------------------------------------------------------*/ + var cacheProfile = new Topic("CacheProfile", "CacheProfile", rootTopic, currentAttributeId++); + var cachedPage = new Topic("CachedPage", "Page", web, currentAttributeId++); + var uncachedPage = new Topic("UncachedPage", "Page", web, currentAttributeId++); + + cacheProfile.Attributes.SetValue("Duration", "10"); + cacheProfile.Attributes.SetValue("Location", "Any"); + + cachedPage.References.SetValue("CacheProfile", cacheProfile); + cachedPage.Attributes.SetValue("View", "Counter"); + + uncachedPage.Attributes.SetValue("View", "Counter"); + /*------------------------------------------------------------------------------------------------------------------------ | Set to cache \-----------------------------------------------------------------------------------------------------------------------*/ diff --git a/OnTopic.AspNetCore.Mvc.IntegrationTests.Host/SampleActivator.cs b/OnTopic.AspNetCore.Mvc.IntegrationTests.Host/SampleActivator.cs index dfeaadcf..837dda39 100644 --- a/OnTopic.AspNetCore.Mvc.IntegrationTests.Host/SampleActivator.cs +++ b/OnTopic.AspNetCore.Mvc.IntegrationTests.Host/SampleActivator.cs @@ -3,8 +3,6 @@ | Client Ignia, LLC | Project Integration Tests Host \=============================================================================================================================*/ -using System; -using System.Diagnostics.CodeAnalysis; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Controllers; using Microsoft.AspNetCore.Mvc.ViewComponents; @@ -12,11 +10,9 @@ using OnTopic.AspNetCore.Mvc.IntegrationTests.Areas.Area.Controllers; using OnTopic.AspNetCore.Mvc.IntegrationTests.Host.Repositories; using OnTopic.Data.Caching; -using OnTopic.Internal.Diagnostics; using OnTopic.Lookup; using OnTopic.Mapping; using OnTopic.Mapping.Hierarchical; -using OnTopic.Repositories; using OnTopic.ViewModels; namespace OnTopic.AspNetCore.Mvc.IntegrationTests.Host { @@ -110,6 +106,8 @@ public object Create(ControllerContext context) { new TopicController(_topicRepository, _topicMappingService), nameof(AreaController) => new AreaController(_topicRepository, _topicMappingService), + nameof(ErrorController) => + new ErrorController(_topicRepository, _topicMappingService), nameof(ControllerController) => new ControllerController(), nameof(SitemapController) => diff --git a/OnTopic.AspNetCore.Mvc.IntegrationTests.Host/Startup.cs b/OnTopic.AspNetCore.Mvc.IntegrationTests.Host/Startup.cs index 84fc7467..69514be5 100644 --- a/OnTopic.AspNetCore.Mvc.IntegrationTests.Host/Startup.cs +++ b/OnTopic.AspNetCore.Mvc.IntegrationTests.Host/Startup.cs @@ -3,12 +3,8 @@ | Client Ignia, LLC | Project Integration Tests Host \=============================================================================================================================*/ -using System.Diagnostics.CodeAnalysis; -using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Mvc.Controllers; using Microsoft.AspNetCore.Mvc.ViewComponents; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; namespace OnTopic.AspNetCore.Mvc.IntegrationTests.Host { @@ -50,6 +46,11 @@ public Startup(IConfiguration configuration) { /// public void ConfigureServices(IServiceCollection services) { + /*------------------------------------------------------------------------------------------------------------------------------ + | Configure: Output Caching + \-----------------------------------------------------------------------------------------------------------------------------*/ + services.AddResponseCaching(); + /*------------------------------------------------------------------------------------------------------------------------ | Configure: MVC \-----------------------------------------------------------------------------------------------------------------------*/ @@ -81,22 +82,26 @@ public static void Configure(IApplicationBuilder app) { | Configure: Error Pages \-----------------------------------------------------------------------------------------------------------------------*/ app.UseDeveloperExceptionPage(); + app.UseStatusCodePagesWithReExecute("/Error/{0}/"); /*------------------------------------------------------------------------------------------------------------------------ | Configure: Server defaults \-----------------------------------------------------------------------------------------------------------------------*/ app.UseStaticFiles(); app.UseRouting(); + app.UseResponseCaching(); /*------------------------------------------------------------------------------------------------------------------------ | Configure: MVC \-----------------------------------------------------------------------------------------------------------------------*/ app.UseEndpoints(endpoints => { + endpoints.MapTopicErrors(includeStaticFiles: false); endpoints.MapDefaultAreaControllerRoute(); endpoints.MapDefaultControllerRoute(); endpoints.MapImplicitAreaControllerRoute(); endpoints.MapTopicAreaRoute(); endpoints.MapTopicRoute("Web"); + endpoints.MapTopicRoute("Error"); endpoints.MapTopicSitemap(); endpoints.MapTopicRedirect(); endpoints.MapControllers(); diff --git a/OnTopic.AspNetCore.Mvc.IntegrationTests.Host/Views/Page/Counter.cshtml b/OnTopic.AspNetCore.Mvc.IntegrationTests.Host/Views/Page/Counter.cshtml new file mode 100644 index 00000000..40a0cf80 --- /dev/null +++ b/OnTopic.AspNetCore.Mvc.IntegrationTests.Host/Views/Page/Counter.cshtml @@ -0,0 +1,18 @@ +@getCounter() + +@functions { + + //Establish a counter for each page path + public static Dictionary pageCounter = new(); + + //Increment the counter for each call to a given page path + public int getCounter() { + var path = Context.Request.Path; + if (!pageCounter.ContainsKey(path)) { + pageCounter.Add(path, 0); + } + pageCounter[path] = pageCounter[path]+1; + return pageCounter[path]; + } + +} \ No newline at end of file diff --git a/OnTopic.AspNetCore.Mvc.IntegrationTests.Host/Views/Page/Page.cshtml b/OnTopic.AspNetCore.Mvc.IntegrationTests.Host/Views/Page/Page.cshtml new file mode 100644 index 00000000..a59782d5 --- /dev/null +++ b/OnTopic.AspNetCore.Mvc.IntegrationTests.Host/Views/Page/Page.cshtml @@ -0,0 +1,3 @@ +@using OnTopic.ViewModels +@model PageTopicViewModel +@Model?.Title \ No newline at end of file diff --git a/OnTopic.AspNetCore.Mvc.IntegrationTests/OnTopic.AspNetCore.Mvc.IntegrationTests.csproj b/OnTopic.AspNetCore.Mvc.IntegrationTests/OnTopic.AspNetCore.Mvc.IntegrationTests.csproj index 11c50225..7f9595ea 100644 --- a/OnTopic.AspNetCore.Mvc.IntegrationTests/OnTopic.AspNetCore.Mvc.IntegrationTests.csproj +++ b/OnTopic.AspNetCore.Mvc.IntegrationTests/OnTopic.AspNetCore.Mvc.IntegrationTests.csproj @@ -1,22 +1,22 @@  - net5.0 + net6.0 false - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + - + runtime; build; native; contentfiles; analyzers; buildtransitive all - + diff --git a/OnTopic.AspNetCore.Mvc.IntegrationTests/Properties/AssemblyInfo.cs b/OnTopic.AspNetCore.Mvc.IntegrationTests/Properties/AssemblyInfo.cs index 8c30842c..dbc8e0ea 100644 --- a/OnTopic.AspNetCore.Mvc.IntegrationTests/Properties/AssemblyInfo.cs +++ b/OnTopic.AspNetCore.Mvc.IntegrationTests/Properties/AssemblyInfo.cs @@ -3,7 +3,17 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ -using System; + +/*============================================================================================================================== +| USING DIRECTIVES (GLOBAL) +\-----------------------------------------------------------------------------------------------------------------------------*/ +global using Microsoft.AspNetCore.Mvc.Testing; +global using OnTopic.AspNetCore.Mvc.IntegrationTests.Host; +global using Xunit; + +/*============================================================================================================================== +| USING DIRECTIVES (LOCAL) +\-----------------------------------------------------------------------------------------------------------------------------*/ using System.Diagnostics.CodeAnalysis; using System.Runtime.InteropServices; diff --git a/OnTopic.AspNetCore.Mvc.IntegrationTests/ServiceCollectionExtensionsTests.cs b/OnTopic.AspNetCore.Mvc.IntegrationTests/ServiceCollectionExtensionsTests.cs index 5bb6a7d5..2ca3fba4 100644 --- a/OnTopic.AspNetCore.Mvc.IntegrationTests/ServiceCollectionExtensionsTests.cs +++ b/OnTopic.AspNetCore.Mvc.IntegrationTests/ServiceCollectionExtensionsTests.cs @@ -4,11 +4,8 @@ | Project Topics Library \=============================================================================================================================*/ using System; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Mvc.Testing; +using System.Net; using Microsoft.AspNetCore.Routing; -using OnTopic.AspNetCore.Mvc.IntegrationTests.Host; -using Xunit; namespace OnTopic.AspNetCore.Mvc.IntegrationTests { @@ -38,136 +35,118 @@ public ServiceCollectionExtensionsTests(WebApplicationFactory factory) } /*========================================================================================================================== - | TEST: MAP TOPIC ROUTE: RESPONDS TO REQUEST + | TEST: REQUEST PAGE: EXPECTED RESULTS \-------------------------------------------------------------------------------------------------------------------------*/ /// - /// Evaluates a route associated with and confirms that it responds appropriately. + /// Evaluates various routes enabled by the routing extension methods to ensure they correctly map to the expected + /// controllers, actions, and views. /// - [Fact] - public async Task MapTopicRoute_RespondsToRequest() { - - var client = _factory.CreateClient(); - var uri = new Uri($"/Web/ContentList/", UriKind.Relative); - var response = await client.GetAsync(uri).ConfigureAwait(false); - var content = await response.Content.ReadAsStringAsync().ConfigureAwait(false); - - response.EnsureSuccessStatusCode(); - - Assert.Equal("text/html; charset=utf-8", response.Content.Headers.ContentType?.ToString()); - Assert.Equal("~/Views/ContentList/ContentList.cshtml", content); - - } - - /*========================================================================================================================== - | TEST: MAP TOPIC AREA ROUTE: RESPONDS TO REQUEST - \-------------------------------------------------------------------------------------------------------------------------*/ - /// - /// Evaluates a route associated with - /// and confirms that it responds appropriately. - /// - [Fact] - public async Task MapTopicAreaRoute_RespondsToRequest() { + [Theory] + [InlineData("/Web/ContentList/", "~/Views/ContentList/ContentList.cshtml")] // MapTopicRoute() + [InlineData("/Area/Area/", "~/Areas/Area/Views/ContentType/ContentType.cshtml")] // MapTopicAreaRoute() + [InlineData("/Area/Controller/AreaAction/", "~/Areas/Area/Views/Controller/AreaAction.cshtml")] // MapTopicAreaRoute() + [InlineData("/Area/Accordion/", "~/Views/ContentList/Accordion.cshtml")] // MapImplicitAreaControllerRoute() + [InlineData("/Topic/3/", "~/Views/ContentList/ContentList.cshtml")] // MapTopicRedirect() + [InlineData("/Error/404", "400")] // MapTopicErrors() + [InlineData("/Error/Http/404", "400")] // MapDefaultControllerRoute() + [InlineData("/Error/Unauthorized/", "Unauthorized")] // MapTopicRoute() + public async Task RequestPage_ExpectedResults(string path, string expectedContent) { var client = _factory.CreateClient(); - var uri = new Uri($"/Area/Area/", UriKind.Relative); + var uri = new Uri(path, UriKind.Relative); var response = await client.GetAsync(uri).ConfigureAwait(false); - var content = await response.Content.ReadAsStringAsync().ConfigureAwait(false); + var actualContent = await response.Content.ReadAsStringAsync().ConfigureAwait(false); response.EnsureSuccessStatusCode(); Assert.Equal("text/html; charset=utf-8", response.Content.Headers.ContentType?.ToString()); - Assert.Equal("~/Areas/Area/Views/ContentType/ContentType.cshtml", content); + Assert.Equal(expectedContent, actualContent); } /*========================================================================================================================== - | TEST: MAP DEFAULT AREA CONTROLLER ROUTE: RESPONDS TO REQUEST + | TEST: MAP TOPIC SITEMAP: RESPONDS TO REQUEST \-------------------------------------------------------------------------------------------------------------------------*/ /// - /// Evaluates a route associated with and confirms that it responds appropriately. + /// Evaluates a route associated with and + /// confirms that it responds appropriately. /// [Fact] - public async Task MapDefaultAreaControllerRoute_RespondsToRequest() { + public async Task MapTopicSitemap_RespondsToRequest() { var client = _factory.CreateClient(); - var uri = new Uri($"/Area/Controller/AreaAction/", UriKind.Relative); + var uri = new Uri($"/Sitemap/", UriKind.Relative); var response = await client.GetAsync(uri).ConfigureAwait(false); var content = await response.Content.ReadAsStringAsync().ConfigureAwait(false); response.EnsureSuccessStatusCode(); - Assert.Equal("text/html; charset=utf-8", response.Content.Headers.ContentType?.ToString()); - Assert.Equal("~/Areas/Area/Views/Controller/AreaAction.cshtml", content); + Assert.Equal("text/xml", response.Content.Headers.ContentType?.ToString()); + Assert.True(content.Contains("/Web/ContentList/", StringComparison.OrdinalIgnoreCase)); } /*========================================================================================================================== - | TEST: MAP IMPLCIT AREA CONTROLLER ROUTE: RESPONDS TO REQUEST + | TEST: USE STATUS CODE PAGES: RETURNS EXPECTED STATUS CODE \-------------------------------------------------------------------------------------------------------------------------*/ /// - /// Evaluates a route associated with and confirms that it responds appropriately. + /// Evaluates a route with an error, and confirms that it returns a page with the expected status code. /// - [Fact] - public async Task MapImplicitAreaControllerRoute_RespondsToRequest() { + [Theory] + [InlineData("/MissingPage/", HttpStatusCode.NotFound, "400")] + [InlineData("/Web/MissingPage/", HttpStatusCode.NotFound, "400")] + [InlineData("/Web/Container/", HttpStatusCode.Forbidden, "400")] + [InlineData("/Scripts/ECMAScript.js", HttpStatusCode.NotFound, "The resource requested could not found.")] + public async Task UseStatusCodePages_ReturnsExpectedStatusCode(string path, HttpStatusCode statusCode, string expectedContent) { var client = _factory.CreateClient(); - var uri = new Uri($"/Area/Accordion/", UriKind.Relative); + var uri = new Uri(path, UriKind.Relative); var response = await client.GetAsync(uri).ConfigureAwait(false); - var content = await response.Content.ReadAsStringAsync().ConfigureAwait(false); + var actualContent = await response.Content.ReadAsStringAsync().ConfigureAwait(false); - response.EnsureSuccessStatusCode(); - - Assert.Equal("text/html; charset=utf-8", response.Content.Headers.ContentType?.ToString()); - Assert.Equal("~/Views/ContentList/Accordion.cshtml", content); + Assert.Equal(statusCode, response.StatusCode); + Assert.Equal(expectedContent, actualContent); } /*========================================================================================================================== - | TEST: MAP TOPIC SITEMAP: RESPONDS TO REQUEST + | TEST: USE RESPONSE CACHING: RETURNS CACHED PAGE \-------------------------------------------------------------------------------------------------------------------------*/ /// - /// Evaluates a route associated with and - /// confirms that it responds appropriately. + /// Evaluates a route with response caching, and confirms that the page remains unchanged after subsequent calls. /// - [Fact] - public async Task MapTopicSitemap_RespondsToRequest() { + /// + /// The Counter.cshtml page will increment a number output for every request to a given path. The CachedPage + /// request will not increment because the cached result is being returned; the UncachedPage will increment because + /// the results are not cached. + /// + [Theory] + [InlineData("/Web/CachedPage/", "1", "1", true)] + [InlineData("/Web/UncachedPage/", "1", "2", false)] + public async Task UseResponseCaching_ReturnsCachedPage( + string path, + string firstResult, + string secondResult, + bool validateHeaders + ) { var client = _factory.CreateClient(); - var uri = new Uri($"/Sitemap/", UriKind.Relative); - var response = await client.GetAsync(uri).ConfigureAwait(false); - var content = await response.Content.ReadAsStringAsync().ConfigureAwait(false); - - response.EnsureSuccessStatusCode(); - - Assert.Equal("text/xml", response.Content.Headers.ContentType?.ToString()); - Assert.True(content.Contains("/Web/ContentList/", StringComparison.OrdinalIgnoreCase)); - - } + var uri = new Uri(path, UriKind.Relative); - /*========================================================================================================================== - | TEST: MAP TOPIC REDIRECT: REDIRECTS REQUEST - \-------------------------------------------------------------------------------------------------------------------------*/ - /// - /// Evaluates a route associated with - /// and confirms that it responds appropriately. - /// - [Fact] - public async Task MapTopicRedirect_RedirectsRequest() { + var response1 = await client.GetAsync(uri).ConfigureAwait(false); + var content1 = await response1.Content.ReadAsStringAsync().ConfigureAwait(false); - var client = _factory.CreateClient(); - var uri = new Uri($"/Topic/3/", UriKind.Relative); - var response = await client.GetAsync(uri).ConfigureAwait(false); - var content = await response.Content.ReadAsStringAsync().ConfigureAwait(false); + var response2 = await client.GetAsync(uri).ConfigureAwait(false); + var content2 = await response2.Content.ReadAsStringAsync().ConfigureAwait(false); - response.EnsureSuccessStatusCode(); + response1.EnsureSuccessStatusCode(); - Assert.Equal("text/html; charset=utf-8", response.Content.Headers.ContentType?.ToString()); - Assert.Equal("~/Views/ContentList/ContentList.cshtml", content); + Assert.StartsWith(firstResult, content1, StringComparison.Ordinal); + Assert.StartsWith(secondResult, content2, StringComparison.Ordinal); + Assert.Equal(validateHeaders? true : null, response1.Headers.CacheControl?.Public); + Assert.Equal(validateHeaders? TimeSpan.FromSeconds(10) : null, response1?.Headers.CacheControl?.MaxAge); } - } -} \ No newline at end of file + } //Class +} //Namespace \ No newline at end of file diff --git a/OnTopic.AspNetCore.Mvc.IntegrationTests/TopicViewLocationExpanderTest.cs b/OnTopic.AspNetCore.Mvc.IntegrationTests/TopicViewLocationExpanderTest.cs index da6c9f39..5f7ab057 100644 --- a/OnTopic.AspNetCore.Mvc.IntegrationTests/TopicViewLocationExpanderTest.cs +++ b/OnTopic.AspNetCore.Mvc.IntegrationTests/TopicViewLocationExpanderTest.cs @@ -3,12 +3,7 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ -using System; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Mvc.Testing; using OnTopic.AspNetCore.Mvc.IntegrationTests.Areas.Area.Controllers; -using OnTopic.AspNetCore.Mvc.IntegrationTests.Host; -using Xunit; namespace OnTopic.AspNetCore.Mvc.IntegrationTests { diff --git a/OnTopic.AspNetCore.Mvc.IntegrationTests/TopicViewResultExecutorTest.cs b/OnTopic.AspNetCore.Mvc.IntegrationTests/TopicViewResultExecutorTest.cs index ea5b749e..c0729621 100644 --- a/OnTopic.AspNetCore.Mvc.IntegrationTests/TopicViewResultExecutorTest.cs +++ b/OnTopic.AspNetCore.Mvc.IntegrationTests/TopicViewResultExecutorTest.cs @@ -3,12 +3,7 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ -using System; using System.Net; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Mvc.Testing; -using OnTopic.AspNetCore.Mvc.IntegrationTests.Host; -using Xunit; namespace OnTopic.AspNetCore.Mvc.IntegrationTests { @@ -163,7 +158,7 @@ public async Task MissingView_ReturnsInternalServerError() { var uri = new Uri("/Web/MissingView/", UriKind.Relative); var response = await client.GetAsync(uri).ConfigureAwait(false); - Assert.Equal(HttpStatusCode.InternalServerError, response.StatusCode); + Assert.Equal(HttpStatusCode.InternalServerError, response.StatusCode); } diff --git a/OnTopic.AspNetCore.Mvc.Tests/ErrorControllerTest.cs b/OnTopic.AspNetCore.Mvc.Tests/ErrorControllerTest.cs new file mode 100644 index 00000000..365fdf87 --- /dev/null +++ b/OnTopic.AspNetCore.Mvc.Tests/ErrorControllerTest.cs @@ -0,0 +1,84 @@ +/*============================================================================================================================== +| Author Ignia, LLC +| Client Ignia, LLC +| Project Topics Library +\=============================================================================================================================*/ +using Microsoft.AspNetCore.Mvc; +using OnTopic.AspNetCore.Mvc; +using OnTopic.AspNetCore.Mvc.Controllers; +using OnTopic.AspNetCore.Mvc.Tests.TestDoubles; +using OnTopic.Data.Caching; +using OnTopic.Mapping; +using OnTopic.Repositories; +using OnTopic.ViewModels; + +namespace OnTopic.Tests { + + /*============================================================================================================================ + | CLASS: ERROR CONTROLLER TEST + \---------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Provides unit tests for the . + /// + [ExcludeFromCodeCoverage] + public class ErrorControllerTest: IClassFixture { + + /*========================================================================================================================== + | PRIVATE VARIABLES + \-------------------------------------------------------------------------------------------------------------------------*/ + readonly ITopicRepository _topicRepository; + readonly ITopicMappingService _topicMappingService; + readonly ControllerContext _context; + + /*========================================================================================================================== + | CONSTRUCTOR + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Initializes a new instance of the with shared resources. + /// + /// + /// This uses the to provide data, and then to + /// manage the in-memory representation of the data. While this introduces some overhead to the tests, the latter is a + /// relatively lightweight façade to any , and prevents the need to duplicate logic for + /// crawling the object graph. In addition, it initializes a shared reference to use for the various + /// tests. + /// + public ErrorControllerTest(TestTopicRepository topicRepository) { + + /*------------------------------------------------------------------------------------------------------------------------ + | Establish dependencies + \-----------------------------------------------------------------------------------------------------------------------*/ + _topicRepository = new CachedTopicRepository(topicRepository); + _topicMappingService = new TopicMappingService(_topicRepository, new TopicViewModelLookupService()); + _context = FakeControllerContext.GetControllerContext("Error"); + + } + + /*========================================================================================================================== + | TEST: ERROR CONTROLLER: HTTP: RETURNS EXPECTED ERROR + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Triggers the action with different status codes, and ensures + /// that the expected is returned in the . + /// + [Theory] + [InlineData(405, "405")] // Exact match + [InlineData(412, "400")] // Fallback to category + [InlineData(512, "Error")] // Fallback to root topic + public async void ErrorController_Http_ReturnsExpectedError(int errorCode, string expectedContent) { + + var controller = new ErrorController(_topicRepository, _topicMappingService) { + ControllerContext = new(_context) + }; + var result = await controller.HttpAsync(errorCode).ConfigureAwait(false) as TopicViewResult; + var model = result?.Model as PageTopicViewModel; + + controller.Dispose(); + + Assert.NotNull(result); + Assert.Equal(expectedContent, model?.Title); + + } + + } //Class +} //Namespace \ No newline at end of file diff --git a/OnTopic.AspNetCore.Mvc.Tests/OnTopic.AspNetCore.Mvc.Tests.csproj b/OnTopic.AspNetCore.Mvc.Tests/OnTopic.AspNetCore.Mvc.Tests.csproj index f453e416..e5a840a6 100644 --- a/OnTopic.AspNetCore.Mvc.Tests/OnTopic.AspNetCore.Mvc.Tests.csproj +++ b/OnTopic.AspNetCore.Mvc.Tests/OnTopic.AspNetCore.Mvc.Tests.csproj @@ -1,18 +1,18 @@  - net5.0 + net6.0 false - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + - + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/OnTopic.AspNetCore.Mvc.Tests/Properties/AssemblyInfo.cs b/OnTopic.AspNetCore.Mvc.Tests/Properties/AssemblyInfo.cs index c7c258e7..f107b131 100644 --- a/OnTopic.AspNetCore.Mvc.Tests/Properties/AssemblyInfo.cs +++ b/OnTopic.AspNetCore.Mvc.Tests/Properties/AssemblyInfo.cs @@ -3,8 +3,17 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ -using System; -using System.Diagnostics.CodeAnalysis; + +/*============================================================================================================================== +| USING DIRECTIVES (GLOBAL) +\-----------------------------------------------------------------------------------------------------------------------------*/ +global using System.Diagnostics.CodeAnalysis; +global using OnTopic.TestDoubles; +global using Xunit; + +/*============================================================================================================================== +| USING DIRECTIVES (LOCAL) +\-----------------------------------------------------------------------------------------------------------------------------*/ using System.Runtime.InteropServices; /*============================================================================================================================== diff --git a/OnTopic.AspNetCore.Mvc.Tests/SitemapControllerTest.cs b/OnTopic.AspNetCore.Mvc.Tests/SitemapControllerTest.cs new file mode 100644 index 00000000..607eb93d --- /dev/null +++ b/OnTopic.AspNetCore.Mvc.Tests/SitemapControllerTest.cs @@ -0,0 +1,232 @@ +/*============================================================================================================================== +| Author Ignia, LLC +| Client Ignia, LLC +| Project Topics Library +\=============================================================================================================================*/ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Controllers; +using Microsoft.AspNetCore.Routing; +using OnTopic.AspNetCore.Mvc.Controllers; +using OnTopic.AspNetCore.Mvc.Tests.TestDoubles; +using OnTopic.Data.Caching; +using OnTopic.Repositories; + +namespace OnTopic.Tests { + + /*============================================================================================================================ + | CLASS: SITEMAP CONTROLLER TEST + \---------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Provides unit tests for the . + /// + [ExcludeFromCodeCoverage] + public class SitemapControllerTest: IClassFixture { + + /*========================================================================================================================== + | PRIVATE VARIABLES + \-------------------------------------------------------------------------------------------------------------------------*/ + readonly ITopicRepository _topicRepository; + readonly ControllerContext _context; + + /*========================================================================================================================== + | CONSTRUCTOR + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Initializes a new instance of the with shared resources. + /// + /// + /// This uses the to provide data, and then to + /// manage the in-memory representation of the data. While this introduces some overhead to the tests, the latter is a + /// relatively lightweight façade to any , and prevents the need to duplicate logic for + /// crawling the object graph. In addition, it initializes a shared reference to use for the various + /// tests. + /// + public SitemapControllerTest(TestTopicRepository topicRepository) { + + /*------------------------------------------------------------------------------------------------------------------------ + | Establish dependencies + \-----------------------------------------------------------------------------------------------------------------------*/ + _topicRepository = new CachedTopicRepository(topicRepository); + + /*------------------------------------------------------------------------------------------------------------------------ + | Establish view model context + \-----------------------------------------------------------------------------------------------------------------------*/ + var routes = new RouteData(); + + routes.Values.Add("rootTopic", "Web"); + routes.Values.Add("path", "Web/Valid/Child/"); + + var actionContext = new ActionContext { + HttpContext = new DefaultHttpContext(), + RouteData = routes, + ActionDescriptor = new ControllerActionDescriptor() + }; + _context = new(actionContext); + + } + + /*========================================================================================================================== + | TEST: SITEMAP CONTROLLER: INDEX: RETURNS SITEMAP XML + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Triggers the index action of the action. + /// + [Fact] + public void SitemapController_Index_ReturnsSitemapXml() { + + var controller = new SitemapController(_topicRepository) { + ControllerContext = new(_context) + }; + var result = controller.Index() as ContentResult; + var model = result?.Content as string; + + controller.Dispose(); + + Assert.NotNull(model); + + Assert.StartsWith("", model, StringComparison.Ordinal); + Assert.Contains("/Web/Valid/Child/", model!, StringComparison.Ordinal); + + } + + /*========================================================================================================================== + | TEST: SITEMAP CONTROLLER: INDEX: EXCLUDES CONTENT TYPES + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Triggers the index action of the action and verifies that it + /// properly excludes List content types, and skips over Container and PageGroup. + /// + [Fact] + public void SitemapController_Index_ExcludesContentTypes() { + + var controller = new SitemapController(_topicRepository) { + ControllerContext = new(_context) + }; + var result = controller.Extended(true) as ContentResult; + var model = result?.Content as string; + + controller.Dispose(); + + Assert.NotNull(model); + Assert.False(model!.Contains("NestedTopics/", StringComparison.Ordinal)); + Assert.False(model!.Contains("NestedTopic/", StringComparison.Ordinal)); + Assert.False(model!.Contains("Redirect/", StringComparison.Ordinal)); + Assert.False(model!.Contains("NoIndex/", StringComparison.Ordinal)); + Assert.False(model!.Contains("Disabled/", StringComparison.Ordinal)); + Assert.False(model!.Contains("PageGroup/", StringComparison.Ordinal)); + + Assert.True(model!.Contains("PageGroupChild/", StringComparison.Ordinal)); + Assert.True(model!.Contains("NoIndexChild/", StringComparison.Ordinal)); + + } + + /*========================================================================================================================== + | TEST: SITEMAP CONTROLLER: INDEX: EXCLUDES CONTAINER DESCENDANTS + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Triggers the index action of the action and verifies that it + /// properly excludes the children of Container topics that are marked as NoIndex. + /// + [Fact] + public void SitemapController_Index_ExcludesContainerDescendants() { + + var controller = new SitemapController(_topicRepository) { + ControllerContext = new(_context) + }; + var result = controller.Extended(true) as ContentResult; + var model = result?.Content as string; + + controller.Dispose(); + + Assert.NotNull(model); + Assert.False(model!.Contains("NoIndexContainer/", StringComparison.Ordinal)); + Assert.False(model!.Contains("NoIndexContainerChild/", StringComparison.Ordinal)); + + } + + /*========================================================================================================================== + | TEST: SITEMAP CONTROLLER: INDEX: EXCLUDES PRIVATE BRANCHES + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Triggers the index action of the action and verifies that it + /// properly excludes the topics that are marked as IsPrivateBranch, including their descendants. + /// + [Fact] + public void SitemapController_Index_ExcludesPrivateBranches() { + + var controller = new SitemapController(_topicRepository) { + ControllerContext = new(_context) + }; + var result = controller.Extended(true) as ContentResult; + var model = result?.Content as string; + + controller.Dispose(); + + Assert.NotNull(model); + Assert.False(model!.Contains("PrivateBranch/", StringComparison.Ordinal)); + Assert.False(model!.Contains("PrivateBranchChild/", StringComparison.Ordinal)); + + } + + /*========================================================================================================================== + | TEST: SITEMAP CONTROLLER: EXTENDED: INCLUDES ATTRIBUTES + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Triggers the extended action of the action and ensures that the + /// results include the expected attributes. + /// + [Fact] + public void SitemapController_Extended_IncludesAttributes() { + + var controller = new SitemapController(_topicRepository) { + ControllerContext = new(_context) + }; + var result = controller.Extended(true) as ContentResult; + var model = result?.Content as string; + + controller.Dispose(); + + Assert.NotNull(model); + + Assert.Contains("", model, StringComparison.Ordinal); + Assert.Contains("/Web/Valid/Child/", model, StringComparison.Ordinal); + + Assert.Contains("Value", model, StringComparison.Ordinal); + Assert.Contains("Title", model, StringComparison.Ordinal); + Assert.Contains("", model, StringComparison.Ordinal); + Assert.Contains("Web:Redirect", model, StringComparison.Ordinal); + Assert.Contains("", model, StringComparison.Ordinal); + Assert.Contains("Web:Redirect", model, StringComparison.Ordinal); + + } + + /*========================================================================================================================== + | TEST: SITEMAP CONTROLLER: EXTENDED: EXCLUDES ATTRIBUTES + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Triggers the index action of the action and verifies that it + /// properly excludes e.g. the Body and IsHidden attributes. + /// + [Fact] + public void SitemapController_Index_ExcludesAttributes() { + + var controller = new SitemapController(_topicRepository) { + ControllerContext = new(_context) + }; + var result = controller.Extended(true) as ContentResult; + var model = result?.Content as string; + + controller.Dispose(); + + Assert.NotNull(model); + + Assert.False(model!.Contains("", StringComparison.Ordinal)); + Assert.False(model!.Contains("", StringComparison.Ordinal)); + Assert.False(model!.Contains("", StringComparison.Ordinal)); + Assert.False(model!.Contains("List", StringComparison.Ordinal)); + + } + + } //Class +} //Namespace \ No newline at end of file diff --git a/OnTopic.AspNetCore.Mvc.Tests/TestDoubles/FakeControllerContext.cs b/OnTopic.AspNetCore.Mvc.Tests/TestDoubles/FakeControllerContext.cs new file mode 100644 index 00000000..f18ec164 --- /dev/null +++ b/OnTopic.AspNetCore.Mvc.Tests/TestDoubles/FakeControllerContext.cs @@ -0,0 +1,61 @@ +/*============================================================================================================================== +| Author Ignia, LLC +| Client Ignia, LLC +| Project Topics Library +\=============================================================================================================================*/ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Controllers; +using Microsoft.AspNetCore.Routing; + +namespace OnTopic.AspNetCore.Mvc.Tests.TestDoubles { + + /*============================================================================================================================ + | CLASS: FAKE CONTROLLER CONTEXT + \---------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Provides a factory method for generating a fake for the purpose of testing controllers. + /// + [ExcludeFromCodeCoverage] + internal static class FakeControllerContext { + + /*========================================================================================================================== + | CONSTRUCTOR + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Creates a new instances of a . + /// + public static ControllerContext GetControllerContext(string rootTopic, string? path = null) { + + /*------------------------------------------------------------------------------------------------------------------------ + | Establish view model context + \-----------------------------------------------------------------------------------------------------------------------*/ + var routes = new RouteData(); + + /*------------------------------------------------------------------------------------------------------------------------ + | Set routes based on parameters + \-----------------------------------------------------------------------------------------------------------------------*/ + routes.Values.Add("rootTopic", rootTopic); + + if (path is not null) { + routes.Values.Add("path", path); + } + + /*------------------------------------------------------------------------------------------------------------------------ + | Create action context + \-----------------------------------------------------------------------------------------------------------------------*/ + var actionContext = new ActionContext { + HttpContext = new DefaultHttpContext(), + RouteData = routes, + ActionDescriptor = new ControllerActionDescriptor() + }; + + /*------------------------------------------------------------------------------------------------------------------------ + | Return new ControllerContext instance + \-----------------------------------------------------------------------------------------------------------------------*/ + return new(actionContext); + + } + + } //Class +} //Namespace diff --git a/OnTopic.AspNetCore.Mvc.Tests/TestDoubles/TestTopicRepository.cs b/OnTopic.AspNetCore.Mvc.Tests/TestDoubles/TestTopicRepository.cs index e922a2f3..614222e6 100644 --- a/OnTopic.AspNetCore.Mvc.Tests/TestDoubles/TestTopicRepository.cs +++ b/OnTopic.AspNetCore.Mvc.Tests/TestDoubles/TestTopicRepository.cs @@ -3,12 +3,10 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ -using System.Diagnostics.CodeAnalysis; using OnTopic.AspNetCore.Mvc.Controllers; using OnTopic.Attributes; using OnTopic.Internal.Diagnostics; using OnTopic.Repositories; -using OnTopic.TestDoubles; namespace OnTopic.AspNetCore.Mvc.Tests.TestDoubles { @@ -74,6 +72,15 @@ private static Topic CreateFakeData() { var pageGroup = new Topic("PageGroup", "PageGroup", rootTopic); _ = new Topic("PageGroupChild", "Page", pageGroup); + /*------------------------------------------------------------------------------------------------------------------------ + | Establish error topics + \-----------------------------------------------------------------------------------------------------------------------*/ + var error = new Topic("Error", "Page", rootTopic); + + _ = new Topic("400", "Page", error); + _ = new Topic("405", "Page", error); + _ = new Topic("Unauthorized", "Page", error); + /*------------------------------------------------------------------------------------------------------------------------ | Define attributes \-----------------------------------------------------------------------------------------------------------------------*/ diff --git a/OnTopic.AspNetCore.Mvc.Tests/TopicControllerTest.cs b/OnTopic.AspNetCore.Mvc.Tests/TopicControllerTest.cs index 51f400e7..64811049 100644 --- a/OnTopic.AspNetCore.Mvc.Tests/TopicControllerTest.cs +++ b/OnTopic.AspNetCore.Mvc.Tests/TopicControllerTest.cs @@ -3,22 +3,14 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ -using System; -using System.Diagnostics.CodeAnalysis; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Mvc.Controllers; -using Microsoft.AspNetCore.Routing; using OnTopic.AspNetCore.Mvc; using OnTopic.AspNetCore.Mvc.Controllers; using OnTopic.AspNetCore.Mvc.Tests.TestDoubles; using OnTopic.Data.Caching; using OnTopic.Mapping; using OnTopic.Repositories; -using OnTopic.TestDoubles; using OnTopic.ViewModels; -using Xunit; namespace OnTopic.Tests { @@ -61,21 +53,7 @@ public TopicControllerTest(TestTopicRepository topicRepository) { _topicRepository = new CachedTopicRepository(topicRepository); _topic = _topicRepository.Load("Root:Web:Valid:Child")!; _topicMappingService = new TopicMappingService(_topicRepository, new TopicViewModelLookupService()); - - /*------------------------------------------------------------------------------------------------------------------------ - | Establish view model context - \-----------------------------------------------------------------------------------------------------------------------*/ - var routes = new RouteData(); - - routes.Values.Add("rootTopic", "Web"); - routes.Values.Add("path", "Web/Valid/Child/"); - - var actionContext = new ActionContext { - HttpContext = new DefaultHttpContext(), - RouteData = routes, - ActionDescriptor = new ControllerActionDescriptor() - }; - _context = new(actionContext); + _context = FakeControllerContext.GetControllerContext("Web", "Web/Valid/Child/"); } @@ -142,167 +120,5 @@ public void RedirectController_TopicRedirect_ReturnsNotFoundObjectResult() { } - /*========================================================================================================================== - | TEST: SITEMAP CONTROLLER: INDEX: RETURNS SITEMAP XML - \-------------------------------------------------------------------------------------------------------------------------*/ - /// - /// Triggers the index action of the action. - /// - [Fact] - public void SitemapController_Index_ReturnsSitemapXml() { - - var controller = new SitemapController(_topicRepository) { - ControllerContext = new(_context) - }; - var result = controller.Index() as ContentResult; - var model = result?.Content as string; - - controller.Dispose(); - - Assert.NotNull(model); - - Assert.StartsWith("", model, StringComparison.Ordinal); - Assert.Contains("/Web/Valid/Child/", model!, StringComparison.Ordinal); - - } - - /*========================================================================================================================== - | TEST: SITEMAP CONTROLLER: INDEX: EXCLUDES CONTENT TYPES - \-------------------------------------------------------------------------------------------------------------------------*/ - /// - /// Triggers the index action of the action and verifies that it - /// properly excludes List content types, and skips over Container and PageGroup. - /// - [Fact] - public void SitemapController_Index_ExcludesContentTypes() { - - var controller = new SitemapController(_topicRepository) { - ControllerContext = new(_context) - }; - var result = controller.Extended(true) as ContentResult; - var model = result?.Content as string; - - controller.Dispose(); - - Assert.NotNull(model); - Assert.False(model!.Contains("NestedTopics/", StringComparison.Ordinal)); - Assert.False(model!.Contains("NestedTopic/", StringComparison.Ordinal)); - Assert.False(model!.Contains("Redirect/", StringComparison.Ordinal)); - Assert.False(model!.Contains("NoIndex/", StringComparison.Ordinal)); - Assert.False(model!.Contains("Disabled/", StringComparison.Ordinal)); - Assert.False(model!.Contains("PageGroup/", StringComparison.Ordinal)); - - Assert.True(model!.Contains("PageGroupChild/", StringComparison.Ordinal)); - Assert.True(model!.Contains("NoIndexChild/", StringComparison.Ordinal)); - - } - - /*========================================================================================================================== - | TEST: SITEMAP CONTROLLER: INDEX: EXCLUDES CONTAINER DESCENDANTS - \-------------------------------------------------------------------------------------------------------------------------*/ - /// - /// Triggers the index action of the action and verifies that it - /// properly excludes the children of Container topics that are marked as NoIndex. - /// - [Fact] - public void SitemapController_Index_ExcludesContainerDescendants() { - - var controller = new SitemapController(_topicRepository) { - ControllerContext = new(_context) - }; - var result = controller.Extended(true) as ContentResult; - var model = result?.Content as string; - - controller.Dispose(); - - Assert.NotNull(model); - Assert.False(model!.Contains("NoIndexContainer/", StringComparison.Ordinal)); - Assert.False(model!.Contains("NoIndexContainerChild/", StringComparison.Ordinal)); - - } - - /*========================================================================================================================== - | TEST: SITEMAP CONTROLLER: INDEX: EXCLUDES PRIVATE BRANCHES - \-------------------------------------------------------------------------------------------------------------------------*/ - /// - /// Triggers the index action of the action and verifies that it - /// properly excludes the topics that are marked as IsPrivateBranch, including their descendants. - /// - [Fact] - public void SitemapController_Index_ExcludesPrivateBranches() { - - var controller = new SitemapController(_topicRepository) { - ControllerContext = new(_context) - }; - var result = controller.Extended(true) as ContentResult; - var model = result?.Content as string; - - controller.Dispose(); - - Assert.NotNull(model); - Assert.False(model!.Contains("PrivateBranch/", StringComparison.Ordinal)); - Assert.False(model!.Contains("PrivateBranchChild/", StringComparison.Ordinal)); - - } - - /*========================================================================================================================== - | TEST: SITEMAP CONTROLLER: EXTENDED: INCLUDES ATTRIBUTES - \-------------------------------------------------------------------------------------------------------------------------*/ - /// - /// Triggers the extended action of the action and ensures that the - /// results include the expected attributes. - /// - [Fact] - public void SitemapController_Extended_IncludesAttributes() { - - var controller = new SitemapController(_topicRepository) { - ControllerContext = new(_context) - }; - var result = controller.Extended(true) as ContentResult; - var model = result?.Content as string; - - controller.Dispose(); - - Assert.NotNull(model); - - Assert.Contains("", model, StringComparison.Ordinal); - Assert.Contains("/Web/Valid/Child/", model, StringComparison.Ordinal); - - Assert.Contains("Value", model, StringComparison.Ordinal); - Assert.Contains("Title", model, StringComparison.Ordinal); - Assert.Contains("", model, StringComparison.Ordinal); - Assert.Contains("Web:Redirect", model, StringComparison.Ordinal); - Assert.Contains("", model, StringComparison.Ordinal); - Assert.Contains("Web:Redirect", model, StringComparison.Ordinal); - - } - - /*========================================================================================================================== - | TEST: SITEMAP CONTROLLER: EXTENDED: EXCLUDES ATTRIBUTES - \-------------------------------------------------------------------------------------------------------------------------*/ - /// - /// Triggers the index action of the action and verifies that it - /// properly excludes e.g. the Body and IsHidden attributes. - /// - [Fact] - public void SitemapController_Index_ExcludesAttributes() { - - var controller = new SitemapController(_topicRepository) { - ControllerContext = new(_context) - }; - var result = controller.Extended(true) as ContentResult; - var model = result?.Content as string; - - controller.Dispose(); - - Assert.NotNull(model); - - Assert.False(model!.Contains("", StringComparison.Ordinal)); - Assert.False(model!.Contains("", StringComparison.Ordinal)); - Assert.False(model!.Contains("", StringComparison.Ordinal)); - Assert.False(model!.Contains("List", StringComparison.Ordinal)); - - } - } //Class } //Namespace \ No newline at end of file diff --git a/OnTopic.AspNetCore.Mvc.Tests/TopicRepositoryExtensionsTest.cs b/OnTopic.AspNetCore.Mvc.Tests/TopicRepositoryExtensionsTest.cs index e97902b1..4b70e8f7 100644 --- a/OnTopic.AspNetCore.Mvc.Tests/TopicRepositoryExtensionsTest.cs +++ b/OnTopic.AspNetCore.Mvc.Tests/TopicRepositoryExtensionsTest.cs @@ -3,13 +3,10 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ -using System.Diagnostics.CodeAnalysis; using Microsoft.AspNetCore.Routing; using OnTopic.AspNetCore.Mvc; using OnTopic.Data.Caching; using OnTopic.Repositories; -using OnTopic.TestDoubles; -using Xunit; namespace OnTopic.Tests { @@ -62,7 +59,7 @@ public void Load_ByRoute_ReturnsTopic() { var currentTopic = _topicRepository.Load(routes); Assert.NotNull(currentTopic); - Assert.Equal(topic, currentTopic); + Assert.Equal(topic, currentTopic); Assert.Equal("Web_0_1_1", currentTopic?.Key); } @@ -84,7 +81,7 @@ public void Load_ByRoute_ReturnsRootTopic() { var currentTopic = _topicRepository.Load(routes); Assert.NotNull(currentTopic); - Assert.Equal(topic, currentTopic); + Assert.Equal(topic, currentTopic); Assert.Equal("Root", currentTopic?.Key); } diff --git a/OnTopic.AspNetCore.Mvc.Tests/TopicViewComponentTest.cs b/OnTopic.AspNetCore.Mvc.Tests/TopicViewComponentTest.cs index 54202191..b40fc646 100644 --- a/OnTopic.AspNetCore.Mvc.Tests/TopicViewComponentTest.cs +++ b/OnTopic.AspNetCore.Mvc.Tests/TopicViewComponentTest.cs @@ -3,9 +3,6 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ -using System; -using System.Diagnostics.CodeAnalysis; -using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc.Rendering; using Microsoft.AspNetCore.Mvc.ViewComponents; @@ -17,9 +14,7 @@ using OnTopic.Mapping; using OnTopic.Mapping.Hierarchical; using OnTopic.Repositories; -using OnTopic.TestDoubles; using OnTopic.ViewModels; -using Xunit; namespace OnTopic.Tests { @@ -126,12 +121,12 @@ public async Task Menu_Invoke_ReturnsNavigationViewModel() { var result = await viewComponent.InvokeAsync().ConfigureAwait(false); var concreteResult = result as ViewViewComponentResult; - var model = concreteResult?.ViewData.Model as NavigationViewModel; + var model = concreteResult?.ViewData?.Model as NavigationViewModel; Assert.NotNull(model); Assert.Equal(_topic.GetWebPath(), model?.CurrentWebPath); Assert.Equal("/Web/", model?.NavigationRoot?.WebPath); - Assert.Equal(3, model?.NavigationRoot?.Children.Count); + Assert.Equal(3, model?.NavigationRoot?.Children.Count); } @@ -152,7 +147,7 @@ public async Task Menu_Invoke_ReturnsConfiguredNavigationRoot() { var result = await viewComponent.InvokeAsync().ConfigureAwait(false); var concreteResult = result as ViewViewComponentResult; - var model = concreteResult?.ViewData.Model as NavigationViewModel; + var model = concreteResult?.ViewData?.Model as NavigationViewModel; Assert.NotNull(model); Assert.Equal(webPath, model?.CurrentWebPath); @@ -199,12 +194,12 @@ public async Task PageLevelNavigation_Invoke_ReturnsNavigationViewModel() { var result = await viewComponent.InvokeAsync().ConfigureAwait(false); var concreteResult = result as ViewViewComponentResult; - var model = concreteResult?.ViewData.Model as NavigationViewModel; + var model = concreteResult?.ViewData?.Model as NavigationViewModel; Assert.NotNull(model); Assert.Equal(_topic.GetWebPath(), model?.CurrentWebPath); Assert.Equal("/Web/Web_3/", model?.NavigationRoot?.WebPath); - Assert.Equal(2, model?.NavigationRoot?.Children.Count); + Assert.Equal(2, model?.NavigationRoot?.Children.Count); Assert.True(model?.NavigationRoot?.IsSelected(_topic.GetWebPath())?? false); } @@ -227,7 +222,7 @@ public async Task PageLevelNavigation_Invoke_ReturnsNull() { var result = await viewComponent.InvokeAsync().ConfigureAwait(false); var concreteResult = result as ViewViewComponentResult; - var model = concreteResult?.ViewData.Model as NavigationViewModel; + var model = concreteResult?.ViewData?.Model as NavigationViewModel; Assert.NotNull(model); Assert.Equal(webPath, model?.CurrentWebPath); @@ -255,7 +250,7 @@ public async Task PageLevelNavigation_InvokeWithNullTopic_ReturnsNull() var result = await viewComponent.InvokeAsync().ConfigureAwait(false); var concreteResult = result as ViewViewComponentResult; - var model = concreteResult?.ViewData.Model as NavigationViewModel; + var model = concreteResult?.ViewData?.Model as NavigationViewModel; Assert.NotNull(model); Assert.Equal(String.Empty, model?.CurrentWebPath); diff --git a/OnTopic.AspNetCore.Mvc.Tests/ValidateTopicAttributeTest.cs b/OnTopic.AspNetCore.Mvc.Tests/ValidateTopicAttributeTest.cs index 3a67d0fe..7fe6ea4f 100644 --- a/OnTopic.AspNetCore.Mvc.Tests/ValidateTopicAttributeTest.cs +++ b/OnTopic.AspNetCore.Mvc.Tests/ValidateTopicAttributeTest.cs @@ -3,9 +3,6 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ -using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Controllers; @@ -15,8 +12,6 @@ using OnTopic.AspNetCore.Mvc.Controllers; using OnTopic.Attributes; using OnTopic.Metadata; -using OnTopic.TestDoubles; -using Xunit; namespace OnTopic.Tests { @@ -49,7 +44,7 @@ public static ActionExecutingContext GetActionExecutingContext(Controller contro var actionExecutingContext = new ActionExecutingContext( actionContext, new List(), - new Dictionary(), + new Dictionary(), controller ); @@ -116,7 +111,7 @@ public void InvalidControllerType_ThrowsException() { | TEST: NULL TOPIC: RETURNS NOT FOUND \-------------------------------------------------------------------------------------------------------------------------*/ /// - /// Ensures that a is thrown if the is null. + /// Ensures that a is thrown if the is null. /// [Fact] public void NullTopic_ReturnsNotFound() { @@ -129,7 +124,7 @@ public void NullTopic_ReturnsNotFound() { controller.Dispose(); - Assert.IsType(context.Result); + Assert.IsType(context.Result); } @@ -204,7 +199,7 @@ public void NestedTopic_List_Returns403() { var result = context.Result as StatusCodeResult; Assert.NotNull(result); - Assert.Equal(403, result?.StatusCode); + Assert.Equal(403, result?.StatusCode); } @@ -231,7 +226,7 @@ public void NestedTopic_Item_Returns403() { var result = context.Result as StatusCodeResult; Assert.NotNull(result); - Assert.Equal(403, result?.StatusCode); + Assert.Equal(403, result?.StatusCode); } @@ -257,7 +252,7 @@ public void Container_Returns403() { var result = context.Result as StatusCodeResult; Assert.NotNull(result); - Assert.Equal(403, result?.StatusCode); + Assert.Equal(403, result?.StatusCode); } @@ -309,7 +304,7 @@ public void PageGroupTopic_Empty_ReturnsRedirect() { var result = context.Result as StatusCodeResult; Assert.NotNull(result); - Assert.Equal(403, result?.StatusCode); + Assert.Equal(403, result?.StatusCode); } diff --git a/OnTopic.AspNetCore.Mvc/Components/MenuViewComponentBase{T}.cs b/OnTopic.AspNetCore.Mvc/Components/MenuViewComponentBase{T}.cs index 77773cfe..1d2d6667 100644 --- a/OnTopic.AspNetCore.Mvc/Components/MenuViewComponentBase{T}.cs +++ b/OnTopic.AspNetCore.Mvc/Components/MenuViewComponentBase{T}.cs @@ -3,15 +3,10 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ -using System; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Mvc; using OnTopic.AspNetCore.Mvc.Controllers; using OnTopic.AspNetCore.Mvc.Models; -using OnTopic.Internal.Diagnostics; using OnTopic.Mapping.Hierarchical; using OnTopic.Models; -using OnTopic.Repositories; namespace OnTopic.AspNetCore.Mvc.Components { diff --git a/OnTopic.AspNetCore.Mvc/Components/NavigationTopicViewComponentBase{T}.cs b/OnTopic.AspNetCore.Mvc/Components/NavigationTopicViewComponentBase{T}.cs index f4246d9e..8ca30e62 100644 --- a/OnTopic.AspNetCore.Mvc/Components/NavigationTopicViewComponentBase{T}.cs +++ b/OnTopic.AspNetCore.Mvc/Components/NavigationTopicViewComponentBase{T}.cs @@ -3,11 +3,8 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ -using System; -using Microsoft.AspNetCore.Mvc; using OnTopic.Mapping.Hierarchical; using OnTopic.Models; -using OnTopic.Repositories; namespace OnTopic.AspNetCore.Mvc.Components { diff --git a/OnTopic.AspNetCore.Mvc/Components/PageLevelNavigationViewComponentBase{T}.cs b/OnTopic.AspNetCore.Mvc/Components/PageLevelNavigationViewComponentBase{T}.cs index 5b82579a..4b6e2528 100644 --- a/OnTopic.AspNetCore.Mvc/Components/PageLevelNavigationViewComponentBase{T}.cs +++ b/OnTopic.AspNetCore.Mvc/Components/PageLevelNavigationViewComponentBase{T}.cs @@ -3,14 +3,10 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ -using System; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Mvc; using OnTopic.AspNetCore.Mvc.Controllers; using OnTopic.AspNetCore.Mvc.Models; using OnTopic.Mapping.Hierarchical; using OnTopic.Models; -using OnTopic.Repositories; namespace OnTopic.AspNetCore.Mvc.Components { diff --git a/OnTopic.AspNetCore.Mvc/Controllers/ErrorController.cs b/OnTopic.AspNetCore.Mvc/Controllers/ErrorController.cs new file mode 100644 index 00000000..3d900fdf --- /dev/null +++ b/OnTopic.AspNetCore.Mvc/Controllers/ErrorController.cs @@ -0,0 +1,84 @@ +/*============================================================================================================================== +| Author Ignia, LLC +| Client Ignia, LLC +| Project Topics Library +\=============================================================================================================================*/ +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Diagnostics; +using OnTopic.Mapping; + +namespace OnTopic.AspNetCore.Mvc.Controllers { + + /*============================================================================================================================ + | CLASS: ERROR CONTROLLER + \---------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Provides a default handler for responding to requests from the configuration. + /// + /// + /// The will redirect to a URL with the + /// HTTP error code in the route. This is fine if there is one error page that, perhaps, injects the error code into the + /// content. It's also fine if there is an error page for every HTTP error. In practice, however, many sites handle some + /// HTTP errors, but not others. Given this, the provides logic to deliver a associated with the HTTP error, if available, and otherwise to fallback first to the + /// HTTP category (e.g., 5xx), and otherwise to a generic error. + /// + public class ErrorController : TopicController { + + /*========================================================================================================================== + | CONSTRUCTOR + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Initializes a new instance of an Error Controller with necessary dependencies. + /// + /// An error controller for loading views associated with HTTP error codes. + public ErrorController( + ITopicRepository topicRepository, + ITopicMappingService topicMappingService + ) : base( + topicRepository, + topicMappingService + ) { } + + /*========================================================================================================================== + | GET: HTTP ERROR (VIEW TOPIC) + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Evaluates the against configured s, and returns the most appropriate + /// content available. + /// + /// A view associated with the requested current . + public async virtual Task HttpAsync([FromRoute(Name="id")] int statusCode, bool includeStaticFiles = true) { + + /*------------------------------------------------------------------------------------------------------------------------ + | Bypass for resources + \-----------------------------------------------------------------------------------------------------------------------*/ + if (!includeStaticFiles) { + var feature = HttpContext.Features.Get(); + if (feature?.OriginalPath.Contains('.', StringComparison.Ordinal)?? false) { + return Content("The resource requested could not found."); + } + } + + /*------------------------------------------------------------------------------------------------------------------------ + | Identify base path + \-----------------------------------------------------------------------------------------------------------------------*/ + var rootTopic = HttpContext.Request.RouteValues.GetValueOrDefault("rootTopic") as string?? "Error"; + + /*------------------------------------------------------------------------------------------------------------------------ + | Identify relevant topic + \-----------------------------------------------------------------------------------------------------------------------*/ + CurrentTopic = TopicRepository.Load($"{rootTopic}:{statusCode}")?? + TopicRepository.Load($"{rootTopic}:{statusCode/100*100}")?? + TopicRepository.Load($"{rootTopic}"); + + /*------------------------------------------------------------------------------------------------------------------------ + | Return topic view + \-----------------------------------------------------------------------------------------------------------------------*/ + return await IndexAsync(CurrentTopic?.GetWebPath()?? "Error").ConfigureAwait(false); + + } + + } //Class +} //Namespace \ No newline at end of file diff --git a/OnTopic.AspNetCore.Mvc/Controllers/RedirectController.cs b/OnTopic.AspNetCore.Mvc/Controllers/RedirectController.cs index 88e1bc93..33de99c0 100644 --- a/OnTopic.AspNetCore.Mvc/Controllers/RedirectController.cs +++ b/OnTopic.AspNetCore.Mvc/Controllers/RedirectController.cs @@ -3,8 +3,6 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ -using Microsoft.AspNetCore.Mvc; -using OnTopic.Repositories; namespace OnTopic.AspNetCore.Mvc.Controllers { diff --git a/OnTopic.AspNetCore.Mvc/Controllers/SitemapController.cs b/OnTopic.AspNetCore.Mvc/Controllers/SitemapController.cs index d7640805..ec21a475 100644 --- a/OnTopic.AspNetCore.Mvc/Controllers/SitemapController.cs +++ b/OnTopic.AspNetCore.Mvc/Controllers/SitemapController.cs @@ -3,17 +3,11 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ -using System; -using System.Collections.Generic; using System.Collections.ObjectModel; using System.Globalization; -using System.Linq; using System.Xml; using System.Xml.Linq; -using Microsoft.AspNetCore.Mvc; using OnTopic.Attributes; -using OnTopic.Internal.Diagnostics; -using OnTopic.Repositories; namespace OnTopic.AspNetCore.Mvc.Controllers { diff --git a/OnTopic.AspNetCore.Mvc/Controllers/TopicController.cs b/OnTopic.AspNetCore.Mvc/Controllers/TopicController.cs index 32cc1ef5..e6fbda4d 100644 --- a/OnTopic.AspNetCore.Mvc/Controllers/TopicController.cs +++ b/OnTopic.AspNetCore.Mvc/Controllers/TopicController.cs @@ -3,12 +3,7 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ -using System.Diagnostics.CodeAnalysis; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Mvc; -using OnTopic.Internal.Diagnostics; using OnTopic.Mapping; -using OnTopic.Repositories; namespace OnTopic.AspNetCore.Mvc.Controllers { @@ -20,6 +15,7 @@ namespace OnTopic.AspNetCore.Mvc.Controllers { /// identifying the topic associated with the given path, determining its content type, and returning a view associated with /// that content type (with potential overrides for multiple views). /// + [TopicResponseCache] public class TopicController : Controller { /*========================================================================================================================== @@ -61,7 +57,7 @@ ITopicMappingService topicMappingService /// Provides a reference to the Topic Repository in order to gain arbitrary access to the entire topic graph. /// /// The TopicRepository associated with the controller. - protected ITopicRepository TopicRepository { get; } + protected internal ITopicRepository TopicRepository { get; } /*========================================================================================================================== | CURRENT TOPIC diff --git a/OnTopic.AspNetCore.Mvc/Models/NavigationViewModel{T}.cs b/OnTopic.AspNetCore.Mvc/Models/NavigationViewModel{T}.cs index ac53fd40..73c067db 100644 --- a/OnTopic.AspNetCore.Mvc/Models/NavigationViewModel{T}.cs +++ b/OnTopic.AspNetCore.Mvc/Models/NavigationViewModel{T}.cs @@ -3,8 +3,6 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ -using System; -using System.Diagnostics.CodeAnalysis; using OnTopic.AspNetCore.Mvc.Components; using OnTopic.Models; @@ -77,7 +75,7 @@ public class NavigationViewModel where T : class, IHierarchicalTopicViewModel /// /// [ExcludeFromCodeCoverage] - [Obsolete("The CurrentKey property has been replaced in favor of CurrentWebPath.", true)] + [Obsolete($"The {nameof(CurrentKey)} property has been replaced in favor of {nameof(CurrentWebPath)}.", true)] public string CurrentKey { get; set; } = default!; } //Class diff --git a/OnTopic.AspNetCore.Mvc/OnTopic.AspNetCore.Mvc.csproj b/OnTopic.AspNetCore.Mvc/OnTopic.AspNetCore.Mvc.csproj index 75f55334..79ae89af 100644 --- a/OnTopic.AspNetCore.Mvc/OnTopic.AspNetCore.Mvc.csproj +++ b/OnTopic.AspNetCore.Mvc/OnTopic.AspNetCore.Mvc.csproj @@ -15,15 +15,15 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/OnTopic.AspNetCore.Mvc/Properties/AssemblyInfo.cs b/OnTopic.AspNetCore.Mvc/Properties/AssemblyInfo.cs index cbe1467f..7a27b9af 100644 --- a/OnTopic.AspNetCore.Mvc/Properties/AssemblyInfo.cs +++ b/OnTopic.AspNetCore.Mvc/Properties/AssemblyInfo.cs @@ -3,7 +3,18 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ -using System; + +/*============================================================================================================================== +| USING DIRECTIVES (GLOBAL) +\-----------------------------------------------------------------------------------------------------------------------------*/ +global using Microsoft.AspNetCore.Mvc; +global using System.Diagnostics.CodeAnalysis; +global using OnTopic.Internal.Diagnostics; +global using OnTopic.Repositories; + +/*============================================================================================================================== +| USING DIRECTIVES (LOCAL) +\-----------------------------------------------------------------------------------------------------------------------------*/ using System.Runtime.InteropServices; /*============================================================================================================================== diff --git a/OnTopic.AspNetCore.Mvc/README.md b/OnTopic.AspNetCore.Mvc/README.md index 819fc2f9..c977a569 100644 --- a/OnTopic.AspNetCore.Mvc/README.md +++ b/OnTopic.AspNetCore.Mvc/README.md @@ -1,5 +1,5 @@ -# OnTopic for ASP.NET Core 3.x, 5.x -The `OnTopic.AspNetCore.Mvc` assembly provides a default implementation for utilizing OnTopic with ASP.NET Core 3.x and ASP.NET Core 5.x. It is the recommended client for working with OnTopic. +# OnTopic for ASP.NET Core +The `OnTopic.AspNetCore.Mvc` assembly provides a default implementation for utilizing OnTopic with ASP.NET Core (3.0 and above) . It is the recommended client for working with OnTopic. [![OnTopic.AspNetCore.Mvc package in Internal feed in Azure Artifacts](https://igniasoftware.feeds.visualstudio.com/_apis/public/Packaging/Feeds/46d5f49c-5e1e-47bb-8b14-43be6c719ba8/Packages/4db5e20c-69c6-4134-823a-c3de06d1176e/Badge)](https://www.nuget.org/packages/OnTopic.AspNetCore.Mvc/) [![Build Status](https://igniasoftware.visualstudio.com/OnTopic/_apis/build/status/OnTopic-CI-V3?branchName=master)](https://igniasoftware.visualstudio.com/OnTopic/_build/latest?definitionId=7&branchName=master) @@ -8,6 +8,7 @@ The `OnTopic.AspNetCore.Mvc` assembly provides a default implementation for util ### Contents - [Components](#components) - [Controllers and View Components](#controllers-and-view-components) +- [Filters](#filters) - [View Conventions](#view-conventions) - [View Matching](#view-matching) - [View Locations](#view-locations) @@ -17,9 +18,10 @@ The `OnTopic.AspNetCore.Mvc` assembly provides a default implementation for util - [Application](#application) - [Route Configuration](#route-configuration) - [Composition Root](#composition-root) + - [Error Handling](#error-handling) ## Components -There are five key components at the heart of the ASP.NET Core implementation. +There are six components at the heart of the ASP.NET Core implementation. - **`TopicController`**: This is a default controller instance that can be used for _any_ topic path. It will automatically validate that the `Topic` exists, that it is not disabled (`!IsDisabled`), and will honor any redirects (e.g., if the `Url` attribute is filled out). Otherwise, it will return a `TopicViewResult` based on a view model, view name, and content type. - **`TopicRouteValueTransformer`**: A `DynamicRouteValueTransformer` for use with the ASP.NET Core's `MapDynamicControllerRoute()` method, allowing for route parameters to be implicitly inferred; notably, it will use the `area` as the default `controller` and `rootTopic`, if those route parameters are not otherwise defined. - **`TopicViewLocationExpander`**: Assists the out-of-the-box Razor view engine in locating views associated with OnTopic, e.g. by looking in `~/Views/ContentTypes/{ContentType}.cshtml`, or `~/Views/{ContentType}/{View}.cshtml`. See [View Locations](#view-locations) below. @@ -29,6 +31,7 @@ There are five key components at the heart of the ASP.NET Core implementation. ## Controllers and View Components There are five main controllers and view components that ship with the ASP.NET Core implementation. In addition to the core **`TopicController`**, these include the following ancillary classes: +- **[`ErrorController`](Controllers/ErrorController.cs)**: Provides a specialized `TopicController` with an `Http()` action for handling status code errors (e.g., from `UseStatusCodePages()`). - **[`RedirectController`](Controllers/RedirectController.cs)**: Provides a single `Redirect` action which can be bound to a route such as `/Topic/{ID}/`; this provides support for permanent URLs that are independent of the `GetWebPath()`. - **[`SitemapController`](Controllers/SitemapController.cs)**: Provides a single `Sitemap` action which recurses over the entire Topic graph, including all attributes, and returns an XML document with a sitemaps.org schema. - **[`MenuViewComponentBase`](Components/MenuViewComponentBase{T}.cs)**: Provides support for a navigation menu by automatically mapping the top three tiers of the current namespace (e.g., `Web`, its children, and grandchildren). Can accept any `INavigationTopicViewModel` as a generic argument; that will be used as the view model for each mapped instance. @@ -43,6 +46,11 @@ There are five main controllers and view components that ship with the ASP.NET C > ): base(topicRepository, hierarchicalTopicMappingService) {} > } +## Filters +There are two filters included with the ASP.NET Core implementation, which are meant to work in conjunction with `TopicController`: +- **[`[ValidateTopic]`](_filters/ValidateTopicAttribute.cs)**: A filter attribute that handles topics that aren't intended to be served publicly, such as `PageGroup` and `Container` content types, or topics with `Url` or `IsDisabled` set. +- **[`[TopicResponseCache]`](_filters/TopicResponseCacheAttribute.cs)**: A filter attribute registered on `TopicController` which checks for an affiliated `CacheProfile` topic and sets HTTP response headers accordingly. Compatible with the [ASP.NET Core Response Caching Middleware](https://docs.microsoft.com/en-us/aspnet/core/performance/caching/middleware). + ## View Conventions By default, OnTopic matches views based on the current topic's `ContentType` and, if available, `View`. @@ -114,6 +122,8 @@ public class Startup { ``` > *Note:* This will register the `TopicViewLocationExpander`, `TopicViewResultExecutor`, `TopicRouteValueTransformer`, as well as all [Controllers](#controllers) that ship with `OnTopic.AspNetCore.Mvc`. +> *Note:* When using ASP.NET Core 6's minimal hosting model, this will instead be placed in the `Program` class as a top-level statement. + In addition, within the same `ConfigureServices()` method, you will need to establish a class that implements `IControllerActivator` and `IViewComponentActivator`, and will represent the site's _Composition Root_ for dependency injection. This will typically look like: ```csharp var activator = new OrganizationNameActivator(Configuration.GetConnectionString("OnTopic")) @@ -138,6 +148,7 @@ public class Startup { endpoints.MapDefaultControllerRoute(); // {controller=Home}/{action=Index}/{id?} endpoints.MapDefaultAreaControllerRoute(); // {area:exists}/{controller}/{action=Index}/{id?} + endpoints.MapTopicErrors(); // Error/{errorCode} endpoints.MapTopicRoute("Web"); // Web/{**path} endpoints.MapTopicRedirect(); // Topic/{topicId} endpoints.MapControllers(); @@ -148,6 +159,8 @@ public class Startup { ``` > *Note:* Because OnTopic relies on wildcard path names, a new route should be configured for every root namespace (e.g., `/Web`). While it's possible to configure OnTopic to evaluate _all_ paths, this makes it difficult to delegate control to other controllers and handlers, when necessary. As a result, it is recommended that each root container be registered individually. +> *Note:* When using ASP.NET Core 6's minimal hosting model, these will instead be placed in the `Program` class as a top-level statement. + ### Composition Root As OnTopic relies on constructor injection, the application must be configured in a **Composition Root**—in the case of ASP.NET Core, that means a custom controller activator for controllers, and view component activator for view components. For controllers, the basic structure of this might look like: ```csharp @@ -165,4 +178,32 @@ return controllerType.Name switch { ``` For a complete reference template, including the ancillary controllers, view components, and a more maintainable structure, see the [`OrganizationNameActivator.cs`](https://gist.github.com/JeremyCaney/00c04b1b9f40d9743793cd45dfaaa606) Gist. Optionally, you may use a dependency injection container. -> *Note:* The default `TopicController` will automatically identify the current topic (based on the `RouteData`), map the current topic to a corresponding view model (based on [the `TopicMappingService` conventions](../OnTopic/Mapping/README.md)), and then return a corresponding view (based on the [view conventions](#view-conventions)). For most applications, this is enough. If custom mapping rules or additional presentation logic are needed, however, implementors can subclass `TopicController`. \ No newline at end of file +> *Note:* The default `TopicController` will automatically identify the current topic (based on the `RouteData`), map the current topic to a corresponding view model (based on [the `TopicMappingService` conventions](../OnTopic/Mapping/README.md)), and then return a corresponding view (based on the [view conventions](#view-conventions)). For most applications, this is enough. If custom mapping rules or additional presentation logic are needed, however, implementors can subclass `TopicController`. + +### Error Handling +The `ErrorController` provides support for handling ASP.NET Core's `UseStatusCodePages()` middleware, while continuing to support a range of other options. Routing to the controller can be supported by any of the following options, in isolation or together: +```csharp +public class Startup { + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { + app.UseEndpoints(endpoints => { + endpoints.MapTopicErrors(); // Error/{errorCode} + endpoints.MapTopicErrors("Errors", false); // Errors/{errorCode}; disables includeStaticFiles + endpoints.MapDefaultControllerRoute(); // Error/Http/{errorCode} + endpoints.MapTopicRoute("Error"); // Error/{path}; e.g., Error/Unauthorized + } + } +} +``` + +> *Note:* When using ASP.NET Core 6's minimal hosting model, these will instead be placed in the `Program` class as a top-level statement. + +The first three of these options all use the `Http()` action, which will provide the following fallback logic: +- If `Error:{errorCode}` exists, use that (e.g., `Error:404`) +- If `Error:{errorCode/100*100} exists, use that (e.g., `Error:400`) +- If `Error` exists, use that (e.g., `Error`) + +These are all intended to be used with one of ASP.NET Core's `UseStatusCodePages()` methods. For instance: +```csharp +app.UseStatusCodePagesWithReExecute("/Error/{0}"); +``` +The last option allows the same `ErrorController` to be used with any other custom error handling that might be configured—such as middleware, or the legacy `` handler—to handle any custom page under the `Error` topic. \ No newline at end of file diff --git a/OnTopic.AspNetCore.Mvc/ServiceCollectionExtensions.cs b/OnTopic.AspNetCore.Mvc/ServiceCollectionExtensions.cs index cadbe17a..b972a345 100644 --- a/OnTopic.AspNetCore.Mvc/ServiceCollectionExtensions.cs +++ b/OnTopic.AspNetCore.Mvc/ServiceCollectionExtensions.cs @@ -3,8 +3,6 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ -using System; -using System.Diagnostics.CodeAnalysis; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Mvc.Infrastructure; using Microsoft.AspNetCore.Mvc.TagHelpers; @@ -12,7 +10,6 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using OnTopic.AspNetCore.Mvc.Controllers; -using OnTopic.Internal.Diagnostics; namespace OnTopic.AspNetCore.Mvc { @@ -213,6 +210,31 @@ public static void MapImplicitAreaControllerRoute(this IEndpointRouteBuilder rou public static void MapImplicitAreaControllerRoute(this IEndpointRouteBuilder routes) => routes.MapDynamicControllerRoute("{area:exists}/{action=Index}"); + /*========================================================================================================================== + | EXTENSION: MAP ERROR ROUTE (IENDPOINTROUTEBUILDER) + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Adds the /Error/{errorCode} endpoint route for the . + /// + /// + /// This allows the to be used in conjunction with e.g., the , by providing a route for capturing the + /// errorCode. + /// + /// The this route is being added to. + /// The name of the root topic that the route should be mapped to. Defaults to Error. + /// Determines if static resources should be covered. Defaults to true. + public static ControllerActionEndpointConventionBuilder MapTopicErrors( + this IEndpointRouteBuilder routes, + string rootTopic = "Error", + bool includeStaticFiles = true + ) => + routes.MapControllerRoute( + name: "TopicError", + pattern: $"{rootTopic}/{{id:int}}/", + defaults: new { controller = "Error", action = "Http", rootTopic, includeStaticFiles } + ); + /*========================================================================================================================== | EXTENSION: MAP TOPIC SITEMAP (IENDPOINTROUTEBUILDER) \-------------------------------------------------------------------------------------------------------------------------*/ diff --git a/OnTopic.AspNetCore.Mvc/TopicRepositoryExtensions.cs b/OnTopic.AspNetCore.Mvc/TopicRepositoryExtensions.cs index 61c83370..6e0cdfbc 100644 --- a/OnTopic.AspNetCore.Mvc/TopicRepositoryExtensions.cs +++ b/OnTopic.AspNetCore.Mvc/TopicRepositoryExtensions.cs @@ -3,11 +3,7 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ -using System; -using System.Collections.Generic; using Microsoft.AspNetCore.Routing; -using OnTopic.Internal.Diagnostics; -using OnTopic.Repositories; namespace OnTopic.AspNetCore.Mvc { diff --git a/OnTopic.AspNetCore.Mvc/TopicRouteValueTransformer.cs b/OnTopic.AspNetCore.Mvc/TopicRouteValueTransformer.cs index 499322b1..6dbe24c4 100644 --- a/OnTopic.AspNetCore.Mvc/TopicRouteValueTransformer.cs +++ b/OnTopic.AspNetCore.Mvc/TopicRouteValueTransformer.cs @@ -3,12 +3,9 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ -using System; -using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc.Routing; using Microsoft.AspNetCore.Routing; -using OnTopic.Internal.Diagnostics; namespace OnTopic.AspNetCore.Mvc { diff --git a/OnTopic.AspNetCore.Mvc/TopicViewLocationExpander.cs b/OnTopic.AspNetCore.Mvc/TopicViewLocationExpander.cs index 191c7285..790c1300 100644 --- a/OnTopic.AspNetCore.Mvc/TopicViewLocationExpander.cs +++ b/OnTopic.AspNetCore.Mvc/TopicViewLocationExpander.cs @@ -3,10 +3,7 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ -using System; -using System.Collections.Generic; using Microsoft.AspNetCore.Mvc.Razor; -using OnTopic.Internal.Diagnostics; namespace OnTopic.AspNetCore.Mvc { diff --git a/OnTopic.AspNetCore.Mvc/TopicViewResult.cs b/OnTopic.AspNetCore.Mvc/TopicViewResult.cs index 01d20459..ad111962 100644 --- a/OnTopic.AspNetCore.Mvc/TopicViewResult.cs +++ b/OnTopic.AspNetCore.Mvc/TopicViewResult.cs @@ -3,12 +3,9 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ -using System.Threading.Tasks; -using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Infrastructure; using Microsoft.AspNetCore.Mvc.ViewFeatures; using Microsoft.Extensions.DependencyInjection; -using OnTopic.Internal.Diagnostics; using OnTopic.Metadata; namespace OnTopic.AspNetCore.Mvc { diff --git a/OnTopic.AspNetCore.Mvc/TopicViewResultExecutor.cs b/OnTopic.AspNetCore.Mvc/TopicViewResultExecutor.cs index 26fc899f..74820214 100644 --- a/OnTopic.AspNetCore.Mvc/TopicViewResultExecutor.cs +++ b/OnTopic.AspNetCore.Mvc/TopicViewResultExecutor.cs @@ -3,21 +3,14 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ -using System; -using System.Collections.Generic; using System.Diagnostics; -using System.Linq; -using System.Threading.Tasks; using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Infrastructure; using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.AspNetCore.Mvc.Razor; using Microsoft.AspNetCore.Mvc.ViewEngines; using Microsoft.AspNetCore.Mvc.ViewFeatures; -using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -using OnTopic.Internal.Diagnostics; namespace OnTopic.AspNetCore.Mvc { @@ -57,10 +50,10 @@ IModelMetadataProvider modelMetadataProvider /// Loops through potential sources for views to identify the most appropriate . /// /// - /// Will look for a view, in order, from the query string (?View=), - /// collection (for matches in the accepts header), then the property, if set, and - /// finally falls back to the . If none of those yield any results, will default to a - /// content type of "Page", which expects to find ~/Views/Page/Page.cshtml. + /// Will look for a view, in order, from the query string (?View=), collection + /// (for matches in the accepts header), then the property, if set, and finally falls back + /// to the . If none of those yield any results, will default to a content type of "Page", + /// which expects to find ~/Views/Page/Page.cshtml. /// /// The associated with the current request. /// The . @@ -115,10 +108,10 @@ public ViewEngineResult FindView(ActionContext actionContext, TopicViewResult vi if (!(view?.Success ?? false) && requestContext.Headers.ContainsKey("Accept")) { foreach (var header in requestContext.Headers["Accept"]) { var value = header.Replace("+", "-", StringComparison.Ordinal); - if (value.Contains("/", StringComparison.Ordinal)) { + if (value.Contains('/', StringComparison.Ordinal)) { value = value[(value.IndexOf("/", StringComparison.Ordinal)+1)..]; } - if (value.Contains(";", StringComparison.Ordinal)) { + if (value.Contains(';', StringComparison.Ordinal)) { value = value[..(value.IndexOf(";", StringComparison.Ordinal))]; } if (value is not null) { diff --git a/OnTopic.AspNetCore.Mvc/_filters/TopicResponseCacheAttribute.cs b/OnTopic.AspNetCore.Mvc/_filters/TopicResponseCacheAttribute.cs new file mode 100644 index 00000000..592eec96 --- /dev/null +++ b/OnTopic.AspNetCore.Mvc/_filters/TopicResponseCacheAttribute.cs @@ -0,0 +1,173 @@ +/*============================================================================================================================== +| Author Ignia, LLC +| Client Ignia, LLC +| Project Topics Library +\=============================================================================================================================*/ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc.Filters; +using Microsoft.AspNetCore.ResponseCaching; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Net.Http.Headers; +using OnTopic.AspNetCore.Mvc.Controllers; +using OnTopic.Attributes; + +namespace OnTopic.AspNetCore.Mvc { + + /*============================================================================================================================ + | CLASS: RESPONSE CACHE FILTER + \---------------------------------------------------------------------------------------------------------------------------*/ + /// + /// When applied to a —or derived class—will apply any configured CacheProfile reference + /// associated with the current topic. + /// + /// + /// + /// The Page content type has a topic reference to a CacheProfile content type, which contains settings for + /// configuring HTTP response headers. The evaluates the current to + /// determine which, if any, CacheProfile it is associated with, and applies the settings to the HTTP response + /// headers. If a CacheProfile is not configured, it will default to the CacheProfile with the of Default. + /// + /// + /// This filter is enabled automatically when is + /// configured. It is only applied to actions on controllers derived from , which is needed + /// in order to ensure access to the current and a configured . + /// + /// + /// If the ASP.NET Core Response Caching Middleware is configured via e.g. , then the page content may be eligible for output caching, depending on the + /// configuration used. This allows the same processed page content to be used for multiple clients without the server + /// needing to rerender them. This reduces response time and CPU usages at a cost of increased memory. + /// + /// + [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)] + public sealed class TopicResponseCacheAttribute : ActionFilterAttribute { + + /*========================================================================================================================== + | PRIVATE VARIABLES + \-------------------------------------------------------------------------------------------------------------------------*/ + private static Topic? _defaultCacheProfile; + + /*========================================================================================================================== + | EVENT: ON ACTION EXECUTING + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + public override void OnActionExecuting(ActionExecutingContext context) { + + /*------------------------------------------------------------------------------------------------------------------------ + | Validate parameters + \-----------------------------------------------------------------------------------------------------------------------*/ + Contract.Requires(context, nameof(context)); + + /*------------------------------------------------------------------------------------------------------------------------ + | Validate controller + \-----------------------------------------------------------------------------------------------------------------------*/ + if (context.Controller is not TopicController controller) { + throw new InvalidOperationException( + $"The {nameof(TopicResponseCacheAttribute)} can only be applied to a controller deriving from {nameof(TopicController)}." + ); + } + + /*------------------------------------------------------------------------------------------------------------------------ + | Ensure default cache profile + \-----------------------------------------------------------------------------------------------------------------------*/ + + // Lookup the default cache profile for reference + if (_defaultCacheProfile is null) { + _defaultCacheProfile = controller.TopicRepository.Load("Configuration:CacheProfiles:Default"); + } + + // Ensure the above lookup is only performed once per application + if (_defaultCacheProfile is null) { + _defaultCacheProfile = new Topic("ImplicitDefault", "CacheProfile"); + } + + /*------------------------------------------------------------------------------------------------------------------------ + | Identify cache profile + \-----------------------------------------------------------------------------------------------------------------------*/ + var cacheProfile = controller.CurrentTopic?.References.GetValue("CacheProfile")?? _defaultCacheProfile; + + // If the empty cache profile is returned + if (cacheProfile.Key is "ImplicitDefault") { + return; + } + + /*------------------------------------------------------------------------------------------------------------------------ + | Establish variables + \-----------------------------------------------------------------------------------------------------------------------*/ + var headers = context.HttpContext.Response.Headers; + var duration = cacheProfile.Attributes.GetInteger("Duration"); + var location = Enum.Parse(cacheProfile.Attributes.GetValue("Location")?? "None"); + var noStore = cacheProfile.Attributes.GetBoolean("NoStore"); + var varyByHeader = cacheProfile.Attributes.GetValue("VaryByHeader"); + var varyByQueryKeys = cacheProfile.Attributes.GetValue("VaryByQueryKeys"); + + /*------------------------------------------------------------------------------------------------------------------------ + | Exit if the cache profile is effectively empty + \-----------------------------------------------------------------------------------------------------------------------*/ + if (duration is 0 && location is 0 && !noStore && String.IsNullOrEmpty(varyByHeader + varyByQueryKeys)) { + return; + } + + /*------------------------------------------------------------------------------------------------------------------------ + | Validate metadata + \-----------------------------------------------------------------------------------------------------------------------*/ + if (!noStore && duration is 0) { + throw new InvalidOperationException( + $"The {nameof(duration)} attribute must be set to a positive value if the {nameof(noStore)} attribute is not enabled." + ); + } + + /*------------------------------------------------------------------------------------------------------------------------ + | Clear existing headers + \-----------------------------------------------------------------------------------------------------------------------*/ + headers.Remove(HeaderNames.Vary); + headers.Remove(HeaderNames.CacheControl); + headers.Remove(HeaderNames.Pragma); + + /*------------------------------------------------------------------------------------------------------------------------ + | Vary by keys, if appropriate + \-----------------------------------------------------------------------------------------------------------------------*/ + if (varyByQueryKeys is not null) { + var responseCachingFeature = context.HttpContext.Features.Get(); + if (responseCachingFeature == null) { + throw new InvalidOperationException( + "VaryByQueryKeys depends on the ASP.NET Response Caching Middleware, which is not currently configured." + ); + } + responseCachingFeature.VaryByQueryKeys = varyByQueryKeys.Split(',', StringSplitOptions.RemoveEmptyEntries); + } + + /*------------------------------------------------------------------------------------------------------------------------ + | Set standard HTTP headers + \-----------------------------------------------------------------------------------------------------------------------*/ + if (!string.IsNullOrEmpty(varyByHeader)) { + headers[HeaderNames.Vary] = varyByHeader; + } + + if (noStore) { + headers[HeaderNames.CacheControl] = "no-store"; + if (location is ResponseCacheLocation.None) { + headers.AppendCommaSeparatedValues(HeaderNames.CacheControl, "no-cache"); + headers[HeaderNames.Pragma] = "no-cache"; + } + return; + } + + if (location is ResponseCacheLocation.None) { + headers[HeaderNames.Pragma] = "no-cache"; + } + + string? cacheControl = location switch { + ResponseCacheLocation.Any => "public", + ResponseCacheLocation.Client => "private", + ResponseCacheLocation.None => "no-cache", + _ => null + }; + + headers[HeaderNames.CacheControl] = $"{cacheControl},max-age={duration}"; + + } + + } //Class +} //Namespace \ No newline at end of file diff --git a/OnTopic.AspNetCore.Mvc/ValidateTopicAttribute.cs b/OnTopic.AspNetCore.Mvc/_filters/ValidateTopicAttribute.cs similarity index 91% rename from OnTopic.AspNetCore.Mvc/ValidateTopicAttribute.cs rename to OnTopic.AspNetCore.Mvc/_filters/ValidateTopicAttribute.cs index 7281def2..068ee707 100644 --- a/OnTopic.AspNetCore.Mvc/ValidateTopicAttribute.cs +++ b/OnTopic.AspNetCore.Mvc/_filters/ValidateTopicAttribute.cs @@ -3,12 +3,8 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ -using System; -using System.Linq; -using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Filters; using OnTopic.AspNetCore.Mvc.Controllers; -using OnTopic.Internal.Diagnostics; namespace OnTopic.AspNetCore.Mvc { @@ -64,26 +60,22 @@ public override void OnActionExecuting(ActionExecutingContext context) { Contract.Requires(context, nameof(context)); /*------------------------------------------------------------------------------------------------------------------------ - | Establish variables + | Validate controller \-----------------------------------------------------------------------------------------------------------------------*/ - var controller = context.Controller as TopicController; - var currentTopic = controller?.CurrentTopic; - - /*------------------------------------------------------------------------------------------------------------------------ - | Validate context - \-----------------------------------------------------------------------------------------------------------------------*/ - if (controller is null) { + if (context.Controller is not TopicController controller) { throw new InvalidOperationException( - $"The {nameof(ValidateTopicAttribute)} can only be applied to a controller deriving from {nameof(TopicController)}." + $"The {nameof(TopicResponseCacheAttribute)} can only be applied to a controller deriving from {nameof(TopicController)}." ); } /*------------------------------------------------------------------------------------------------------------------------ - | Handle exceptions + | Validate current topic \-----------------------------------------------------------------------------------------------------------------------*/ + var currentTopic = controller.CurrentTopic; + if (currentTopic is null) { if (!AllowNull) { - context.Result = controller.NotFound("There is no topic associated with this path."); + context.Result = controller.NotFound(); } return; } diff --git a/OnTopic.Data.Caching/CachedTopicRepository.cs b/OnTopic.Data.Caching/CachedTopicRepository.cs index d55c9fcb..24504eb5 100644 --- a/OnTopic.Data.Caching/CachedTopicRepository.cs +++ b/OnTopic.Data.Caching/CachedTopicRepository.cs @@ -3,7 +3,6 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ -using System; using OnTopic.Internal.Diagnostics; using OnTopic.Querying; using OnTopic.Repositories; diff --git a/OnTopic.Data.Caching/OnTopic.Data.Caching.csproj b/OnTopic.Data.Caching/OnTopic.Data.Caching.csproj index 39ee8cce..f12ace2a 100644 --- a/OnTopic.Data.Caching/OnTopic.Data.Caching.csproj +++ b/OnTopic.Data.Caching/OnTopic.Data.Caching.csproj @@ -14,15 +14,15 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/OnTopic.Data.Caching/Properties/AssemblyInfo.cs b/OnTopic.Data.Caching/Properties/AssemblyInfo.cs index ce663c2d..b68ceb35 100644 --- a/OnTopic.Data.Caching/Properties/AssemblyInfo.cs +++ b/OnTopic.Data.Caching/Properties/AssemblyInfo.cs @@ -3,7 +3,6 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ -using System; using System.Runtime.InteropServices; /*============================================================================================================================== diff --git a/OnTopic.Data.Sql.Database/Tables/Topics.sql b/OnTopic.Data.Sql.Database/Tables/Topics.sql index c3de682e..c4bda366 100644 --- a/OnTopic.Data.Sql.Database/Tables/Topics.sql +++ b/OnTopic.Data.Sql.Database/Tables/Topics.sql @@ -16,9 +16,13 @@ TABLE [dbo].[Topics] ( CONSTRAINT [PK_Topics] PRIMARY KEY CLUSTERED ( [TopicID] ASC ), - CONSTRAINT [FK_Topics_Topics] - FOREIGN KEY ( [ParentID] ) - REFERENCES [Topics]([TopicID]) + CONSTRAINT [UK_TopicKey] + UNIQUE ( [TopicKey], + [ParentID] + ), + CONSTRAINT [FK_Topics_Topics] + FOREIGN KEY ( [ParentID] ) + REFERENCES [Topics]([TopicID]) ); GO diff --git a/OnTopic.Data.Sql/Models/AttributeValuesDataTable.cs b/OnTopic.Data.Sql/Models/AttributeValuesDataTable.cs index e682582b..c979a89a 100644 --- a/OnTopic.Data.Sql/Models/AttributeValuesDataTable.cs +++ b/OnTopic.Data.Sql/Models/AttributeValuesDataTable.cs @@ -3,7 +3,6 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ -using System.Data; using OnTopic.Attributes; using OnTopic.Collections.Specialized; diff --git a/OnTopic.Data.Sql/Models/TopicListDataTable.cs b/OnTopic.Data.Sql/Models/TopicListDataTable.cs index 7d5ab686..1ba3f056 100644 --- a/OnTopic.Data.Sql/Models/TopicListDataTable.cs +++ b/OnTopic.Data.Sql/Models/TopicListDataTable.cs @@ -3,7 +3,6 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ -using System.Data; namespace OnTopic.Data.Sql.Models { diff --git a/OnTopic.Data.Sql/Models/TopicReferencesDataTable.cs b/OnTopic.Data.Sql/Models/TopicReferencesDataTable.cs index d2632a41..a9c1349b 100644 --- a/OnTopic.Data.Sql/Models/TopicReferencesDataTable.cs +++ b/OnTopic.Data.Sql/Models/TopicReferencesDataTable.cs @@ -3,7 +3,6 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ -using System.Data; namespace OnTopic.Data.Sql.Models { diff --git a/OnTopic.Data.Sql/OnTopic.Data.Sql.csproj b/OnTopic.Data.Sql/OnTopic.Data.Sql.csproj index 7335d22d..d9219f9c 100644 --- a/OnTopic.Data.Sql/OnTopic.Data.Sql.csproj +++ b/OnTopic.Data.Sql/OnTopic.Data.Sql.csproj @@ -14,16 +14,16 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/OnTopic.Data.Sql/Properties/AssemblyInfo.cs b/OnTopic.Data.Sql/Properties/AssemblyInfo.cs index 638c33b8..e314ced8 100644 --- a/OnTopic.Data.Sql/Properties/AssemblyInfo.cs +++ b/OnTopic.Data.Sql/Properties/AssemblyInfo.cs @@ -3,7 +3,17 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ -using System; + +/*============================================================================================================================== +| USING DIRECTIVES (GLOBAL) +\-----------------------------------------------------------------------------------------------------------------------------*/ +global using System.Data; +global using Microsoft.Data.SqlClient; +global using OnTopic.Internal.Diagnostics; + +/*============================================================================================================================== +| USING DIRECTIVES (LOCAL) +\-----------------------------------------------------------------------------------------------------------------------------*/ using System.Runtime.CompilerServices; using System.Runtime.InteropServices; diff --git a/OnTopic.Data.Sql/SqlCommandExtensions.cs b/OnTopic.Data.Sql/SqlCommandExtensions.cs index eede7e16..8ebd48e4 100644 --- a/OnTopic.Data.Sql/SqlCommandExtensions.cs +++ b/OnTopic.Data.Sql/SqlCommandExtensions.cs @@ -3,12 +3,8 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ -using System; -using System.Data; using System.Globalization; using System.Text; -using Microsoft.Data.SqlClient; -using OnTopic.Internal.Diagnostics; namespace OnTopic.Data.Sql { @@ -127,12 +123,6 @@ private static void AddParameter( ParameterDirection paramDirection = ParameterDirection.Input ) { - /*------------------------------------------------------------------------------------------------------------------------ - | Validate input - \-----------------------------------------------------------------------------------------------------------------------*/ - Contract.Requires(command, "The SQL command object must be specified."); - Contract.Requires(command.Parameters, "The SQL command object's parameters collection must be available"); - /*------------------------------------------------------------------------------------------------------------------------ | Establish basic parameter \-----------------------------------------------------------------------------------------------------------------------*/ diff --git a/OnTopic.Data.Sql/SqlDataReaderExtensions.cs b/OnTopic.Data.Sql/SqlDataReaderExtensions.cs index 02e59272..c6beef57 100644 --- a/OnTopic.Data.Sql/SqlDataReaderExtensions.cs +++ b/OnTopic.Data.Sql/SqlDataReaderExtensions.cs @@ -3,15 +3,9 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ -using System; -using System.Collections.Generic; -using System.Data; using System.Diagnostics; -using System.Linq; using System.Net; -using Microsoft.Data.SqlClient; using OnTopic.Collections.Specialized; -using OnTopic.Internal.Diagnostics; using OnTopic.Querying; namespace OnTopic.Data.Sql { @@ -201,7 +195,7 @@ private static void AddTopic(this IDataReader reader, TopicIndex topics, bool? m /*------------------------------------------------------------------------------------------------------------------------ | Assign parent \-----------------------------------------------------------------------------------------------------------------------*/ - if (parentId >= 0 && current.Parent?.Id != parentId && topics.Keys.Contains(parentId)) { + if (parentId >= 0 && current.Parent?.Id != parentId && topics.ContainsKey(parentId)) { current.Parent = topics[parentId]; } @@ -359,7 +353,7 @@ private static void SetRelationships(this IDataReader reader, TopicIndex topics, var related = (Topic?)null; // Fetch the related topic - if (topics.Keys.Contains(targetTopicId)) { + if (topics.ContainsKey(targetTopicId)) { related = topics[targetTopicId]; } @@ -417,7 +411,7 @@ private static void SetReferences(this IDataReader reader, TopicIndex topics, bo // Fetch the related topic if (targetTopicId is null) { } - else if (topics.Keys.Contains(targetTopicId.Value)) { + else if (topics.ContainsKey(targetTopicId.Value)) { referenced = topics[targetTopicId.Value]; } else { diff --git a/OnTopic.Data.Sql/SqlTopicRepository.cs b/OnTopic.Data.Sql/SqlTopicRepository.cs index 77dcea63..7d61f6cc 100644 --- a/OnTopic.Data.Sql/SqlTopicRepository.cs +++ b/OnTopic.Data.Sql/SqlTopicRepository.cs @@ -3,15 +3,10 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ -using System; -using System.Data; using System.Diagnostics.CodeAnalysis; using System.Globalization; -using System.Linq; using System.Text; -using Microsoft.Data.SqlClient; using OnTopic.Data.Sql.Models; -using OnTopic.Internal.Diagnostics; using OnTopic.Querying; using OnTopic.Repositories; diff --git a/OnTopic.TestDoubles/DummyTopicMappingService.cs b/OnTopic.TestDoubles/DummyTopicMappingService.cs index 7caeb753..c26310f1 100644 --- a/OnTopic.TestDoubles/DummyTopicMappingService.cs +++ b/OnTopic.TestDoubles/DummyTopicMappingService.cs @@ -3,9 +3,6 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ -using System; -using System.Diagnostics.CodeAnalysis; -using System.Threading.Tasks; using OnTopic.Mapping; using OnTopic.Mapping.Annotations; diff --git a/OnTopic.TestDoubles/DummyTopicRepository.cs b/OnTopic.TestDoubles/DummyTopicRepository.cs index 2256256a..bee868cc 100644 --- a/OnTopic.TestDoubles/DummyTopicRepository.cs +++ b/OnTopic.TestDoubles/DummyTopicRepository.cs @@ -3,9 +3,6 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ -using System; -using System.Diagnostics.CodeAnalysis; -using OnTopic.Metadata; using OnTopic.Repositories; namespace OnTopic.TestDoubles { diff --git a/OnTopic.TestDoubles/Metadata/BooleanAttributeDescriptor.cs b/OnTopic.TestDoubles/Metadata/BooleanAttributeDescriptor.cs index 4c7f9109..2c43fcb9 100644 --- a/OnTopic.TestDoubles/Metadata/BooleanAttributeDescriptor.cs +++ b/OnTopic.TestDoubles/Metadata/BooleanAttributeDescriptor.cs @@ -3,8 +3,6 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ -using System.Diagnostics.CodeAnalysis; -using OnTopic.Metadata; namespace OnTopic.TestDoubles.Metadata { diff --git a/OnTopic.TestDoubles/Metadata/NestedTopicListAttributeDescriptor.cs b/OnTopic.TestDoubles/Metadata/NestedTopicListAttributeDescriptor.cs index 5cf49a6c..d7c295a8 100644 --- a/OnTopic.TestDoubles/Metadata/NestedTopicListAttributeDescriptor.cs +++ b/OnTopic.TestDoubles/Metadata/NestedTopicListAttributeDescriptor.cs @@ -3,8 +3,6 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ -using System.Diagnostics.CodeAnalysis; -using OnTopic.Metadata; namespace OnTopic.TestDoubles.Metadata { diff --git a/OnTopic.TestDoubles/Metadata/RelationshipAttributeDescriptor.cs b/OnTopic.TestDoubles/Metadata/RelationshipAttributeDescriptor.cs index e7f2f4fd..788aa167 100644 --- a/OnTopic.TestDoubles/Metadata/RelationshipAttributeDescriptor.cs +++ b/OnTopic.TestDoubles/Metadata/RelationshipAttributeDescriptor.cs @@ -3,8 +3,6 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ -using System.Diagnostics.CodeAnalysis; -using OnTopic.Metadata; namespace OnTopic.TestDoubles.Metadata { diff --git a/OnTopic.TestDoubles/Metadata/TextAttributeDescriptor.cs b/OnTopic.TestDoubles/Metadata/TextAttributeDescriptor.cs index e6100dd1..37413fb6 100644 --- a/OnTopic.TestDoubles/Metadata/TextAttributeDescriptor.cs +++ b/OnTopic.TestDoubles/Metadata/TextAttributeDescriptor.cs @@ -3,8 +3,6 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ -using System.Diagnostics.CodeAnalysis; -using OnTopic.Metadata; namespace OnTopic.TestDoubles.Metadata { diff --git a/OnTopic.TestDoubles/Metadata/TopicReferenceAttributeDescriptor.cs b/OnTopic.TestDoubles/Metadata/TopicReferenceAttributeDescriptor.cs index 3523872b..4ac8e46b 100644 --- a/OnTopic.TestDoubles/Metadata/TopicReferenceAttributeDescriptor.cs +++ b/OnTopic.TestDoubles/Metadata/TopicReferenceAttributeDescriptor.cs @@ -3,8 +3,6 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ -using System.Diagnostics.CodeAnalysis; -using OnTopic.Metadata; namespace OnTopic.TestDoubles.Metadata { diff --git a/OnTopic.TestDoubles/OnTopic.TestDoubles.csproj b/OnTopic.TestDoubles/OnTopic.TestDoubles.csproj index d18d27d9..43669628 100644 --- a/OnTopic.TestDoubles/OnTopic.TestDoubles.csproj +++ b/OnTopic.TestDoubles/OnTopic.TestDoubles.csproj @@ -12,11 +12,11 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/OnTopic.TestDoubles/Properties/AssemblyInfo.cs b/OnTopic.TestDoubles/Properties/AssemblyInfo.cs index 4bb6a499..acf4282d 100644 --- a/OnTopic.TestDoubles/Properties/AssemblyInfo.cs +++ b/OnTopic.TestDoubles/Properties/AssemblyInfo.cs @@ -3,8 +3,16 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ -using System; -using System.Diagnostics.CodeAnalysis; + +/*============================================================================================================================== +| USING DIRECTIVES (LOCAL) +\-----------------------------------------------------------------------------------------------------------------------------*/ +global using System.Diagnostics.CodeAnalysis; +global using OnTopic.Metadata; + +/*============================================================================================================================== +| USING DIRECTIVES (LOCAL) +\-----------------------------------------------------------------------------------------------------------------------------*/ using System.Runtime.InteropServices; /*============================================================================================================================== diff --git a/OnTopic.TestDoubles/StubTopicRepository.cs b/OnTopic.TestDoubles/StubTopicRepository.cs index 5182c79b..b9bc47b8 100644 --- a/OnTopic.TestDoubles/StubTopicRepository.cs +++ b/OnTopic.TestDoubles/StubTopicRepository.cs @@ -3,13 +3,9 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ -using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; using System.Globalization; using OnTopic.Attributes; using OnTopic.Internal.Diagnostics; -using OnTopic.Metadata; using OnTopic.Querying; using OnTopic.Repositories; @@ -205,7 +201,10 @@ public IEnumerable GetAttributesProxy( \-------------------------------------------------------------------------------------------------------------------------*/ /// [ExcludeFromCodeCoverage] - [Obsolete("Deprecated. Instead, use the new SetContentTypeDescriptorsProxy(), which provides the same function.", true)] + [Obsolete( + $"Deprecated. Instead, use the new {nameof(SetContentTypeDescriptorsProxy)}, which provides the same function.", + true + )] public ContentTypeDescriptorCollection GetContentTypeDescriptorsProxy(ContentTypeDescriptor topicGraph) => base.SetContentTypeDescriptors(topicGraph); diff --git a/OnTopic.Tests/AttributeCollectionTest.cs b/OnTopic.Tests/AttributeCollectionTest.cs index d7acbd74..56343b2a 100644 --- a/OnTopic.Tests/AttributeCollectionTest.cs +++ b/OnTopic.Tests/AttributeCollectionTest.cs @@ -3,13 +3,9 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ -using System; using System.Collections; -using System.Diagnostics.CodeAnalysis; using System.Globalization; -using OnTopic.Attributes; using OnTopic.Collections.Specialized; -using OnTopic.Internal.Diagnostics; using OnTopic.Tests.Entities; using Xunit; @@ -109,7 +105,7 @@ public void GetInteger_CorrectValue_IsReturned() { topic.Attributes.SetInteger("Number1", 1); - Assert.Equal(1, topic.Attributes.GetInteger("Number1", 5)); + Assert.Equal(1, topic.Attributes.GetInteger("Number1", 5)); } @@ -131,9 +127,9 @@ public void GetInteger_InheritedValue_IsReturned() { baseTopic.Attributes.SetInteger("Number1", 1); - Assert.Equal(1, topic.Attributes.GetInteger("Number1", 5)); - Assert.Equal(1, childTopic.Attributes.GetInteger("Number1", 5, true)); - Assert.Equal(0, topic.Attributes.GetInteger("Number1", inheritFromBase: false)); + Assert.Equal(1, topic.Attributes.GetInteger("Number1", 5)); + Assert.Equal(1, childTopic.Attributes.GetInteger("Number1", 5, true)); + Assert.Equal(0, topic.Attributes.GetInteger("Number1", inheritFromBase: false)); } @@ -150,8 +146,8 @@ public void GetInteger_IncorrectValue_ReturnsDefault() { topic.Attributes.SetValue("Number3", "Invalid"); - Assert.Equal(0, topic.Attributes.GetInteger("Number3")); - Assert.Equal(5, topic.Attributes.GetInteger("Number3", 5)); + Assert.Equal(0, topic.Attributes.GetInteger("Number3")); + Assert.Equal(5, topic.Attributes.GetInteger("Number3", 5)); } @@ -166,8 +162,8 @@ public void GetInteger_IncorrectKey_ReturnsDefault() { var topic = new Topic("Test", "Container"); - Assert.Equal(0, topic.Attributes.GetInteger("InvalidKey")); - Assert.Equal(5, topic.Attributes.GetInteger("InvalidKey", 5)); + Assert.Equal(0, topic.Attributes.GetInteger("InvalidKey")); + Assert.Equal(5, topic.Attributes.GetInteger("InvalidKey", 5)); } @@ -184,7 +180,7 @@ public void GetDouble_CorrectValue_IsReturned() { topic.Attributes.SetDouble("Number1", 1); - Assert.Equal(1.0, topic.Attributes.GetDouble("Number1", 5.0)); + Assert.Equal(1.0, topic.Attributes.GetDouble("Number1", 5.0)); } @@ -206,9 +202,9 @@ public void GetDouble_InheritedValue_IsReturned() { baseTopic.Attributes.SetDouble("Number1", 1); - Assert.Equal(1.0, topic.Attributes.GetDouble("Number1", 5.0)); - Assert.Equal(1.0, childTopic.Attributes.GetDouble("Number1", 5.0, true)); - Assert.Equal(0.0, topic.Attributes.GetInteger("Number1", inheritFromBase: false)); + Assert.Equal(1.0, topic.Attributes.GetDouble("Number1", 5.0)); + Assert.Equal(1.0, childTopic.Attributes.GetDouble("Number1", 5.0, true)); + Assert.Equal(0.0, topic.Attributes.GetInteger("Number1", inheritFromBase: false)); } @@ -225,8 +221,8 @@ public void GetDouble_IncorrectValue_ReturnsDefault() { topic.Attributes.SetValue("Number3", "Invalid"); - Assert.Equal(0, topic.Attributes.GetDouble("Number3")); - Assert.Equal(5.0, topic.Attributes.GetDouble("Number3", 5.0)); + Assert.Equal(0.0, topic.Attributes.GetDouble("Number3")); + Assert.Equal(5.0, topic.Attributes.GetDouble("Number3", 5.0)); } @@ -241,8 +237,8 @@ public void GetDouble_IncorrectKey_ReturnsDefault() { var topic = new Topic("Test", "Container"); - Assert.Equal(0, topic.Attributes.GetDouble("InvalidKey")); - Assert.Equal(5.0, topic.Attributes.GetDouble("InvalidKey", 5.0)); + Assert.Equal(0.0, topic.Attributes.GetDouble("InvalidKey")); + Assert.Equal(5.0, topic.Attributes.GetDouble("InvalidKey", 5.0)); } @@ -260,7 +256,7 @@ public void GetDateTime_CorrectValue_IsReturned() { topic.Attributes.SetDateTime("DateTime1", dateTime1); - Assert.Equal(dateTime1, topic.Attributes.GetDateTime("DateTime1", DateTime.MinValue)); + Assert.Equal(dateTime1, topic.Attributes.GetDateTime("DateTime1", DateTime.MinValue)); } @@ -283,9 +279,9 @@ public void GetDateTime_InheritedValue_IsReturned() { baseTopic.Attributes.SetDateTime("DateTime1", dateTime1); - Assert.Equal(dateTime1, topic.Attributes.GetDateTime("DateTime1", DateTime.Now)); - Assert.Equal(dateTime1, childTopic.Attributes.GetDateTime("DateTime1", DateTime.Now, true)); - Assert.Equal(new DateTime(), topic.Attributes.GetDateTime("DateTime1", inheritFromBase: false)); + Assert.Equal(dateTime1, topic.Attributes.GetDateTime("DateTime1", DateTime.Now)); + Assert.Equal(dateTime1, childTopic.Attributes.GetDateTime("DateTime1", DateTime.Now, true)); + Assert.Equal(new DateTime(), topic.Attributes.GetDateTime("DateTime1", inheritFromBase: false)); } @@ -303,8 +299,8 @@ public void GetDateTime_IncorrectValue_ReturnsDefault() { topic.Attributes.SetValue("DateTime2", "IncorrectValue"); - Assert.Equal(new DateTime(), topic.Attributes.GetDateTime("DateTime2")); - Assert.Equal(dateTime1, topic.Attributes.GetDateTime("DateTime2", dateTime1)); + Assert.Equal(new DateTime(), topic.Attributes.GetDateTime("DateTime2")); + Assert.Equal(dateTime1, topic.Attributes.GetDateTime("DateTime2", dateTime1)); } @@ -323,8 +319,8 @@ public void GetDateTime_IncorrectKey_ReturnsDefault() { topic.Attributes.SetDateTime("DateTime2", dateTime2); - Assert.Equal(new DateTime(), topic.Attributes.GetDateTime("DateTime3")); - Assert.Equal(dateTime1, topic.Attributes.GetDateTime("DateTime3", dateTime1)); + Assert.Equal(new DateTime(), topic.Attributes.GetDateTime("DateTime3")); + Assert.Equal(dateTime1, topic.Attributes.GetDateTime("DateTime3", dateTime1)); } @@ -408,6 +404,50 @@ public void GetBoolean_IncorrectKey_ReturnDefault() { } + /*========================================================================================================================== + | TEST: GET URI: INHERITED VALUE: IS RETURNED + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Ensures that URI values can be set and retrieved as expected via inheritance, both via + /// and . + /// + [Fact] + public void GetUri_InheritedValue_IsReturned() { + + var baseTopic = new Topic("Base", "Container"); + var topic = new Topic("Test", "Container"); + var childTopic = new Topic("Child", "Container", topic); + var url = "https://www.github.com/OnTopicCMS/"; + var uri = new Uri(url); + + topic.BaseTopic = baseTopic; + + baseTopic.Attributes.SetUri("Url", uri); + + Assert.Equal(uri, topic.Attributes.GetUri("Url")); + Assert.Equal(uri, childTopic.Attributes.GetUri("Url", inheritFromParent: true)); + Assert.Null(topic.Attributes.GetUri("Url", inheritFromBase: false)); + + } + + /*========================================================================================================================== + | TEST: GET URI: INCORRECT VALUE: RETURN DEFAULT + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Ensures that invalid values return the default. + /// + [Fact] + public void GetUri_IncorrectValue_ReturnDefault() { + + var topic = new Topic("Test", "Container"); + var url = "https://www.github.com/OnTopicCMS/"; + var uri = new Uri(url); + + Assert.Null(topic.Attributes.GetUri("InvalidUrl")); + Assert.Equal(uri, topic.Attributes.GetUri("InvalidUrl", uri)); + + } + /*========================================================================================================================== | TEST: SET VALUE: CORRECT VALUE: IS RETURNED \-------------------------------------------------------------------------------------------------------------------------*/ @@ -686,9 +726,9 @@ public void IsDirty_MarkClean_UpdatesLastModified() { topic.Attributes.TryGetValue("Bar", out var cleanedAttribute2); topic.Attributes.TryGetValue("Baz", out var cleanedAttribute3); - Assert.Equal(firstVersion, cleanedAttribute1?.LastModified); - Assert.Equal(secondVersion, cleanedAttribute2?.LastModified); - Assert.Equal(thirdVersion, cleanedAttribute3?.LastModified); + Assert.Equal(firstVersion, cleanedAttribute1?.LastModified); + Assert.Equal(secondVersion, cleanedAttribute2?.LastModified); + Assert.Equal(thirdVersion, cleanedAttribute3?.LastModified); } @@ -874,7 +914,7 @@ public void Add_NumericValueWithBusinessLogic_IsReturned() { topic.Attributes.SetInteger("NumericAttribute", 1); - Assert.Equal(1, topic.NumericAttribute); + Assert.Equal(1, topic.NumericAttribute); } @@ -928,7 +968,7 @@ public void Add_DateTimeValueWithBusinessLogic_IsReturned() { topic.Attributes.SetDateTime("DateTimeAttribute", dateTime); - Assert.Equal(dateTime, topic.DateTimeAttribute); + Assert.Equal(dateTime, topic.DateTimeAttribute); } diff --git a/OnTopic.Tests/AttributeDictionaryTest.cs b/OnTopic.Tests/AttributeDictionaryTest.cs new file mode 100644 index 00000000..5df95d71 --- /dev/null +++ b/OnTopic.Tests/AttributeDictionaryTest.cs @@ -0,0 +1,228 @@ +/*============================================================================================================================== +| Author Ignia, LLC +| Client Ignia, LLC +| Project Topics Library +\=============================================================================================================================*/ +using Xunit; + +namespace OnTopic.Tests { + + /*============================================================================================================================ + | CLASS: ATTRIBUTE DICTIONARY TEST + \---------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Provides unit tests for the class. + /// + [ExcludeFromCodeCoverage] + public class AttributeDictionaryTest { + + /*========================================================================================================================== + | TEST: GET VALUE: RETURNS EXPECTED VALUE + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Constructs a based on the data and confirms that returns the value. + /// + /// The value to add to the dictionary. + /// The value expected to be returned. + [Theory] + [InlineData(null, null)] + [InlineData("", null)] + [InlineData(" ", null)] + [InlineData("0", "0")] + [InlineData("True", "True")] + [InlineData("Hello", "Hello")] + public void GetValue_ReturnsExpectedValue(string? input, string? expected) { + + var attributes = new AttributeDictionary() {{"Key", input}}; + + Assert.Equal(expected, attributes.GetValue("Key")); + + } + + /*========================================================================================================================== + | TEST: GET BOOLEAN: RETURNS EXPECTED VALUE + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Constructs a based on the data and confirms that returns the value. + /// + /// The value to add to the dictionary. + /// The value expected to be returned. + [Theory] + [InlineData("0", false)] + [InlineData("1", true)] + [InlineData("False", false)] + [InlineData("True", true)] + [InlineData("", null)] + [InlineData("Hello", null)] + public void GetBoolean_ReturnsExpectedValue(string input, bool? expected) { + + var attributes = new AttributeDictionary() {{"Key", input}}; + + Assert.Equal(expected, attributes.GetBoolean("Key")); + + } + + /*========================================================================================================================== + | TEST: GET INTEGER: RETURNS EXPECTED VALUE + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Constructs a based on the data and confirms that returns the value. + /// + /// The value to add to the dictionary. + /// The value expected to be returned. + [Theory] + [InlineData("0", 0)] + [InlineData("1", 1)] + [InlineData("2.4", null)] + [InlineData("", null)] + [InlineData("Hello", null)] + public void GetInteger_ReturnsExpectedValue(string input, int? expected) { + + var attributes = new AttributeDictionary() {{"Key", input}}; + + Assert.Equal(expected, attributes.GetInteger("Key")); + + } + + /*========================================================================================================================== + | TEST: GET DOUBLE: RETURNS EXPECTED VALUE + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Constructs a based on the data and confirms that returns the value. + /// + /// The value to add to the dictionary. + /// The value expected to be returned. + [Theory] + [InlineData("0.0", 0.0)] + [InlineData("1.0", 1.0)] + [InlineData("1", 1.0)] + [InlineData("1.4", 1.4)] + [InlineData("", null)] + [InlineData("Hello", null)] + public void GetDouble_ReturnsExpectedValue(string input, double? expected) { + + var attributes = new AttributeDictionary() {{"Key", input}}; + + Assert.Equal(expected, attributes.GetDouble("Key")); + + } + + /*========================================================================================================================== + | TEST: GET DATE/TIME: RETURNS EXPECTED VALUE + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Constructs a based on the data and confirms that returns the expected value. + /// + /// The value to add to the dictionary. + /// Determines whether a valid is expected in response. + [Theory] + [InlineData("1976-10-15 01:02:03", true)] + [InlineData("October 15, 1976 01:02:03 AM", true)] + [InlineData("15 Oct 1976 01:02:03", true)] + [InlineData("10/15/1976 01:02:03 AM", true)] + [InlineData("", false)] + [InlineData("Hello", false)] + public void GetDate_ReturnsExpectedValue(string input, bool isSet) { + + var attributes = new AttributeDictionary() {{"Key", input}}; + + Assert.Equal(isSet? new DateTime(1976, 10, 15, 1, 2, 3) : null, attributes.GetDateTime("Key")); + + } + + /*========================================================================================================================== + | TEST: GET URI: RETURNS EXPECTED VALUE + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Constructs a based on the data and confirms that returns the expected value. + /// + /// The value to add to the dictionary. + [Theory] + [InlineData("https://www.github.com/OnTopicCMS")] + [InlineData("Some:\\\\URL")] + public void GetUri_ReturnsExpectedValue(string input) { + + var attributes = new AttributeDictionary() {{"Key", input}}; + + Assert.Equal(new Uri(input), attributes.GetUri("Key")); + + } + + /*========================================================================================================================== + | TEST: GET {TYPE}: INVALID KEY: RETURNS NULL + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Constructs a and confirms that each of the methods return null if an invalid key is passed. + /// + [Fact] + public void GetType_InvalidKey_ReturnsNull() { + + var attributes = new AttributeDictionary(); + + Assert.Null(attributes.GetValue("MissingKey")); + Assert.Null(attributes.GetBoolean("MissingKey")); + Assert.Null(attributes.GetInteger("MissingKey")); + Assert.Null(attributes.GetDouble("MissingKey")); + Assert.Null(attributes.GetDateTime("MissingKey")); + Assert.Null(attributes.GetUri("MissingKey")); + + } + + /*========================================================================================================================== + | TEST: AS ATTRIBUTE DICTIONARY: EXCLUDED KEYS: EXCLUDED + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Constructs a using + /// and confirms that doesn't include the excluded values. + /// + [Fact] + public void AsAttributeDictionary_ExcludedKeys_Excluded() { + + var topic = new Topic("Test", "Page"); + + topic.Attributes.SetValue("Title", "Page Title"); + topic.Attributes.SetValue("LastModified", "October 15, 1976"); + topic.Attributes.SetValue("Subtitle", "Subtitle"); + + var attributes = topic.Attributes.AsAttributeDictionary(); + + Assert.Single(attributes.Keys); + Assert.Null(attributes.GetValue("Title")); + Assert.Null(attributes.GetValue("LastModified")); + Assert.Equal("Subtitle", attributes.GetValue("Subtitle")); + + } + + /*========================================================================================================================== + | TEST: AS ATTRIBUTE DICTIONARY: INHERIT FROM BASE: INHERITS VALUES + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Constructs a using + /// and confirms that correctly inherits values. + /// + [Fact] + public void AsAttributeDictionary_InheritFromBase_InheritsValues() { + + var baseTopic = new Topic("BaseTopic", "Page"); + var topic = new Topic("Test", "Page") { + BaseTopic = baseTopic + }; + + baseTopic.Attributes.SetValue("Subtitle", "Subtitle"); + + var attributes = topic.Attributes.AsAttributeDictionary(true); + + Assert.Single(attributes.Keys); + Assert.Equal("Subtitle", attributes.GetValue("Subtitle")); + + } + + } //Class +} //Namespace \ No newline at end of file diff --git a/OnTopic.Tests/AttributeValueConverterTest.cs b/OnTopic.Tests/AttributeValueConverterTest.cs index d6cf1293..028b80a0 100644 --- a/OnTopic.Tests/AttributeValueConverterTest.cs +++ b/OnTopic.Tests/AttributeValueConverterTest.cs @@ -3,9 +3,6 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ -using System; -using System.Diagnostics.CodeAnalysis; -using OnTopic.Attributes; using Xunit; namespace OnTopic.Tests { diff --git a/OnTopic.Tests/BindingModels/ContentTypeDescriptorTopicBindingModel.cs b/OnTopic.Tests/BindingModels/ContentTypeDescriptorTopicBindingModel.cs index 6e0c6d78..da463089 100644 --- a/OnTopic.Tests/BindingModels/ContentTypeDescriptorTopicBindingModel.cs +++ b/OnTopic.Tests/BindingModels/ContentTypeDescriptorTopicBindingModel.cs @@ -3,7 +3,6 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ -using System.Collections.ObjectModel; using OnTopic.ViewModels.BindingModels; namespace OnTopic.Tests.BindingModels { diff --git a/OnTopic.Tests/BindingModels/DisabledAttributeTopicBindingModel.cs b/OnTopic.Tests/BindingModels/DisabledAttributeTopicBindingModel.cs index 9975803f..b2c49135 100644 --- a/OnTopic.Tests/BindingModels/DisabledAttributeTopicBindingModel.cs +++ b/OnTopic.Tests/BindingModels/DisabledAttributeTopicBindingModel.cs @@ -3,8 +3,6 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ -using System; -using OnTopic.Mapping.Annotations; namespace OnTopic.Tests.BindingModels { diff --git a/OnTopic.Tests/BindingModels/InvalidAttributeTopicBindingModel.cs b/OnTopic.Tests/BindingModels/InvalidAttributeTopicBindingModel.cs index 3454fff6..b5eb55ae 100644 --- a/OnTopic.Tests/BindingModels/InvalidAttributeTopicBindingModel.cs +++ b/OnTopic.Tests/BindingModels/InvalidAttributeTopicBindingModel.cs @@ -3,7 +3,6 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ -using System; namespace OnTopic.Tests.BindingModels { diff --git a/OnTopic.Tests/BindingModels/InvalidChildrenTopicBindingModel.cs b/OnTopic.Tests/BindingModels/InvalidChildrenTopicBindingModel.cs index 09eedea6..84f3d0ce 100644 --- a/OnTopic.Tests/BindingModels/InvalidChildrenTopicBindingModel.cs +++ b/OnTopic.Tests/BindingModels/InvalidChildrenTopicBindingModel.cs @@ -3,8 +3,6 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ -using System; -using System.Collections.ObjectModel; namespace OnTopic.Tests.BindingModels { diff --git a/OnTopic.Tests/BindingModels/InvalidNestedTopicListTypeTopicBindingModel.cs b/OnTopic.Tests/BindingModels/InvalidNestedTopicListTypeTopicBindingModel.cs index a5980ade..516ffae2 100644 --- a/OnTopic.Tests/BindingModels/InvalidNestedTopicListTypeTopicBindingModel.cs +++ b/OnTopic.Tests/BindingModels/InvalidNestedTopicListTypeTopicBindingModel.cs @@ -3,9 +3,7 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ -using System; using System.Collections; -using System.Collections.Generic; namespace OnTopic.Tests.BindingModels { diff --git a/OnTopic.Tests/BindingModels/InvalidParentTopicBindingModel.cs b/OnTopic.Tests/BindingModels/InvalidParentTopicBindingModel.cs index dc1c9416..a31cb977 100644 --- a/OnTopic.Tests/BindingModels/InvalidParentTopicBindingModel.cs +++ b/OnTopic.Tests/BindingModels/InvalidParentTopicBindingModel.cs @@ -3,7 +3,6 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ -using System; namespace OnTopic.Tests.BindingModels { diff --git a/OnTopic.Tests/BindingModels/InvalidReferenceTypeTopicBindingModel.cs b/OnTopic.Tests/BindingModels/InvalidReferenceTypeTopicBindingModel.cs index 02e378bb..1df246f9 100644 --- a/OnTopic.Tests/BindingModels/InvalidReferenceTypeTopicBindingModel.cs +++ b/OnTopic.Tests/BindingModels/InvalidReferenceTypeTopicBindingModel.cs @@ -3,7 +3,6 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ -using System; using OnTopic.Models; using OnTopic.Tests.ViewModels; diff --git a/OnTopic.Tests/BindingModels/InvalidRelationshipBaseTypeTopicBindingModel.cs b/OnTopic.Tests/BindingModels/InvalidRelationshipBaseTypeTopicBindingModel.cs index ad1d50c2..d18328a5 100644 --- a/OnTopic.Tests/BindingModels/InvalidRelationshipBaseTypeTopicBindingModel.cs +++ b/OnTopic.Tests/BindingModels/InvalidRelationshipBaseTypeTopicBindingModel.cs @@ -3,8 +3,6 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ -using System; -using System.Collections.ObjectModel; using OnTopic.Models; using OnTopic.Tests.ViewModels; diff --git a/OnTopic.Tests/BindingModels/InvalidRelationshipListTypeTopicBindingModel.cs b/OnTopic.Tests/BindingModels/InvalidRelationshipListTypeTopicBindingModel.cs index 9330127f..bfa407f0 100644 --- a/OnTopic.Tests/BindingModels/InvalidRelationshipListTypeTopicBindingModel.cs +++ b/OnTopic.Tests/BindingModels/InvalidRelationshipListTypeTopicBindingModel.cs @@ -3,9 +3,7 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ -using System; using System.Collections; -using System.Collections.Generic; using OnTopic.ViewModels.BindingModels; namespace OnTopic.Tests.BindingModels { diff --git a/OnTopic.Tests/BindingModels/InvalidRelationshipTypeTopicBindingModel.cs b/OnTopic.Tests/BindingModels/InvalidRelationshipTypeTopicBindingModel.cs index b8ea6c35..7c0690ca 100644 --- a/OnTopic.Tests/BindingModels/InvalidRelationshipTypeTopicBindingModel.cs +++ b/OnTopic.Tests/BindingModels/InvalidRelationshipTypeTopicBindingModel.cs @@ -3,9 +3,6 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ -using System; -using System.Collections.ObjectModel; -using OnTopic.Mapping.Annotations; using OnTopic.ViewModels.BindingModels; namespace OnTopic.Tests.BindingModels { diff --git a/OnTopic.Tests/BindingModels/MapToParentTopicBindingModel.cs b/OnTopic.Tests/BindingModels/MapToParentTopicBindingModel.cs index 832db2f9..f4d436e7 100644 --- a/OnTopic.Tests/BindingModels/MapToParentTopicBindingModel.cs +++ b/OnTopic.Tests/BindingModels/MapToParentTopicBindingModel.cs @@ -3,7 +3,6 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ -using OnTopic.Mapping.Annotations; namespace OnTopic.Tests.BindingModels { diff --git a/OnTopic.Tests/BindingModels/PageTopicBindingModel.cs b/OnTopic.Tests/BindingModels/PageTopicBindingModel.cs index 5a22c894..a091d352 100644 --- a/OnTopic.Tests/BindingModels/PageTopicBindingModel.cs +++ b/OnTopic.Tests/BindingModels/PageTopicBindingModel.cs @@ -5,7 +5,6 @@ \=============================================================================================================================*/ using System.ComponentModel; using System.ComponentModel.DataAnnotations; -using OnTopic.Mapping.Annotations; namespace OnTopic.Tests.BindingModels { diff --git a/OnTopic.Tests/ContentTypeDescriptorTest.cs b/OnTopic.Tests/ContentTypeDescriptorTest.cs index 75877cf9..0d928dbc 100644 --- a/OnTopic.Tests/ContentTypeDescriptorTest.cs +++ b/OnTopic.Tests/ContentTypeDescriptorTest.cs @@ -3,9 +3,6 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ -using System; -using System.Diagnostics.CodeAnalysis; -using System.Linq; using OnTopic.Metadata; using Xunit; @@ -39,7 +36,7 @@ public void ContentTypeDescriptor_PermittedContentTypes_ReturnsCollection() { page.Relationships.SetValue("ContentTypes", page); page.Relationships.SetValue("ContentTypes", video); - Assert.Equal(2, page.PermittedContentTypes.Count); + Assert.Equal(2, page.PermittedContentTypes.Count); Assert.Empty(video.PermittedContentTypes); Assert.Contains(page, page.PermittedContentTypes); Assert.Contains(video, page.PermittedContentTypes); @@ -71,7 +68,7 @@ public void ContentTypeDescriptor_ResetPermittedContentTypes_ReturnsUpdated() { page.Relationships.Remove("ContentTypes", page); page.Relationships.SetValue("ContentTypes", slideshow); - Assert.Equal(2, page.PermittedContentTypes.Count); + Assert.Equal(2, page.PermittedContentTypes.Count); Assert.Contains(video, page.PermittedContentTypes); Assert.Contains(slideshow, page.PermittedContentTypes); Assert.DoesNotContain(page, page.PermittedContentTypes); @@ -100,7 +97,7 @@ public void ContentTypeDescriptor_AttributeDescriptors_ReturnsInherited() { var urlAttribute = new AttributeDescriptor("Url", "AttributeDescriptor", videoAttributes); Assert.Single(page.AttributeDescriptors); - Assert.Equal(2, video.AttributeDescriptors.Count); + Assert.Equal(2, video.AttributeDescriptors.Count); Assert.Contains(titleAttribute, video.AttributeDescriptors); Assert.Contains(urlAttribute, video.AttributeDescriptors); @@ -134,7 +131,7 @@ public void ContentTypeDescriptor_ResetAttributeDescriptors_ReturnsUpdated() { page.ResetAttributeDescriptors(); Assert.Single(page.AttributeDescriptors); - Assert.Equal(2, video.AttributeDescriptors.Count); + Assert.Equal(2, video.AttributeDescriptors.Count); Assert.Contains(descriptionAttribute, video.AttributeDescriptors); Assert.Contains(urlAttribute, video.AttributeDescriptors); Assert.DoesNotContain(titleAttribute, video.AttributeDescriptors); @@ -199,7 +196,7 @@ public void ContentTypeDescriptorCollection_ConstructWithValues_ReturnsValues() var contentTypeCollection = new ContentTypeDescriptorCollection(rootContentType); - Assert.Equal(3, contentTypeCollection.Count); + Assert.Equal(3, contentTypeCollection.Count); } @@ -225,7 +222,7 @@ public void ContentTypeDescriptorCollection_Refresh_ReturnsUpdated() { contentTypeCollection.Refresh(rootContentType); - Assert.Equal(3, contentTypeCollection.Count); + Assert.Equal(3, contentTypeCollection.Count); Assert.Contains(slideshowContentType, contentTypeCollection); Assert.DoesNotContain(videoContentType, contentTypeCollection); diff --git a/OnTopic.Tests/ContractTest.cs b/OnTopic.Tests/ContractTest.cs index 1b56c5ba..e397ff0c 100644 --- a/OnTopic.Tests/ContractTest.cs +++ b/OnTopic.Tests/ContractTest.cs @@ -3,9 +3,7 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ -using System; -using System.Diagnostics.CodeAnalysis; -using OnTopic.Internal.Diagnostics; + using Xunit; namespace OnTopic.Tests { diff --git a/OnTopic.Tests/Entities/CustomTopic.cs b/OnTopic.Tests/Entities/CustomTopic.cs index 3ee6daf4..82e3a0c3 100644 --- a/OnTopic.Tests/Entities/CustomTopic.cs +++ b/OnTopic.Tests/Entities/CustomTopic.cs @@ -3,12 +3,8 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ -using System; using System.Globalization; -using OnTopic.Attributes; -using OnTopic.Internal.Diagnostics; using OnTopic.Associations; -using System.Diagnostics.CodeAnalysis; namespace OnTopic.Tests.Entities { diff --git a/OnTopic.Tests/Exceptions/NoMessageException.cs b/OnTopic.Tests/Exceptions/NoMessageException.cs index 25f801bf..1121c944 100644 --- a/OnTopic.Tests/Exceptions/NoMessageException.cs +++ b/OnTopic.Tests/Exceptions/NoMessageException.cs @@ -3,10 +3,7 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ -using System; -using System.Diagnostics.CodeAnalysis; using System.Runtime.Serialization; -using OnTopic.Internal.Diagnostics; #pragma warning disable CA1032 // Implement standard exception constructors diff --git a/OnTopic.Tests/Fixtures/TopicInfrastructureFixture.cs b/OnTopic.Tests/Fixtures/TopicInfrastructureFixture.cs index 660f6db1..3a9a4a79 100644 --- a/OnTopic.Tests/Fixtures/TopicInfrastructureFixture.cs +++ b/OnTopic.Tests/Fixtures/TopicInfrastructureFixture.cs @@ -3,20 +3,15 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; using OnTopic.Data.Caching; using OnTopic.Lookup; using OnTopic.Mapping; using OnTopic.Repositories; using OnTopic.TestDoubles; using OnTopic.Tests.TestDoubles; -using OnTopic.ViewModels; -namespace OnTopic.Tests.Fixtures { +namespace OnTopic.Tests.Fixtures +{ /*============================================================================================================================ | CLASS: TOPIC INFRASTRUCTURE FIXTURE diff --git a/OnTopic.Tests/Fixtures/TypeAccessorFixture.cs b/OnTopic.Tests/Fixtures/TypeAccessorFixture.cs new file mode 100644 index 00000000..3ccea59e --- /dev/null +++ b/OnTopic.Tests/Fixtures/TypeAccessorFixture.cs @@ -0,0 +1,40 @@ +/*============================================================================================================================== +| Author Ignia, LLC +| Client Ignia, LLC +| Project Topics Library +\=============================================================================================================================*/ +using OnTopic.Internal.Reflection; + +namespace OnTopic.Tests.Fixtures +{ + + /*============================================================================================================================ + | CLASS: TYPE ACCESSOR FIXTURE + \---------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Introduces a shared context to use for unit tests depending on an . + /// + public class TypeAccessorFixture { + + /*========================================================================================================================== + | CONSTRUCTOR + \-------------------------------------------------------------------------------------------------------------------------*/ + public TypeAccessorFixture() { + + /*------------------------------------------------------------------------------------------------------------------------ + | Create type accessor + \-----------------------------------------------------------------------------------------------------------------------*/ + TypeAccessor = new TypeAccessor(typeof(T)); + + } + + /*========================================================================================================================== + | TYPE ACCESSOR + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// A for accessing instances. + /// + internal TypeAccessor TypeAccessor { get; private set; } + + } +} diff --git a/OnTopic.Tests/HierarchicalTopicMappingServiceTest.cs b/OnTopic.Tests/HierarchicalTopicMappingServiceTest.cs index cd5e2dcc..ef20f949 100644 --- a/OnTopic.Tests/HierarchicalTopicMappingServiceTest.cs +++ b/OnTopic.Tests/HierarchicalTopicMappingServiceTest.cs @@ -3,16 +3,11 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ -using System; -using System.Diagnostics.CodeAnalysis; -using System.Threading.Tasks; using OnTopic.Data.Caching; -using OnTopic.Internal.Diagnostics; using OnTopic.Mapping.Hierarchical; using OnTopic.Repositories; using OnTopic.TestDoubles; using OnTopic.Tests.Fixtures; -using OnTopic.ViewModels; using Xunit; namespace OnTopic.Tests { @@ -150,7 +145,7 @@ public async Task GetViewModel_WithTwoLevels_ReturnsGraph() { var viewModel = await _hierarchicalMappingService.GetViewModelAsync(rootTopic, 1).ConfigureAwait(false); Assert.NotNull(viewModel); - Assert.Equal(3, viewModel?.Children.Count); + Assert.Equal(3, viewModel?.Children.Count); Assert.Empty(viewModel?.Children[0].Children); } diff --git a/OnTopic.Tests/ITopicRepositoryTest.cs b/OnTopic.Tests/ITopicRepositoryTest.cs index 74b8cd1f..13ef2e05 100644 --- a/OnTopic.Tests/ITopicRepositoryTest.cs +++ b/OnTopic.Tests/ITopicRepositoryTest.cs @@ -3,12 +3,7 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ -using System; -using System.Diagnostics.CodeAnalysis; -using System.Linq; -using OnTopic.Attributes; using OnTopic.Data.Caching; -using OnTopic.Internal.Diagnostics; using OnTopic.Repositories; using OnTopic.TestDoubles; using OnTopic.Tests.Fixtures; @@ -27,7 +22,7 @@ namespace OnTopic.Tests { /// underlying functions are also operating correctly. /// [ExcludeFromCodeCoverage] - [Collection("Shared Repository")] + [Xunit.Collection("Shared Repository")] public class ITopicRepositoryTest { /*========================================================================================================================== @@ -72,7 +67,7 @@ public void Load_Default_ReturnsTopicTopic() { var rootTopic = _topicRepository.Load(); - Assert.Equal(2, rootTopic?.Children.Count); + Assert.Equal(2, rootTopic?.Children.Count); Assert.Equal("Configuration", rootTopic?.Children.First().Key); Assert.Equal("Web", rootTopic?.Children.Last().Key); @@ -138,12 +133,12 @@ public void Save() { _topicRepository.Save(topic); - Assert.NotEqual(-1, topic.Id); - Assert.Equal(-1, child.Id); + Assert.NotEqual(-1, topic.Id); + Assert.Equal(-1, child.Id); _topicRepository.Save(topic, true); - Assert.NotEqual(-1, child.Id); + Assert.NotEqual(-1, child.Id); } @@ -163,7 +158,7 @@ public void Move_ToNewParent_ConfirmedMove() { _topicRepository.Move(topic, destination); - Assert.Equal(topic.Parent, destination); + Assert.Equal(topic.Parent, destination); Assert.Single(source.Children); Assert.Single(destination.Children); @@ -184,8 +179,8 @@ public void Move_ToNewSibling_ConfirmedMove() { _topicRepository.Move(topic, parent, sibling); - Assert.Equal(topic.Parent, parent); - Assert.Equal(2, parent.Children.Count); + Assert.Equal(topic.Parent, parent); + Assert.Equal(2, parent.Children.Count); Assert.Equal("Sibling", parent.Children.First().Key); Assert.Equal("Topic", parent.Children[1].Key); diff --git a/OnTopic.Tests/ITypeLookupServiceTest.cs b/OnTopic.Tests/ITypeLookupServiceTest.cs index 24b57786..50d8a007 100644 --- a/OnTopic.Tests/ITypeLookupServiceTest.cs +++ b/OnTopic.Tests/ITypeLookupServiceTest.cs @@ -3,12 +3,9 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ -using System; -using System.Diagnostics.CodeAnalysis; using OnTopic.Lookup; using OnTopic.Tests.TestDoubles; using OnTopic.Tests.ViewModels; -using OnTopic.ViewModels; using Xunit; namespace OnTopic.Tests { @@ -38,8 +35,8 @@ public void Composite_LookupValidType_ReturnsType() { var lookupServiceB = new TopicViewModelLookupService(); var compositeLookup = new CompositeTypeLookupService(lookupServiceA, lookupServiceB); - Assert.Equal(typeof(SlideshowTopicViewModel), compositeLookup.Lookup(nameof(SlideshowTopicViewModel))); - Assert.Equal(typeof(MapToParentTopicViewModel), compositeLookup.Lookup(nameof(MapToParentTopicViewModel))); + Assert.Equal(typeof(SlideshowTopicViewModel), compositeLookup.Lookup(nameof(SlideshowTopicViewModel))); + Assert.Equal(typeof(MapToParentTopicViewModel), compositeLookup.Lookup(nameof(MapToParentTopicViewModel))); Assert.Null(compositeLookup.Lookup(nameof(Topic))); } @@ -57,7 +54,7 @@ public void DynamicTopicViewModelLookupService_LookupTopicViewModel_ReturnsFallb var lookupService = new DynamicTopicViewModelLookupService(); var topicViewModel = lookupService.Lookup("FallbackTopicViewModel", "FallbackViewModel"); - Assert.Equal(typeof(FallbackViewModel), topicViewModel); + Assert.Equal(typeof(FallbackViewModel), topicViewModel); } @@ -74,7 +71,7 @@ public void TopicViewModelLookupService_LookupTopicViewModel_ReturnsFallbackView var lookupService = new FakeViewModelLookupService(); var topicViewModel = lookupService.Lookup("FallbackTopicViewModel", "FallbackViewModel"); - Assert.Equal(typeof(FallbackViewModel), topicViewModel); + Assert.Equal(typeof(FallbackViewModel), topicViewModel); } diff --git a/OnTopic.Tests/KeyedTopicCollectionTest.cs b/OnTopic.Tests/KeyedTopicCollectionTest.cs index 05f3b573..82c33fc1 100644 --- a/OnTopic.Tests/KeyedTopicCollectionTest.cs +++ b/OnTopic.Tests/KeyedTopicCollectionTest.cs @@ -3,10 +3,6 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ -using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.Linq; using OnTopic.Collections; using Xunit; @@ -57,7 +53,7 @@ public void Constructor_IEnumerable_SeedsTopics() { var topicsCollection = new KeyedTopicCollection(topics); - Assert.Equal(10, topicsCollection.Count); + Assert.Equal(10, topicsCollection.Count); } @@ -110,7 +106,7 @@ public void ReadOnlyKeyedTopicCollection_GetValue_ReturnsValue() { collection.Add(topic); - Assert.Equal(topic, readOnlyCollection.GetValue(topic.Key)); + Assert.Equal(topic, readOnlyCollection.GetValue(topic.Key)); } @@ -131,7 +127,7 @@ public void ReadOnlyKeyedTopicCollection_Indexer_ReturnsValue() { collection.Add(topic); - Assert.Equal(topic, readOnlyCollection[topic.Key]); + Assert.Equal(topic, readOnlyCollection[topic.Key]); } @@ -165,7 +161,7 @@ public void AsReadOnly_ReturnsReadOnlyKeyedTopicCollection() { var readOnlyCollection = topics.AsReadOnly(); - Assert.Equal(10, readOnlyCollection.Count); + Assert.Equal(10, readOnlyCollection.Count); Assert.Equal("Topic0", readOnlyCollection.First().Key); } @@ -187,7 +183,7 @@ public void AsReadOnly_ReturnsReadOnlyTopicCollection() { var readOnlyCollection = topics.AsReadOnly(); - Assert.Equal(10, readOnlyCollection.Count); + Assert.Equal(10, readOnlyCollection.Count); Assert.Equal("Topic0", readOnlyCollection.First().Key); } diff --git a/OnTopic.Tests/MemberAccessorTest.cs b/OnTopic.Tests/MemberAccessorTest.cs new file mode 100644 index 00000000..1b61c67c --- /dev/null +++ b/OnTopic.Tests/MemberAccessorTest.cs @@ -0,0 +1,407 @@ +/*============================================================================================================================== +| Author Ignia, LLC +| Client Ignia, LLC +| Project Topics Library +\=============================================================================================================================*/ +using System.Reflection; +using OnTopic.Internal.Reflection; +using OnTopic.Tests.ViewModels; +using Xunit; + +namespace OnTopic.Tests { + + /*============================================================================================================================ + | CLASS: MEMBER ACCESSOR TEST + \---------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Provides basic unit tests for the class. + /// + /// + /// Most access to the class will be through higher-level classes which provide type validation + /// and conversion. As a result, the unit tests for the will focus on the more fundamental + /// behavior of validating and translating a object into a . + /// + [ExcludeFromCodeCoverage] + public class MemberAccessorTest { + + /*========================================================================================================================== + | TEST: IS NULLABLE: NULLABLE PROPERTY: RETURNS TRUE + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Assembles a new from a that is nullable, and confirms that the + /// property is set to true. + /// + [Fact] + public void IsNullable_NullableProperty_ReturnsTrue() { + + var type = typeof(MemberAccessorViewModel); + var memberInfo = type.GetMember(nameof(MemberAccessorViewModel.NullableProperty)).FirstOrDefault()!; + var memberAccessor = new MemberAccessor(memberInfo); + + Assert.True(memberAccessor.IsNullable); + + } + + /*========================================================================================================================== + | TEST: IS NULLABLE: NON-NULLABLE PROPERTY: RETURNS FALSE + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Assembles a new from a that is not nullable, and confirms that + /// the property is set to false. + /// + [Fact] + public void IsNullable_NonNullableProperty_ReturnsFalse() { + + var type = typeof(MemberAccessorViewModel); + var memberInfo = type.GetMember(nameof(MemberAccessorViewModel.NonNullableProperty)).FirstOrDefault()!; + var memberAccessor = new MemberAccessor(memberInfo); + + Assert.False(memberAccessor.IsNullable); + + } + + /*========================================================================================================================== + | TEST: IS NULLABLE: NON-NULLABLE REFERENCE PROPERTY: RETURNS TRUE + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Assembles a new from a that is a non-nullable reference type, + /// and confirms that the property is set to true. + /// + /// + /// Unfortunately, the .NET reflection libraries don't (yet) have the ability to determine if a nullable reference types + /// are nullable or not, and thus this should always return true. + /// + [Fact] + public void IsNullable_NonNullableReferenceProperty_ReturnsTrue() { + + var type = typeof(MemberAccessorViewModel); + var memberInfo = type.GetMember(nameof(MemberAccessorViewModel.NonNullableReferenceGetter)).FirstOrDefault()!; + var memberAccessor = new MemberAccessor(memberInfo); + + Assert.True(memberAccessor.IsNullable); + + } + + /*========================================================================================================================== + | TEST: IS GETTABLE: READ-ONLY PROPERTY: RETURNS TRUE + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Assembles a new from a that is read-only, and confirms that the + /// property is set to true. + /// + [Fact] + public void IsGettable_ReadOnlyProperty_ReturnsTrue() { + + var type = typeof(MemberAccessorViewModel); + var memberInfo = type.GetMember(nameof(MemberAccessorViewModel.ReadOnlyProperty)).FirstOrDefault()!; + var memberAccessor = new MemberAccessor(memberInfo); + + Assert.True(memberAccessor.CanRead); + + } + + /*========================================================================================================================== + | TEST: IS GETTABLE: WRITE-ONLY PROPERTY: RETURNS FALSE + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Assembles a new from a that is write-only, and confirms that the + /// property is set to false. + /// + [Fact] + public void IsGettable_WriteOnlyProperty_ReturnsFalse() { + + var type = typeof(MemberAccessorViewModel); + var memberInfo = type.GetMember(nameof(MemberAccessorViewModel.WriteOnlyProperty)).FirstOrDefault()!; + var memberAccessor = new MemberAccessor(memberInfo); + + Assert.False(memberAccessor.CanRead); + + } + + /*========================================================================================================================== + | TEST: IS SETTABLE: WRITE-ONLY PROPERTY: RETURNS TRUE + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Assembles a new from a that is write-only, and confirms that the + /// property is set to false. + /// + [Fact] + public void IsSettable_WriteOnlyProperty_ReturnsTrue() { + + var type = typeof(MemberAccessorViewModel); + var memberInfo = type.GetMember(nameof(MemberAccessorViewModel.WriteOnlyProperty)).FirstOrDefault()!; + var memberAccessor = new MemberAccessor(memberInfo); + + Assert.True(memberAccessor.CanWrite); + + } + + /*========================================================================================================================== + | TEST: IS SETTABLE: READ-ONLY PROPERTY: RETURNS FALSE + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Assembles a new from a that is read-only, and confirms that the + /// property is set to false. + /// + [Fact] + public void IsSettable_ReadOnlyProperty_ReturnsFalse() { + + var type = typeof(MemberAccessorViewModel); + var memberInfo = type.GetMember(nameof(MemberAccessorViewModel.ReadOnlyProperty)).FirstOrDefault()!; + var memberAccessor = new MemberAccessor(memberInfo); + + Assert.False(memberAccessor.CanWrite); + + } + + /*========================================================================================================================== + | TEST: GET VALUE: VALID PROPERTY: RETURNS VALUE + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Assembles a new from a , and attempts to call with a compliant object, expecting that the correct value will be returned. + /// + [Fact] + public void GetValue_ValidProperty_ReturnsValue() { + + var type = typeof(MemberAccessorViewModel); + var memberInfo = type.GetMember(nameof(MemberAccessorViewModel.NonNullableProperty)).FirstOrDefault()!; + var memberAccessor = new MemberAccessor(memberInfo); + var sourceObject = new MemberAccessorViewModel() { NonNullableProperty = 15 }; + var returnObject = memberAccessor.GetValue(sourceObject); + + Assert.Equal(sourceObject.NonNullableProperty, returnObject); + + } + + /*========================================================================================================================== + | TEST: GET VALUE: VALID METHOD: RETURNS VALUE + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Assembles a new from a , and attempts to call with a compliant object, expecting that the correct value will be returned. + /// + [Theory] + [InlineData(15)] + [InlineData(null)] + public void GetValue_ValidMethod_ReturnsValue(int? value) { + + var type = typeof(MemberAccessorViewModel); + var memberInfo = type.GetMember(nameof(MemberAccessorViewModel.GetMethod)).FirstOrDefault()!; + var memberAccessor = new MemberAccessor(memberInfo); + var sourceObject = new MemberAccessorViewModel(); + + sourceObject.SetMethod(value); + + var returnObject = memberAccessor.GetValue(sourceObject); + + Assert.Equal(value, returnObject); + + } + + /*========================================================================================================================== + | TEST: GET VALUE: TYPE MISMATCH: THROWS EXCEPTION + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Assembles a new from a , and attempts to call with an object that doesn't contain the , expecting that an + /// will be thrown. + /// + [Fact] + public void GetValue_TypeMismatch_ThrowsException() { + + var type = typeof(MemberAccessorViewModel); + var memberInfo = type.GetMember(nameof(MemberAccessorViewModel.ReadOnlyProperty)).FirstOrDefault()!; + var memberAccessor = new MemberAccessor(memberInfo); + + Assert.Throws(() => + memberAccessor.GetValue(new object()) + ); + + } + + /*========================================================================================================================== + | TEST: SET VALUE: VALID PROPERTY: SETS VALUE + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Assembles a new from a , and attempts to call with a compliant object, expecting that the correct value will be + /// set. + /// + [Theory] + [InlineData(15)] + [InlineData(null)] + public void SetValue_ValidProperty_SetsValue(int? value) { + + var type = typeof(MemberAccessorViewModel); + var memberInfo = type.GetMember(nameof(MemberAccessorViewModel.NullableProperty)).FirstOrDefault()!; + var memberAccessor = new MemberAccessor(memberInfo); + var sourceObject = new MemberAccessorViewModel { + NullableProperty = 5 + }; + memberAccessor.SetValue(sourceObject, value); + + Assert.Equal(value, sourceObject.NullableProperty); + + } + + /*========================================================================================================================== + | TEST: SET VALUE: VALID METHOD: SETS VALUE + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Assembles a new from a , and attempts to call with a compliant object, expecting that the correct value will be + /// set. + /// + [Theory] + [InlineData(15)] + [InlineData(null)] + public void SetValue_ValidMethod_SetsValue(int? value) { + + var type = typeof(MemberAccessorViewModel); + var memberInfo = type.GetMember(nameof(MemberAccessorViewModel.SetMethod)).FirstOrDefault()!; + var memberAccessor = new MemberAccessor(memberInfo); + var sourceObject = new MemberAccessorViewModel(); + + sourceObject.SetMethod(5); + memberAccessor.SetValue(sourceObject, value); + + Assert.Equal(value, sourceObject.GetMethod()); + + } + + /*========================================================================================================================== + | TEST: SET VALUE: MEMBER TYPE MISMATCH: THROWS EXCEPTION + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Assembles a new from a , and attempts to call with an object that isn't compatible with the , expecting that an will be thrown. + /// + [Fact] + public void SetValue_MemberTypeMismatch_ThrowsException() { + + var type = typeof(MemberAccessorViewModel); + var memberInfo = type.GetMember(nameof(MemberAccessorViewModel.NonNullableReferenceGetter)).FirstOrDefault()!; + var memberAccessor = new MemberAccessor(memberInfo); + + Assert.Throws(() => + memberAccessor.SetValue(new MemberAccessorViewModel(), new object()) + ); + + } + + /*========================================================================================================================== + | TEST: IS VALID: PROPERTY: RETURNS TRUE + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Validates a that is a and confirms that it is accepted. + /// + [Fact] + public void IsValid_Property_ReturnsTrue() { + + var type = typeof(MemberAccessorViewModel); + var memberInfo = type.GetProperty(nameof(MemberAccessorViewModel.NonNullableProperty))!; + + Assert.True(MemberAccessor.IsValid(memberInfo)); + + } + + /*========================================================================================================================== + | TEST: IS VALID: GETTER METHOD: RETURNS TRUE + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Validates a that is a with a return type and no parameters and + /// confirms that it is accepted. + /// + [Fact] + public void IsValid_GetterMethod_ReturnsTrue() { + + var type = typeof(MemberAccessorViewModel); + var memberInfo = type.GetMethod(nameof(MemberAccessorViewModel.GetMethod))!; + + Assert.NotNull(memberInfo); + Assert.True(MemberAccessor.IsValid(memberInfo)); + + } + + /*========================================================================================================================== + | TEST: IS VALID: INVALID GETTER METHOD: RETURNS FALSE + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Validates a that is a with a parameter and confirms that it is + /// rejected. + /// + /// + /// The is only designed to be able to get methods that have a return type and no parameters. + /// + [Fact] + public void IsValid_InvalidGetterMethod_ReturnsFalse() + { + var type = typeof(MemberAccessorViewModel); + var memberInfo = type.GetMethod(nameof(MemberAccessorViewModel.InvalidGetMethod))!; + + Assert.NotNull(memberInfo); + Assert.False(MemberAccessor.IsValid(memberInfo)); + + } + + /*========================================================================================================================== + | TEST: IS VALID: SETTER METHOD: RETURNS TRUE + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Validates a that is a with no return type and one parameter and + /// confirms that it is accepted. + /// + [Fact] + public void IsValid_SetterMethod_ReturnsTrue() { + + var type = typeof(MemberAccessorViewModel); + var memberInfo = type.GetMethod(nameof(MemberAccessorViewModel.SetMethod))!; + + Assert.True(MemberAccessor.IsValid(memberInfo)); + + } + + /*========================================================================================================================== + | TEST: IS VALID: INVALID SETTER METHOD: RETURNS FALSE + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Validates a that is a with no return type and two parameters and + /// confirms that it is rejected. + /// + /// + /// The is only designed to be able to set methods that are void and accept a single + /// parameter. + /// + [Fact] + public void IsValid_InvalidSetterMethod_ReturnsFalse() + { + var type = typeof(MemberAccessorViewModel); + var memberInfo = type.GetMethod(nameof(MemberAccessorViewModel.InvalidSetMethod))!; + + Assert.NotNull(memberInfo); + Assert.False(MemberAccessor.IsValid(memberInfo)); + + } + + /*========================================================================================================================== + | TEST: IS VALID: CONSTRUCTOR: RETURNS FALSE + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Validates a that is not a or a and + /// confirms that it is rejected. + /// + [Fact] + public void IsValid_Constructor_ReturnsFalse() { + + var type = typeof(MemberAccessorViewModel); + var memberInfo = type.GetConstructor(Array.Empty())!; + + Assert.NotNull(memberInfo); + Assert.False(MemberAccessor.IsValid(memberInfo)); + + } + + } //Class +} //Namespace \ No newline at end of file diff --git a/OnTopic.Tests/OnTopic.Tests.csproj b/OnTopic.Tests/OnTopic.Tests.csproj index b072bfa0..580ff84d 100644 --- a/OnTopic.Tests/OnTopic.Tests.csproj +++ b/OnTopic.Tests/OnTopic.Tests.csproj @@ -1,19 +1,19 @@  - net5.0 + net6.0 false CS1591 - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + - + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/OnTopic.Tests/Properties/AssemblyInfo.cs b/OnTopic.Tests/Properties/AssemblyInfo.cs index 0b5717cb..ffc500a5 100644 --- a/OnTopic.Tests/Properties/AssemblyInfo.cs +++ b/OnTopic.Tests/Properties/AssemblyInfo.cs @@ -3,8 +3,20 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ -using System; -using System.Diagnostics.CodeAnalysis; + +/*============================================================================================================================== +| USING DIRECTIVES (GLOBAL) +\-----------------------------------------------------------------------------------------------------------------------------*/ +global using System.Collections.ObjectModel; +global using System.Diagnostics.CodeAnalysis; +global using OnTopic.Attributes; +global using OnTopic.Internal.Diagnostics; +global using OnTopic.Mapping.Annotations; +global using OnTopic.ViewModels; + +/*============================================================================================================================== +| USING DIRECTIVES (LOCAL) +\-----------------------------------------------------------------------------------------------------------------------------*/ using System.Runtime.InteropServices; /*============================================================================================================================== diff --git a/OnTopic.Tests/ReverseTopicMappingServiceTest.cs b/OnTopic.Tests/ReverseTopicMappingServiceTest.cs index 1957108f..c3ceeaa2 100644 --- a/OnTopic.Tests/ReverseTopicMappingServiceTest.cs +++ b/OnTopic.Tests/ReverseTopicMappingServiceTest.cs @@ -3,16 +3,10 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ -using System; using System.Collections; -using System.Collections.Generic; using System.ComponentModel.DataAnnotations; -using System.Diagnostics.CodeAnalysis; -using System.Threading.Tasks; using OnTopic.Data.Caching; -using OnTopic.Internal.Diagnostics; using OnTopic.Mapping; -using OnTopic.Mapping.Annotations; using OnTopic.Mapping.Reverse; using OnTopic.Metadata; using OnTopic.Models; @@ -21,7 +15,6 @@ using OnTopic.TestDoubles.Metadata; using OnTopic.Tests.BindingModels; using OnTopic.Tests.Fixtures; -using OnTopic.ViewModels; using Xunit; namespace OnTopic.Tests { @@ -92,7 +85,7 @@ public async Task Map_Generic_ReturnsNewTopic() { Assert.Equal("TextAttributeDescriptor", target?.ContentType); Assert.Equal("Test Attribute", target?.Title); Assert.Equal("Hello", target?.DefaultValue); - Assert.Equal(true, target?.IsRequired); + Assert.Equal(true, target?.IsRequired); } @@ -120,7 +113,7 @@ public async Task Map_Dynamic_ReturnsNewTopic() { Assert.Equal("TextAttributeDescriptor", target?.ContentType); Assert.Equal("Test Attribute", target?.Title); Assert.Equal("Hello", target?.DefaultValue); - Assert.Equal(true, target?.IsRequired); + Assert.Equal(true, target?.IsRequired); } @@ -158,8 +151,8 @@ public async Task Map_Existing_ReturnsUpdatedTopic() { Assert.Equal("TextAttributeDescriptor", target?.ContentType); Assert.Equal("Test", target?.Title); //Should inherit from "Key" since it will be null Assert.Equal("World", target?.DefaultValue); - Assert.Equal(false, target?.IsRequired); - Assert.Equal(false, target?.IsExtendedAttribute); + Assert.Equal(false, target?.IsRequired); + Assert.Equal(false, target?.IsExtendedAttribute); Assert.Equal("Original Description", target?.Attributes.GetValue("Description")); } @@ -205,7 +198,7 @@ public async Task Map_ComplexObject_ReturnsFlattenedTopic() { bindingModel.AlternateContact.Email = "AlternateContact@Ignia.com"; bindingModel.BillingContact.Email = "BillingContact@Ignia.com"; - var target = (Topic?)await _mappingService.MapAsync(bindingModel).ConfigureAwait(false); + var target = await _mappingService.MapAsync(bindingModel).ConfigureAwait(false); Assert.NotNull(target); Assert.Equal("Jeremy", target?.Attributes.GetValue("Name")); @@ -261,7 +254,7 @@ public async Task Map_Relationships_ReturnsMappedTopic() { var target = (ContentTypeDescriptor?)await _mappingService.MapAsync(bindingModel, topic).ConfigureAwait(false); - Assert.Equal(3, target?.PermittedContentTypes.Count); + Assert.Equal(3, target?.PermittedContentTypes.Count); Assert.True(target?.PermittedContentTypes.Contains(contentTypes[0])); Assert.True(target?.PermittedContentTypes.Contains(contentTypes[1])); Assert.True(target?.PermittedContentTypes.Contains(contentTypes[2])); @@ -323,7 +316,7 @@ public async Task Map_NestedTopics_ReturnsMappedTopic() { var target = (ContentTypeDescriptor?)await _mappingService.MapAsync(bindingModel, topic).ConfigureAwait(false); - Assert.Equal(3, target?.AttributeDescriptors.Count); + Assert.Equal(3, target?.AttributeDescriptors.Count); Assert.NotNull(target?.AttributeDescriptors.GetValue("Attribute1")); Assert.NotNull(target?.AttributeDescriptors.GetValue("Attribute2")); Assert.NotNull(target?.AttributeDescriptors.GetValue("Attribute3")); diff --git a/OnTopic.Tests/Schemas/AttributesDataTable.cs b/OnTopic.Tests/Schemas/AttributesDataTable.cs index b3bfb2ce..5edf1da7 100644 --- a/OnTopic.Tests/Schemas/AttributesDataTable.cs +++ b/OnTopic.Tests/Schemas/AttributesDataTable.cs @@ -3,11 +3,8 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ -using System; using System.Data; -using System.Diagnostics.CodeAnalysis; using OnTopic.Data.Sql; -using OnTopic.Internal.Diagnostics; namespace OnTopic.Tests.Schemas { diff --git a/OnTopic.Tests/Schemas/ExtendedAttributesDataTable.cs b/OnTopic.Tests/Schemas/ExtendedAttributesDataTable.cs index afe6508c..060d60c5 100644 --- a/OnTopic.Tests/Schemas/ExtendedAttributesDataTable.cs +++ b/OnTopic.Tests/Schemas/ExtendedAttributesDataTable.cs @@ -3,12 +3,9 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ -using System; using System.Data; -using System.Diagnostics.CodeAnalysis; using System.Xml; using OnTopic.Data.Sql; -using OnTopic.Internal.Diagnostics; namespace OnTopic.Tests.Schemas { diff --git a/OnTopic.Tests/Schemas/RelationshipsDataTable.cs b/OnTopic.Tests/Schemas/RelationshipsDataTable.cs index 4eb55c21..665c728f 100644 --- a/OnTopic.Tests/Schemas/RelationshipsDataTable.cs +++ b/OnTopic.Tests/Schemas/RelationshipsDataTable.cs @@ -3,11 +3,8 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ -using System; using System.Data; -using System.Diagnostics.CodeAnalysis; using OnTopic.Data.Sql; -using OnTopic.Internal.Diagnostics; namespace OnTopic.Tests.Schemas { diff --git a/OnTopic.Tests/Schemas/TopicReferencesDataTable.cs b/OnTopic.Tests/Schemas/TopicReferencesDataTable.cs index ac859f41..8639a101 100644 --- a/OnTopic.Tests/Schemas/TopicReferencesDataTable.cs +++ b/OnTopic.Tests/Schemas/TopicReferencesDataTable.cs @@ -3,11 +3,8 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ -using System; using System.Data; -using System.Diagnostics.CodeAnalysis; using OnTopic.Data.Sql; -using OnTopic.Internal.Diagnostics; namespace OnTopic.Tests.Schemas { diff --git a/OnTopic.Tests/Schemas/TopicsDataTable.cs b/OnTopic.Tests/Schemas/TopicsDataTable.cs index ece458ad..0c5a99a6 100644 --- a/OnTopic.Tests/Schemas/TopicsDataTable.cs +++ b/OnTopic.Tests/Schemas/TopicsDataTable.cs @@ -3,11 +3,8 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ -using System; using System.Data; -using System.Diagnostics.CodeAnalysis; using OnTopic.Data.Sql; -using OnTopic.Internal.Diagnostics; namespace OnTopic.Tests.Schemas { diff --git a/OnTopic.Tests/Schemas/VersionHistoryDataTable.cs b/OnTopic.Tests/Schemas/VersionHistoryDataTable.cs index 314fc867..9f7a712a 100644 --- a/OnTopic.Tests/Schemas/VersionHistoryDataTable.cs +++ b/OnTopic.Tests/Schemas/VersionHistoryDataTable.cs @@ -3,11 +3,8 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ -using System; using System.Data; -using System.Diagnostics.CodeAnalysis; using OnTopic.Data.Sql; -using OnTopic.Internal.Diagnostics; namespace OnTopic.Tests.Schemas { diff --git a/OnTopic.Tests/SqlTopicRepositoryTest.cs b/OnTopic.Tests/SqlTopicRepositoryTest.cs index bc19ed78..b8c1b5d8 100644 --- a/OnTopic.Tests/SqlTopicRepositoryTest.cs +++ b/OnTopic.Tests/SqlTopicRepositoryTest.cs @@ -3,10 +3,7 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ -using System; using System.Data; -using System.Diagnostics.CodeAnalysis; -using System.Linq; using System.Text; using Microsoft.Data.SqlClient; using OnTopic.Associations; @@ -44,7 +41,7 @@ public void LoadTopicGraph_WithTopic_ReturnsTopic() { var topic = tableReader.LoadTopicGraph(); Assert.NotNull(topic); - Assert.Equal(1, topic?.Id); + Assert.Equal(1, topic?.Id); } @@ -72,7 +69,7 @@ public void LoadTopicGraph_WithNewParent_UpdatesParent() { tableReader.LoadTopicGraph(topic); - Assert.Equal(parent2, child.Parent); + Assert.Equal(parent2, child.Parent); } @@ -97,7 +94,7 @@ public void LoadTopicGraph_WithAttributes_ReturnsAttributes() { var topic = tableReader.LoadTopicGraph(); Assert.NotNull(topic); - Assert.Equal(1, topic?.Id); + Assert.Equal(1, topic?.Id); Assert.Equal("Value", topic?.Attributes.GetValue("Test")); } @@ -154,8 +151,8 @@ public void LoadTopicGraph_WithRelationship_ReturnsRelationship() { var topic = tableReader.LoadTopicGraph(); Assert.NotNull(topic); - Assert.Equal(1, topic?.Id); - Assert.Equal(2, topic?.Relationships.GetValues("Test").FirstOrDefault()?.Id); + Assert.Equal(1, topic?.Id); + Assert.Equal(2, topic?.Relationships.GetValues("Test").FirstOrDefault()?.Id); Assert.True(topic?.Relationships.IsFullyLoaded); } @@ -183,7 +180,7 @@ public void LoadTopicGraph_WithMissingRelationship_NotFullyLoaded() { var topic = tableReader.LoadTopicGraph(); Assert.NotNull(topic); - Assert.Equal(1, topic?.Id); + Assert.Equal(1, topic?.Id); Assert.Empty(topic?.Relationships); Assert.False(topic?.Relationships.IsFullyLoaded); @@ -212,8 +209,8 @@ public void LoadTopicGraph_WithReference_ReturnsReference() { var topic = tableReader.LoadTopicGraph(); Assert.NotNull(topic); - Assert.Equal(1, topic?.Id); - Assert.Equal(2, topic?.References.GetValue("Test")?.Id); + Assert.Equal(1, topic?.Id); + Assert.Equal(2, topic?.References.GetValue("Test")?.Id); Assert.True(topic?.References.IsDirty()); } @@ -242,8 +239,8 @@ public void LoadTopicGraph_WithExternalReference_ReturnsReference() { var topic = tableReader.LoadTopicGraph(referenceTopic, false); Assert.NotNull(topic); - Assert.Equal(1, topic?.Id); - Assert.Equal(2, topic?.References.GetValue("Test")?.Id); + Assert.Equal(1, topic?.Id); + Assert.Equal(2, topic?.References.GetValue("Test")?.Id); Assert.True(topic?.References.IsFullyLoaded); Assert.False(topic?.References.IsDirty()); @@ -303,7 +300,7 @@ public void LoadTopicGraph_WithMissingReference_NotFullyLoaded() { var topic = tableReader.LoadTopicGraph(); Assert.NotNull(topic); - Assert.Equal(1, topic?.Id); + Assert.Equal(1, topic?.Id); Assert.Empty(topic?.References); Assert.False(topic?.References.IsFullyLoaded); @@ -362,7 +359,7 @@ public void LoadTopicGraph_WithVersionHistory_ReturnsVersions() { var topic = tableReader.LoadTopicGraph(); Assert.NotNull(topic); - Assert.Equal(1, topic?.Id); + Assert.Equal(1, topic?.Id); Assert.Single(topic?.VersionHistory); Assert.True(topic?.VersionHistory.Contains(DateTime.MinValue)); @@ -384,7 +381,7 @@ public void TopicListDataTable_AddRow_Succeeds() { dataTable.AddRow(2); dataTable.AddRow(2); - Assert.Equal(3, dataTable.Rows.Count); + Assert.Equal(3, dataTable.Rows.Count); Assert.Single(dataTable.Columns); dataTable.Dispose(); @@ -408,8 +405,8 @@ public void AttributeValuesDataTable_AddRow_Succeeds() { dataTable.AddRow("ContentType", "Page"); dataTable.AddRow("ParentId", "4"); - Assert.Equal(3, dataTable.Rows.Count); - Assert.Equal(2, dataTable.Columns.Count); + Assert.Equal(3, dataTable.Rows.Count); + Assert.Equal(2, dataTable.Columns.Count); dataTable.Dispose(); @@ -432,8 +429,8 @@ public void TopicReferencesDataTable_AddRow_Succeeds() { dataTable.AddRow("Parent", 2); dataTable.AddRow("RootTopic", 3); - Assert.Equal(3, dataTable.Rows.Count); - Assert.Equal(2, dataTable.Columns.Count); + Assert.Equal(3, dataTable.Rows.Count); + Assert.Equal(2, dataTable.Columns.Count); dataTable.Dispose(); @@ -458,7 +455,7 @@ public void SqlCommand_AddParameter_String() { Assert.Single(command.Parameters); Assert.True(command.Parameters.Contains("@TopicKey")); Assert.Equal("Root", (string?)sqlParameter?.Value); - Assert.Equal(SqlDbType.VarChar, sqlParameter?.SqlDbType); + Assert.Equal(SqlDbType.VarChar, sqlParameter?.SqlDbType); command.Dispose(); @@ -483,7 +480,7 @@ public void SqlCommand_AddParameter_NullString() { Assert.Single(command.Parameters); Assert.True(command.Parameters.Contains("@TopicKey")); Assert.Null(sqlParameter?.Value); - Assert.Equal(SqlDbType.VarChar, sqlParameter?.SqlDbType); + Assert.Equal(SqlDbType.VarChar, sqlParameter?.SqlDbType); command.Dispose(); @@ -507,8 +504,8 @@ public void SqlCommand_AddParameter_Int() { Assert.Single(command.Parameters); Assert.True(command.Parameters.Contains("@TopicId")); - Assert.Equal(5, (int?)sqlParameter?.Value); - Assert.Equal(SqlDbType.Int, sqlParameter?.SqlDbType); + Assert.Equal(5, (int?)sqlParameter?.Value); + Assert.Equal(SqlDbType.Int, sqlParameter?.SqlDbType); command.Dispose(); @@ -532,8 +529,8 @@ public void SqlCommand_AddParameter_Bool() { Assert.Single(command.Parameters); Assert.True(command.Parameters.Contains("@IsHidden")); - Assert.Equal(true, (bool?)sqlParameter?.Value); - Assert.Equal(SqlDbType.Bit, sqlParameter?.SqlDbType); + Assert.Equal(true, (bool?)sqlParameter?.Value); + Assert.Equal(SqlDbType.Bit, sqlParameter?.SqlDbType); command.Dispose(); @@ -558,8 +555,8 @@ public void SqlCommand_AddParameter_DateTime() { Assert.Single(command.Parameters); Assert.True(command.Parameters.Contains("@LastModified")); - Assert.Equal(lastModified, (DateTime?)sqlParameter?.Value); - Assert.Equal(SqlDbType.DateTime2, sqlParameter?.SqlDbType); + Assert.Equal(lastModified, (DateTime?)sqlParameter?.Value); + Assert.Equal(SqlDbType.DateTime2, sqlParameter?.SqlDbType); command.Dispose(); @@ -584,8 +581,8 @@ public void SqlCommand_AddParameter_DataTable() { Assert.Single(command.Parameters); Assert.True(command.Parameters.Contains("@Relationships")); - Assert.Equal(dataTable, (DataTable?)sqlParameter?.Value); - Assert.Equal(SqlDbType.Structured, sqlParameter?.SqlDbType); + Assert.Equal(dataTable, (DataTable?)sqlParameter?.Value); + Assert.Equal(SqlDbType.Structured, sqlParameter?.SqlDbType); command.Dispose(); dataTable.Dispose(); @@ -612,7 +609,7 @@ public void SqlCommand_AddParameter_StringBuilder() { Assert.Single(command.Parameters); Assert.True(command.Parameters.Contains("@AttributesXml")); Assert.Equal(xml.ToString(), (string?)sqlParameter?.Value); - Assert.Equal(SqlDbType.Xml, sqlParameter?.SqlDbType); + Assert.Equal(SqlDbType.Xml, sqlParameter?.SqlDbType); command.Dispose(); @@ -638,9 +635,9 @@ public void SqlCommand_AddOutputParameter_ReturnCode() { Assert.Single(command.Parameters); Assert.True(command.Parameters.Contains("@TopicId")); - Assert.Equal(5, command.GetReturnCode("TopicId")); - Assert.Equal(ParameterDirection.ReturnValue, sqlParameter?.Direction); - Assert.Equal(SqlDbType.Int, sqlParameter?.SqlDbType); + Assert.Equal(5, command.GetReturnCode("TopicId")); + Assert.Equal(ParameterDirection.ReturnValue, sqlParameter?.Direction); + Assert.Equal(SqlDbType.Int, sqlParameter?.SqlDbType); command.Dispose(); @@ -665,9 +662,9 @@ public void SqlCommand_AddOutputParameter_ReturnsDefault() { Assert.Single(command.Parameters); Assert.True(command.Parameters.Contains("@TopicId")); - Assert.Equal(-1, command.GetReturnCode("TopicId")); - Assert.Equal(ParameterDirection.ReturnValue, sqlParameter?.Direction); - Assert.Equal(SqlDbType.Int, sqlParameter?.SqlDbType); + Assert.Equal(-1, command.GetReturnCode("TopicId")); + Assert.Equal(ParameterDirection.ReturnValue, sqlParameter?.Direction); + Assert.Equal(SqlDbType.Int, sqlParameter?.SqlDbType); command.Dispose(); diff --git a/OnTopic.Tests/TestDoubles/DummyStaticTypeLookupService.cs b/OnTopic.Tests/TestDoubles/DummyStaticTypeLookupService.cs index bc5fcbcd..aed9d8ae 100644 --- a/OnTopic.Tests/TestDoubles/DummyStaticTypeLookupService.cs +++ b/OnTopic.Tests/TestDoubles/DummyStaticTypeLookupService.cs @@ -3,9 +3,6 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ -using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; namespace OnTopic.Lookup { diff --git a/OnTopic.Tests/TestDoubles/FakeViewModelLookupService.cs b/OnTopic.Tests/TestDoubles/FakeViewModelLookupService.cs index d6a3d435..304b6094 100644 --- a/OnTopic.Tests/TestDoubles/FakeViewModelLookupService.cs +++ b/OnTopic.Tests/TestDoubles/FakeViewModelLookupService.cs @@ -6,7 +6,6 @@ using OnTopic.Lookup; using OnTopic.Tests.ViewModels; using OnTopic.Tests.ViewModels.Metadata; -using OnTopic.ViewModels; namespace OnTopic.Tests.TestDoubles { diff --git a/OnTopic.Tests/TopicMappingServiceTest.cs b/OnTopic.Tests/TopicMappingServiceTest.cs index f18077b9..d2296953 100644 --- a/OnTopic.Tests/TopicMappingServiceTest.cs +++ b/OnTopic.Tests/TopicMappingServiceTest.cs @@ -3,18 +3,10 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ -using System; -using System.Collections.Generic; using System.ComponentModel.DataAnnotations; -using System.Diagnostics.CodeAnalysis; using System.Globalization; -using System.Linq; -using System.Threading.Tasks; -using OnTopic.Attributes; using OnTopic.Data.Caching; -using OnTopic.Internal.Diagnostics; using OnTopic.Mapping; -using OnTopic.Mapping.Annotations; using OnTopic.Mapping.Internal; using OnTopic.Metadata; using OnTopic.Repositories; @@ -24,7 +16,6 @@ using OnTopic.Tests.Fixtures; using OnTopic.Tests.ViewModels; using OnTopic.Tests.ViewModels.Metadata; -using OnTopic.ViewModels; using Xunit; namespace OnTopic.Tests { @@ -72,6 +63,140 @@ public TopicMappingServiceTest(TopicInfrastructureFixture f } + /*========================================================================================================================== + | TEST: MAP: LOAD TESTING: EVALUATE THRESHOLD + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Establishes a and tests creating and mapping instances in bulk, + /// as a quick-and-easy way of assessing performance. + /// + /// + /// + /// The includes functionality to map properties to attributes via a constructor that + /// accepts a . This introduces some overhead which is not cost effective if there are + /// not any attributes that map to properties. For larger numbers of mapped attributes, however, the can reduce the mapping time considerably, while also giving more control over the model + /// construction to the model developer. This test is intended to help identify and optimize that threshold based on + /// improvements to the underlying , , and convenience method. + /// + /// + /// This is only intended to be enabled when needed for specialized performance testing. + /// + /// + [Fact] + public async Task Map_LoadTesting_EvaluateThreshold() { + + /*------------------------------------------------------------------------------------------------------------------------ + | Establish variables + \-----------------------------------------------------------------------------------------------------------------------*/ + var runs = 0; // The number of mapping operations to perform + var propertyCount = 10; // The number of property values to set on the LoadTestingModel + + /*------------------------------------------------------------------------------------------------------------------------ + | Establish data model + \-----------------------------------------------------------------------------------------------------------------------*/ + var topic = new Topic("Test", "ContentList", null); + + for (var i = 0; i <= propertyCount; i++) { + topic.Attributes.SetInteger("Property"+i, i); + } + + /*------------------------------------------------------------------------------------------------------------------------ + | Run load testing + \-----------------------------------------------------------------------------------------------------------------------*/ + for (var i = 0; i < runs; i++) { + await _mappingService.MapAsync(topic).ConfigureAwait(false); + } + + /*------------------------------------------------------------------------------------------------------------------------ + | Always assume the test passed + \-----------------------------------------------------------------------------------------------------------------------*/ + Assert.True(true); + + } + + /*========================================================================================================================== + | TEST: MAP: LOAD TESTING: EVALUATE TIME + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Establishes a and tests creating and mapping instances in bulk, + /// as a quick-and-easy way of assessing performance. + /// + [Fact] + public async Task Map_LoadTesting_EvaluateTime() { + + /*------------------------------------------------------------------------------------------------------------------------ + | Establish variables + \-----------------------------------------------------------------------------------------------------------------------*/ + var runs = 0; // The number of mapping operations to perform + var useFullAttributeSet = false; // Include a larget set of attribute values + var includeNestedTopics = false; // Only include a minimal set of properties + + /*------------------------------------------------------------------------------------------------------------------------ + | Establish object model + \-----------------------------------------------------------------------------------------------------------------------*/ + var currentId = 1; + var baseTopic = new Topic("Base", "Page", null, currentId++); + var grandparent = new Topic("Grandparent", "Page", null, currentId++); + var parent = new Topic("Parent", "Page", grandparent, currentId++); + var topic = new Topic("Test", "ContentList", parent, currentId++); + var contentItems = new Topic("ContentItems", "List", topic, currentId++); + + if (includeNestedTopics) { + _ = new Topic("Item1", "ContentItem", contentItems, currentId++); + _ = new Topic("Item2", "ContentItem", contentItems, currentId++); + _ = new Topic("Item3", "ContentItem", contentItems, currentId++); + _ = new Topic("Item4", "ContentItem", contentItems, currentId++); + _ = new Topic("Item5", "ContentItem", contentItems, currentId++); + _ = new Topic("Item6", "ContentItem", contentItems, currentId++); + } + + grandparent.BaseTopic = baseTopic; + + /*------------------------------------------------------------------------------------------------------------------------ + | Populate attributes + \-----------------------------------------------------------------------------------------------------------------------*/ + foreach (var contentItem in contentItems.Children) { + contentItem.Attributes.SetDateTime("LastModified", DateTime.Now); + contentItem.Attributes.SetValue("Description", "Value1"); + if (useFullAttributeSet) { + contentItem.Attributes.SetValue("LearnMoreUrl", "/Topic/23/"); + contentItem.Attributes.SetValue("ThumbnailImage", "/Image.jpg"); + contentItem.Attributes.SetValue("Description", "Value1"); + contentItem.Attributes.SetValue("Category", "Gumby"); + } + } + + topic.Attributes.SetValue("Title", "Friendly Title"); + topic.Attributes.SetValue("IsHidden", "0"); + if (useFullAttributeSet) { + topic.Attributes.SetValue("View", "Test"); + topic.Attributes.SetValue("ShortTitle", "Short Title"); + topic.Attributes.SetValue("Subtitle", "Subtitle"); + topic.Attributes.SetValue("MetaTitle", "Meta Title"); + topic.Attributes.SetValue("MetaDescription", "Meta Description"); + topic.Attributes.SetValue("MetaKeywords", "Load;Test;Keywords"); + topic.Attributes.SetValue("NoIndex", "0"); + topic.Attributes.SetValue("Body", "Body of test topic"); + } + + baseTopic.Attributes.SetValue("Title", "Inherited Title"); + + /*------------------------------------------------------------------------------------------------------------------------ + | Run load testing + \-----------------------------------------------------------------------------------------------------------------------*/ + for (var i = 0; i <= runs; i++) { + await _mappingService.MapAsync(topic).ConfigureAwait(false); + } + + /*------------------------------------------------------------------------------------------------------------------------ + | Always assume the test passed + \-----------------------------------------------------------------------------------------------------------------------*/ + Assert.True(true); + + } + /*========================================================================================================================== | TEST: MAP: GENERIC: RETURNS NEW MODEL \-------------------------------------------------------------------------------------------------------------------------*/ @@ -156,6 +281,46 @@ public async Task Map_Generic_ReturnsConvertedProperty() { } + /*========================================================================================================================== + | TEST: MAP: ATTRIBUTE DICTIONARY: RETURNS NEW MODEL + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Establishes a and attempts to map a view model with a constructor containing a . Confirms that the expected model is returned. + /// + [Fact] + public async Task Map_AttributeDictionary_ReturnsNewModel() { + + var topic = new Topic("Test", "Page"); + var lastModified = new DateTime(2021, 12, 22); + + topic.Attributes.SetValue("Title", "Value"); + topic.Attributes.SetValue("ShortTitle", "Short Title"); + topic.Attributes.SetValue("Subtitle", "Subtitle"); + topic.Attributes.SetValue("MetaTitle", "Meta Title"); + topic.Attributes.SetValue("MetaDescription", "Meta Description"); + topic.Attributes.SetValue("MetaKeywords", "Load;Test;Keywords"); + topic.Attributes.SetValue("NoIndex", "0"); + topic.Attributes.SetValue("Body", "Body of test topic"); + topic.Attributes.SetValue("MappedProperty", "Mapped Value"); + topic.Attributes.SetValue("UnmappedProperty", "Unmapped Value"); + topic.VersionHistory.Add(lastModified); + + var target = await _mappingService.MapAsync(topic).ConfigureAwait(false); + + Assert.Equal("Value", target?.Title); + Assert.Equal("Short Title", target?.ShortTitle); + Assert.Equal("Subtitle", target?.Subtitle); + Assert.Equal("Meta Title", target?.MetaTitle); + Assert.Equal("Meta Description", target?.MetaDescription); + Assert.Equal(false, target?.NoIndex); + Assert.Equal("Load;Test;Keywords", target?.MetaKeywords); + Assert.Equal("Mapped Value", target?.MappedProperty); + Assert.Null(target?.UnmappedProperty); + Assert.Equal(lastModified, target?.LastModified); + + } + /*========================================================================================================================== | TEST: MAP: CONSTRUCTOR: RETURNS NEW MODEL \-------------------------------------------------------------------------------------------------------------------------*/ @@ -189,7 +354,7 @@ public async Task Map_Constructor_ReturnsNewModel() { Assert.Equal("Foo", target?.ScalarValue); Assert.NotNull(target?.TopicReference); Assert.NotNull(target?.Relationships); - Assert.Equal(5, target?.OptionalValue); + Assert.Equal(5, target?.OptionalValue); Assert.Single(target?.Relationships); Assert.Equal("Bar", target?.TopicReference?.ScalarValue); @@ -353,8 +518,8 @@ public async Task Map_NullablePropertiesWithInvalidValues_ReturnsNullOrDefault() //The following should not be null since they map to non-nullable properties which will have default values Assert.Equal(topic.Title, target?.Title); - Assert.Equal(topic.IsHidden, target?.IsHidden); - Assert.Equal(topic.LastModified, target?.LastModified); + Assert.Equal(topic.IsHidden, target?.IsHidden); + Assert.Equal(topic.LastModified, target?.LastModified); } @@ -384,15 +549,15 @@ public async Task Map_NullablePropertiesWithInvalidValues_ReturnsValues() { var target = await _mappingService.MapAsync(topic).ConfigureAwait(false); Assert.Equal("Hello World.", target?.NullableString); - Assert.Equal(43, target?.NullableInteger); - Assert.Equal(3.14159265359, target?.NullableDouble); - Assert.Equal(true, target?.NullableBoolean); - Assert.Equal(new(1976, 10, 15), target?.NullableDateTime); - Assert.Equal(new("/Web/Path/File?Query=String", UriKind.RelativeOrAbsolute), target?.NullableUrl); + Assert.Equal(43, target?.NullableInteger); + Assert.Equal(3.14159265359, target?.NullableDouble); + Assert.Equal(true, target?.NullableBoolean); + Assert.Equal(new(1976, 10, 15), target?.NullableDateTime); + Assert.Equal(new("/Web/Path/File?Query=String", UriKind.RelativeOrAbsolute), target?.NullableUrl); Assert.Equal(topic.Title, target?.Title); - Assert.Equal(topic.IsHidden, target?.IsHidden); - Assert.Equal(topic.LastModified, target?.LastModified); + Assert.Equal(topic.IsHidden, target?.IsHidden); + Assert.Equal(topic.LastModified, target?.LastModified); } @@ -436,7 +601,7 @@ public void MappedTopicCache_TryGetValue_ReturnsEntry() { var isSuccess = cache.TryGetValue(topicId, viewModel.GetType(), out var result); Assert.True(isSuccess); - Assert.Equal(viewModel, result?.MappedTopic); + Assert.Equal(viewModel, result?.MappedTopic); } @@ -482,7 +647,7 @@ public void MappedTopicCache_GetOrAdd_ReturnsExisting() { var isSuccess = cache.TryGetValue(1, newViewModel.GetType(), out var result); Assert.True(isSuccess); - Assert.Equal(initialViewModel, result?.MappedTopic); + Assert.Equal(initialViewModel, result?.MappedTopic); } @@ -574,7 +739,7 @@ public void MappedTopicCacheEntry_AddMissingAssociations_SetsUnion() { cacheEntry.AddMissingAssociations(associations); - Assert.Equal(AssociationTypes.Children | AssociationTypes.Parents, cacheEntry.Associations); + Assert.Equal(AssociationTypes.Children | AssociationTypes.Parents, cacheEntry.Associations); } @@ -598,7 +763,7 @@ public async Task Map_Relationships_ReturnsMappedModel() { var target = await _mappingService.MapAsync(topic).ConfigureAwait(false); - Assert.Equal(2, target?.Cousins.Count); + Assert.Equal(2, target?.Cousins.Count); Assert.NotNull(GetChildTopic(target?.Cousins, "Cousin1")); Assert.NotNull(GetChildTopic(target?.Cousins, "Cousin2")); Assert.Null(GetChildTopic(target?.Cousins, "Sibling")); @@ -683,8 +848,8 @@ public async Task Map_CustomCollection_ReturnsCollection() { var topic = (ContentTypeDescriptor?)_topicRepository.Load("Root:Configuration:ContentTypes:Page"); var target = await _mappingService.MapAsync(topic).ConfigureAwait(false); - Assert.Equal(8, target?.AttributeDescriptors.Count); - Assert.Equal(2, target?.PermittedContentTypes.Count); + Assert.Equal(8, target?.AttributeDescriptors.Count); + Assert.Equal(2, target?.PermittedContentTypes.Count); //Ensure custom collections are not recursively followed without instruction Assert.NotEqual(topic, topic?.PermittedContentTypes.LastOrDefault()); @@ -712,7 +877,7 @@ public async Task Map_NestedTopics_ReturnsMappedModel() { var target = await _mappingService.MapAsync(topic).ConfigureAwait(false); - Assert.Equal(2, target?.Categories.Count); + Assert.Equal(2, target?.Categories.Count); Assert.NotNull(GetChildTopic(target?.Categories, "NestedTopic1")); Assert.NotNull(GetChildTopic(target?.Categories, "NestedTopic2")); @@ -742,7 +907,7 @@ public async Task Map_Children_ReturnsMappedModel() { var target = await _mappingService.MapAsync(topic).ConfigureAwait(false); - Assert.Equal(4, target?.Children.Count); + Assert.Equal(4, target?.Children.Count); Assert.NotNull(GetChildTopic(target?.Children, "ChildTopic1")); Assert.NotNull(GetChildTopic(target?.Children, "ChildTopic2")); Assert.NotNull(GetChildTopic(target?.Children, "ChildTopic3")); @@ -776,7 +941,7 @@ public async Task Map_Children_SkipsDisabled() { var target = await _mappingService.MapAsync(topic).ConfigureAwait(false); - Assert.Equal(2, target?.Children.Count); + Assert.Equal(2, target?.Children.Count); Assert.NotNull(GetChildTopic(target?.Children, "ChildTopic1")); Assert.NotNull(GetChildTopic(target?.Children, "ChildTopic2")); Assert.Null(GetChildTopic(target?.Children, "ChildTopic3")); @@ -830,7 +995,7 @@ public async Task Map_MapAs_ReturnsTopicReference() { var target = (MapAsTopicViewModel?)await _mappingService.MapAsync(topic).ConfigureAwait(false); Assert.NotNull(target?.TopicReference); - Assert.Equal(typeof(AscendentTopicViewModel), target?.TopicReference?.GetType()); + Assert.IsType(target?.TopicReference); } @@ -855,7 +1020,7 @@ public async Task Map_MapAs_ReturnsRelationships() { var target = (MapAsTopicViewModel?)await _mappingService.MapAsync(topic).ConfigureAwait(false); Assert.Single(target?.Relationships); - Assert.Equal(typeof(AscendentTopicViewModel), target?.Relationships.FirstOrDefault()?.GetType()); + Assert.IsType(target?.Relationships.FirstOrDefault()); } @@ -969,7 +1134,7 @@ public async Task Map_RecursiveRelationships_ReturnsGraph() { var distantCousinTarget = GetChildTopic(cousinTarget?.Children, "ChildTopic3") as RelationWithChildrenTopicViewModel; //Because Cousins is set to recurse over Children, its children should be set - Assert.Equal(3, cousinTarget?.Children.Count); + Assert.Equal(3, cousinTarget?.Children.Count); //Because Cousins is not set to recurse over Cousins, its cousins should NOT be set (even though there is one cousin) Assert.Empty(cousinTarget?.Cousins); @@ -999,7 +1164,7 @@ public async Task Map_Slideshow_ReturnsDerivedViewModels() { var target = await _mappingService.MapAsync(topic).ConfigureAwait(false); - Assert.Equal(4, target?.ContentItems.Count); + Assert.Equal(4, target?.ContentItems.Count); Assert.NotNull(GetChildTopic(target?.ContentItems, "ChildTopic1")); Assert.NotNull(GetChildTopic(target?.ContentItems, "ChildTopic2")); Assert.NotNull(GetChildTopic(target?.ContentItems, "ChildTopic3")); @@ -1029,7 +1194,7 @@ public async Task Map_TopicEntities_ReturnsTopics() { var target = await _mappingService.MapAsync(topic).ConfigureAwait(false); var relatedTopic3copy = (getRelatedTopic(target, "RelatedTopic3")); - Assert.Equal(3, target?.RelatedTopics.Count); + Assert.Equal(3, target?.RelatedTopics.Count); Assert.NotNull(getRelatedTopic(target, "RelatedTopic1")); Assert.NotNull(getRelatedTopic(target, "RelatedTopic2")); @@ -1056,7 +1221,7 @@ public async Task Map_MetadataLookup_ReturnsLookupItems() { var target = (MetadataLookupTopicViewModel?)await _mappingService.MapAsync(topic).ConfigureAwait(false); - Assert.Equal(5, target?.Categories.Count); + Assert.Equal(5, target?.Categories.Count); } @@ -1078,8 +1243,8 @@ public async Task Map_CachedTopic_ReturnsSameReference() { var mappedTopic = await _mappingService.MapAsync(topic).ConfigureAwait(false); - Assert.Equal(mappedTopic?.FirstItem, mappedTopic?.SecondItem); - Assert.Equal(mappedTopic?.FirstItem?.Parent, mappedTopic?.SecondItem?.Parent); + Assert.Equal(mappedTopic?.FirstItem, mappedTopic?.SecondItem); + Assert.Equal(mappedTopic?.FirstItem?.Parent, mappedTopic?.SecondItem?.Parent); Assert.Null(mappedTopic?.FirstItem?.Reference); Assert.Null(mappedTopic?.SecondItem?.Reference); @@ -1102,8 +1267,8 @@ public async Task Map_CachedTopic_ReturnsProgressiveReference() { var mappedTopic = await _mappingService.MapAsync(topic).ConfigureAwait(false); - Assert.Equal(mappedTopic?.FirstItem, mappedTopic?.SecondItem); - Assert.Equal(mappedTopic?.FirstItem?.Parent, mappedTopic?.SecondItem?.Reference); + Assert.Equal(mappedTopic?.FirstItem, mappedTopic?.SecondItem); + Assert.Equal(mappedTopic?.FirstItem?.Parent, mappedTopic?.SecondItem?.Reference); } @@ -1122,7 +1287,7 @@ public async Task Map_CircularReference_ReturnsCachedParent() { var mappedTopic = await _mappingService.MapAsync(topic).ConfigureAwait(false); - Assert.Equal(mappedTopic, mappedTopic?.Children.First().Parent); + Assert.Equal(mappedTopic, mappedTopic?.Children.First().Parent); } @@ -1146,7 +1311,7 @@ public async Task Map_FilterByCollectionType_ReturnsFilteredCollection() { var specialized = target?.Children.GetByContentType("DescendentSpecialized"); - Assert.Equal(2, specialized?.Count); + Assert.Equal(2, specialized?.Count); Assert.NotNull(GetChildTopic(specialized, "ChildTopic2")); Assert.NotNull(GetChildTopic(specialized, "ChildTopic3")); Assert.Null(GetChildTopic(specialized, "ChildTopic4")); @@ -1189,7 +1354,7 @@ public async Task Map_CompatibleProperties_MapObjectReference() { var target = await _mappingService.MapAsync(topic).ConfigureAwait(false); - Assert.Equal(topic.ModelType, target?.ModelType); + Assert.Equal(topic.ModelType, target?.ModelType); Assert.Single(target?.VersionHistory); } @@ -1261,7 +1426,7 @@ public async Task Map_NullProperty_MapsDefaultValue() { var target = await _mappingService.MapAsync(topic).ConfigureAwait(false); Assert.Equal("Default", target?.DefaultString); - Assert.Equal(10, target?.DefaultInt); + Assert.Equal(10, target?.DefaultInt); Assert.True(target?.DefaultBool); } @@ -1335,7 +1500,7 @@ public async Task Map_FilterByAttribute_ReturnsFilteredCollection() { var target = await _mappingService.MapAsync(topic).ConfigureAwait(false); - Assert.Equal(2, target?.Children.Count); + Assert.Equal(2, target?.Children.Count); } @@ -1376,7 +1541,7 @@ public async Task Map_FilterByContentType_ReturnsFilteredCollection() { var target = await _mappingService.MapAsync(topic).ConfigureAwait(false); - Assert.Equal(2, target?.Children.Count); + Assert.Equal(2, target?.Children.Count); } @@ -1401,7 +1566,7 @@ public async Task Map_FlattenAttribute_ReturnsFlatCollection() { var target = await _mappingService.MapAsync(topic).ConfigureAwait(false); - Assert.Equal(25, target?.Children.Count); + Assert.Equal(25, target?.Children.Count); } @@ -1428,6 +1593,7 @@ public async Task Map_FlattenAttribute_ExcludeTopics() { Assert.Single(target?.Children); } + /*========================================================================================================================== | TEST: MAP: CACHED TOPIC: RETURNS CACHED MODEL \-------------------------------------------------------------------------------------------------------------------------*/ @@ -1445,7 +1611,7 @@ public async Task Map_CachedTopic_ReturnsCachedModel() { var target1 = (FilteredTopicViewModel?)await cachedMappingService.MapAsync(topic).ConfigureAwait(false); var target2 = (FilteredTopicViewModel?)await cachedMappingService.MapAsync(topic).ConfigureAwait(false); - Assert.Equal(target1, target2); + Assert.Equal(target1, target2); } @@ -1463,11 +1629,11 @@ public async Task Map_CachedTopic_ReturnsUniqueReferencePerType() { var topic = new Topic("Test", "Filtered", null, 5); - var target1 = (FilteredTopicViewModel?)await cachedMappingService.MapAsync(topic).ConfigureAwait(false); - var target2 = (FilteredTopicViewModel?)await cachedMappingService.MapAsync(topic).ConfigureAwait(false); + var target1 = await cachedMappingService.MapAsync(topic).ConfigureAwait(false); + var target2 = await cachedMappingService.MapAsync(topic).ConfigureAwait(false); var target3 = (TopicViewModel?)await cachedMappingService.MapAsync(topic).ConfigureAwait(false); - Assert.Equal(target1, target2); + Assert.Equal(target1, target2); Assert.NotEqual(target1, target3); } diff --git a/OnTopic.Tests/TopicQueryingTest.cs b/OnTopic.Tests/TopicQueryingTest.cs index 1b3b65b2..b2492196 100644 --- a/OnTopic.Tests/TopicQueryingTest.cs +++ b/OnTopic.Tests/TopicQueryingTest.cs @@ -3,12 +3,8 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ -using System; -using System.Diagnostics.CodeAnalysis; -using System.Linq; using OnTopic.Collections; using OnTopic.Data.Caching; -using OnTopic.Internal.Diagnostics; using OnTopic.Metadata; using OnTopic.Querying; using OnTopic.Repositories; @@ -78,9 +74,9 @@ public void FindAllByAttribute_ReturnsCorrectTopics() { grandNieceTopic.Attributes.SetValue("Foo", "Bar"); greatGrandChildTopic.Attributes.SetValue("Foo", "Bar"); - Assert.Equal(greatGrandChildTopic, parentTopic.FindAllByAttribute("Foo", "Bar").First()); - Assert.Equal(2, parentTopic.FindAllByAttribute("Foo", "Bar").Count); - Assert.Equal(grandChildTopic, parentTopic.FindAllByAttribute("Foo", "Baz").First()); + Assert.Equal(greatGrandChildTopic, parentTopic.FindAllByAttribute("Foo", "Bar").First()); + Assert.Equal(2, parentTopic.FindAllByAttribute("Foo", "Bar").Count); + Assert.Equal(grandChildTopic, parentTopic.FindAllByAttribute("Foo", "Baz").First()); } @@ -100,7 +96,7 @@ public void FindFirstParent_ReturnsCorrectTopic() { var foundTopic = greatGrandChildTopic.FindFirstParent(t => t.Id is 5); - Assert.Equal(childTopic, foundTopic); + Assert.Equal(childTopic, foundTopic); } @@ -138,7 +134,7 @@ public void GetRootTopic_ReturnsRootTopic() { var rootTopic = greatGrandChildTopic.GetRootTopic(); - Assert.Equal(parentTopic, rootTopic); + Assert.Equal(parentTopic, rootTopic); } @@ -155,7 +151,7 @@ public void GetRootTopic_ReturnsCurrentTopic() { var rootTopic = topic.GetRootTopic(); - Assert.Equal(topic, rootTopic); + Assert.Equal(topic, rootTopic); } @@ -174,7 +170,7 @@ public void GetByUniqueKey_RootKey_ReturnsRootTopic() { var foundTopic = parentTopic.GetByUniqueKey("ParentTopic"); Assert.NotNull(foundTopic); - Assert.Equal(parentTopic, foundTopic); + Assert.Equal(parentTopic, foundTopic); } @@ -195,7 +191,7 @@ public void GetByUniqueKey_ValidKey_ReturnsTopic() { var foundTopic = greatGrandChildTopic1.GetByUniqueKey("ParentTopic:ChildTopic:GrandChildTopic:GreatGrandChildTopic2"); - Assert.Equal(greatGrandChildTopic2, foundTopic); + Assert.Equal(greatGrandChildTopic2, foundTopic); } diff --git a/OnTopic.Tests/TopicReferenceCollectionTest.cs b/OnTopic.Tests/TopicReferenceCollectionTest.cs index 226c0d71..35cc11dc 100644 --- a/OnTopic.Tests/TopicReferenceCollectionTest.cs +++ b/OnTopic.Tests/TopicReferenceCollectionTest.cs @@ -3,12 +3,9 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ -using System; using OnTopic.Associations; using OnTopic.Tests.Entities; using OnTopic.Collections.Specialized; -using System.Collections.ObjectModel; -using System.Diagnostics.CodeAnalysis; using Xunit; namespace OnTopic.Tests { @@ -206,7 +203,7 @@ public void SetValue_ExistingReference_TopicUpdated() { topic.References.SetValue("Reference", reference); topic.References.SetValue("Reference", newReference); - Assert.Equal(newReference, topic.References.GetValue("Reference")); + Assert.Equal(newReference, topic.References.GetValue("Reference")); Assert.Empty(reference.IncomingRelationships.GetValues("Reference")); Assert.Single(newReference.IncomingRelationships.GetValues("Reference")); @@ -240,7 +237,7 @@ public void SetValue_ExistingReference_IncomingRelationshipsUpdates() { Assert.False(topic.References.Contains("Reference")); Assert.Null(topic.References.GetValue("Reference")); - Assert.Equal(0, reference.IncomingRelationships.GetValues("Reference")?.Count); + Assert.Equal(0, reference.IncomingRelationships.GetValues("Reference")?.Count); } @@ -303,7 +300,7 @@ public void GetTopic_ExistingReference_ReturnsTopic() { topic.References.SetValue("Reference", reference); - Assert.Equal(reference, topic.References.GetValue("Reference")); + Assert.Equal(reference, topic.References.GetValue("Reference")); } @@ -347,7 +344,7 @@ public void GetTopic_InheritedReference_ReturnsTopic() { parentTopic.BaseTopic = baseTopic; baseTopic.References.SetValue("Reference", reference); - Assert.Equal(reference, topic.References.GetValue("Reference", true)); + Assert.Equal(reference, topic.References.GetValue("Reference", true)); } @@ -412,7 +409,7 @@ public void Add_TopicReferenceWithBusinessLogic_IsReturned() { topic.References.SetValue("TopicReference", reference); - Assert.Equal(reference, topic.TopicReference); + Assert.Equal(reference, topic.TopicReference); } @@ -439,7 +436,8 @@ public void Add_TopicReferenceWithBusinessLogic_RemovedReference() { | TEST: ADD: TOPIC REFERENCE WITH BUSINESS LOGIC: THROWS EXCEPTION \-------------------------------------------------------------------------------------------------------------------------*/ /// - /// Sets a topic reference on a topic instance with an invalid value; ensures an exception is thrown. + /// Sets a topic reference on a topic instance with an invalid value; ensures an + /// is thrown. /// [Fact] public void Add_TopicReferenceWithBusinessLogic_ThrowsException() { @@ -457,7 +455,8 @@ public void Add_TopicReferenceWithBusinessLogic_ThrowsException() { | TEST: SET: TOPIC REFERENCE WITH BUSINESS LOGIC: THROWS EXCEPTION \-------------------------------------------------------------------------------------------------------------------------*/ /// - /// Sets a topic reference on a topic instance with an invalid value; ensures an exception is thrown. + /// Sets a topic reference on a topic instance with an invalid value; ensures an + /// is thrown. /// [Fact] public void Set_TopicReferenceWithBusinessLogic_ThrowsException() { diff --git a/OnTopic.Tests/TopicRelationshipMultiMapTest.cs b/OnTopic.Tests/TopicRelationshipMultiMapTest.cs index 63534a42..7136a232 100644 --- a/OnTopic.Tests/TopicRelationshipMultiMapTest.cs +++ b/OnTopic.Tests/TopicRelationshipMultiMapTest.cs @@ -3,9 +3,6 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ -using System; -using System.Diagnostics.CodeAnalysis; -using System.Linq; using OnTopic.Associations; using OnTopic.Collections; using OnTopic.Collections.Specialized; @@ -36,7 +33,7 @@ public void SetValue_CreatesRelationship() { parent.Relationships.SetValue("Friends", related); - Assert.Equal(parent.Relationships.GetValues("Friends").First(), related); + Assert.Equal(parent.Relationships.GetValues("Friends").First(), related); } @@ -132,7 +129,7 @@ public void SetValue_CreatesIncomingRelationship() { relationships.SetValue("Friends", related); - Assert.Equal(related.IncomingRelationships.GetValues("Friends").First(), parent); + Assert.Equal(related.IncomingRelationships.GetValues("Friends").First(), parent); } @@ -194,7 +191,7 @@ public void SetValue_UpdatesKeyCount() { relationships.SetValue("Relationship" + i, new Topic("Related" + i, "Page")); } - Assert.Equal(5, relationships.Keys.Count); + Assert.Equal(5, relationships.Keys.Count); Assert.Contains("Relationship3", relationships.Keys); } @@ -222,8 +219,8 @@ public void GetEnumerator_ReturnsKeyValuesPairs() { counter++; } - Assert.Equal(5, readOnlyRelationships.Count); - Assert.Equal(5, counter); + Assert.Equal(5, readOnlyRelationships.Count); + Assert.Equal(5, counter); } @@ -247,7 +244,7 @@ public void Indexer_ReturnsKeyValuesPair() { multiMap.Add(keyValuesPair); Assert.Single(readOnlyTopicMultiMap); - Assert.Equal(topic, readOnlyTopicMultiMap["Relationship"].FirstOrDefault()); + Assert.Equal(topic, readOnlyTopicMultiMap["Relationship"].FirstOrDefault()); } @@ -268,9 +265,9 @@ public void GetAllValues_ReturnsAllTopics() { relationships.SetValue("Relationship" + i, new Topic("Related" + i, "Page")); } - Assert.Equal(5, relationships.Count); + Assert.Equal(5, relationships.Count); Assert.Equal("Related3", relationships.GetValues("Relationship3").First().Key); - Assert.Equal(5, relationships.GetAllValues().Count); + Assert.Equal(5, relationships.GetAllValues().Count); } @@ -291,7 +288,7 @@ public void GetAllValues_ContentTypes_ReturnsAllContentTypes() { relationships.SetValue("Relationship" + i, new Topic("Related" + i, "ContentType" + i)); } - Assert.Equal(5, relationships.Keys.Count); + Assert.Equal(5, relationships.Keys.Count); Assert.Single(relationships.GetAllValues("ContentType3")); } diff --git a/OnTopic.Tests/TopicRepositoryBaseTest.cs b/OnTopic.Tests/TopicRepositoryBaseTest.cs index 176b6fa2..300b9324 100644 --- a/OnTopic.Tests/TopicRepositoryBaseTest.cs +++ b/OnTopic.Tests/TopicRepositoryBaseTest.cs @@ -3,13 +3,8 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ -using System; -using System.Diagnostics.CodeAnalysis; -using System.Linq; -using OnTopic.Attributes; using OnTopic.Collections.Specialized; using OnTopic.Data.Caching; -using OnTopic.Internal.Diagnostics; using OnTopic.Metadata; using OnTopic.Repositories; using OnTopic.TestDoubles; @@ -65,7 +60,7 @@ public void Load_ValidTopicId_ReturnsExpectedTopic() { var topic = _topicRepository.Load(11111); - Assert.Equal(11111, topic?.Id); + Assert.Equal(11111, topic?.Id); } @@ -105,7 +100,7 @@ public void Load_ValidDate_ReturnsTopic() { var topic = _cachedTopicRepository.Load(11111, version); Assert.True(topic?.VersionHistory.Contains(version)); - Assert.Equal(version.AddTicks(-(version.Ticks % TimeSpan.TicksPerSecond)), topic?.LastModified); + Assert.Equal(version.AddTicks(-(version.Ticks % TimeSpan.TicksPerSecond)), topic?.LastModified); } @@ -128,7 +123,7 @@ public void Rollback_Topic_UpdatesLastModified() { } Assert.True(topic?.VersionHistory.Contains(version)); - Assert.Equal(version.AddTicks(-(version.Ticks % TimeSpan.TicksPerSecond)), topic?.LastModified); + Assert.Equal(version.AddTicks(-(version.Ticks % TimeSpan.TicksPerSecond)), topic?.LastModified); } @@ -575,7 +570,7 @@ public void GetContentTypeDescriptors_ReturnsContentTypes() { var contentTypes = _topicRepository.GetContentTypeDescriptors(); - Assert.Equal(15, contentTypes.Count); + Assert.Equal(15, contentTypes.Count); Assert.NotNull(contentTypes.GetValue("ContentTypeDescriptor")); Assert.NotNull(contentTypes.GetValue("Page")); Assert.NotNull(contentTypes.GetValue("LookupListItem")); @@ -599,7 +594,7 @@ public void GetContentTypeDescriptors_WithTopicGraph_ReturnsMergedContentTypes() _topicRepository.SetContentTypeDescriptorsProxy(rootContentType); - Assert.NotEqual(contentTypeCount, contentTypes.Count); + Assert.NotEqual(contentTypeCount, contentTypes.Count); Assert.Contains(newContentType, contentTypes); } @@ -639,7 +634,7 @@ public void GetContentTypeDescriptor_GetNewContentType_ReturnsFromTopicGraph() { var contentType = _topicRepository.GetContentTypeDescriptorProxy(topic); Assert.NotNull(contentType); - Assert.Equal(contentType, newContentType); + Assert.Equal(contentType, newContentType); } @@ -728,7 +723,7 @@ public void Save_ContentTypeDescriptor_UpdatesPermittedContentTypes() { _topicRepository.Save(contentTypesRoot, true); - Assert.NotEqual(initialCount, pageContentType.PermittedContentTypes.Count); + Assert.NotEqual(initialCount, pageContentType.PermittedContentTypes.Count); } @@ -866,10 +861,10 @@ public void Move_AfterSibling_SetCorrectly() { _topicRepository.Move(topic, target, sibling); - Assert.Equal(target, topic.Parent); - Assert.Equal(0, target.Children.IndexOf(sibling)); - Assert.Equal(1, target.Children.IndexOf(topic)); - Assert.Equal(2, target.Children.IndexOf(olderSibling)); + Assert.Equal(target, topic.Parent); + Assert.Equal(0, target.Children.IndexOf(sibling)); + Assert.Equal(1, target.Children.IndexOf(topic)); + Assert.Equal(2, target.Children.IndexOf(olderSibling)); } @@ -895,7 +890,7 @@ public void Move_ContentTypeDescriptor_UpdatesContentTypeCache() { _topicRepository.Move(contactContentType, pageContentType); - Assert.NotEqual(contactContentType?.AttributeDescriptors.Count, contactAttributeCount); + Assert.NotEqual(contactContentType?.AttributeDescriptors.Count, contactAttributeCount); } @@ -924,7 +919,7 @@ public void Save_AttributeDescriptor_UpdatesContentType() { _topicRepository.Save(newAttribute); - Assert.Equal(attributeCount+1, childContentType.AttributeDescriptors.Count); + Assert.Equal(attributeCount+1, childContentType.AttributeDescriptors.Count); } @@ -1000,8 +995,8 @@ public void Load_TopicLoadedEvent_IsRaisedWithVersion() { _cachedTopicRepository.TopicLoaded -= eventHandler; Assert.True(hasFired); - Assert.Equal(topicId, topic?.Id); - Assert.Equal(version, topic?.VersionHistory.LastOrDefault()); + Assert.Equal(topicId, topic?.Id); + Assert.Equal(version, topic?.VersionHistory.LastOrDefault()); void eventHandler(object? sender, TopicLoadEventArgs eventArgs) => hasFired = true; diff --git a/OnTopic.Tests/TopicTest.cs b/OnTopic.Tests/TopicTest.cs index ef1f90b4..23bad445 100644 --- a/OnTopic.Tests/TopicTest.cs +++ b/OnTopic.Tests/TopicTest.cs @@ -3,9 +3,6 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ -using System; -using System.Diagnostics.CodeAnalysis; -using OnTopic.Attributes; using OnTopic.Collections; using OnTopic.Metadata; using OnTopic.Repositories; @@ -128,8 +125,8 @@ public void Parent_SetValue_UpdatesParent() { parentTopic.Id = 5; childTopic.Parent = parentTopic; - Assert.Equal(parentTopic.Children["Child"], childTopic); - Assert.Equal(5, childTopic.Parent.Id); + Assert.Equal(parentTopic.Children["Child"], childTopic); + Assert.Equal(5, childTopic.Parent.Id); } @@ -186,10 +183,10 @@ public void Parent_ChangeValue_UpdatesParent() { Parent = targetParent }; - Assert.Equal(targetParent.Children["ChildTopic"], childTopic); + Assert.Equal(targetParent.Children["ChildTopic"], childTopic); Assert.True(targetParent.Children.Contains("ChildTopic")); Assert.False(sourceParent.Children.Contains("ChildTopic")); - Assert.Equal(10, childTopic.Parent.Id); + Assert.Equal(10, childTopic.Parent.Id); } @@ -273,7 +270,7 @@ public void LastModified_UpdateLastModified_ReturnsExpectedValue() { topic.LastModified = lastModified; - Assert.Equal(lastModified, topic.LastModified); + Assert.Equal(lastModified, topic.LastModified); } @@ -292,7 +289,7 @@ public void LastModified_UpdateVersionHistory_ReturnsExpectedValue() { topic.VersionHistory.Add(lastModified); - Assert.Equal(lastModified, topic.LastModified); + Assert.Equal(lastModified, topic.LastModified); } @@ -311,7 +308,7 @@ public void LastModified_UpdateValue_ReturnsExpectedValue() { topic.Attributes.SetValue("LastModified", lastModified.ToShortDateString()); - Assert.Equal(lastModified, topic.LastModified); + Assert.Equal(lastModified, topic.LastModified); } @@ -334,8 +331,8 @@ public void BaseTopic_UpdateValue_ReturnsExpectedValue() { topic.BaseTopic = secondBaseTopic; topic.BaseTopic = finalBaseTopic; - Assert.Equal(topic.BaseTopic, finalBaseTopic); - Assert.Equal(2, topic.References.GetValue("BaseTopic")?.Id); + Assert.Equal(topic.BaseTopic, finalBaseTopic); + Assert.Equal(2, topic.References.GetValue("BaseTopic")?.Id); } @@ -356,8 +353,8 @@ public void BaseTopic_ResavedValue_ReturnsExpectedValue() { baseTopic.Id = 5; topic.BaseTopic = baseTopic; - Assert.Equal(topic.BaseTopic, baseTopic); - Assert.Equal(5, topic.References.GetValue("BaseTopic")?.Id); + Assert.Equal(topic.BaseTopic, baseTopic); + Assert.Equal(5, topic.References.GetValue("BaseTopic")?.Id); } diff --git a/OnTopic.Tests/TypeAccessorTest.cs b/OnTopic.Tests/TypeAccessorTest.cs new file mode 100644 index 00000000..6653e097 --- /dev/null +++ b/OnTopic.Tests/TypeAccessorTest.cs @@ -0,0 +1,700 @@ +/*============================================================================================================================== +| Author Ignia, LLC +| Client Ignia, LLC +| Project Topics Library +\=============================================================================================================================*/ +using System.ComponentModel; +using System.Reflection; +using OnTopic.Internal.Reflection; +using OnTopic.Metadata; +using OnTopic.Tests.BindingModels; +using OnTopic.Tests.Fixtures; +using OnTopic.Tests.ViewModels; +using OnTopic.Tests.ViewModels.Metadata; +using Xunit; + +namespace OnTopic.Tests { + + /*============================================================================================================================ + | CLASS: TYPE ACCESSOR TESTS + \---------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Provides unit tests for the classes. + /// + [ExcludeFromCodeCoverage] + public class TypeAccessorTest: IClassFixture> { + + /*========================================================================================================================== + | PRIVATE VARIABLES + \-------------------------------------------------------------------------------------------------------------------------*/ + internal readonly TypeAccessor _typeAccessor; + + /*========================================================================================================================== + | CONSTRUCTOR + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Initializes a new instance of the with shared resources. + /// + /// + /// This establishes a shared based on the so that + /// members can be processed once, and then evaluated from a single instance. + /// + public TypeAccessorTest(TypeAccessorFixture fixture) { + + /*------------------------------------------------------------------------------------------------------------------------ + | Validate parameters + \-----------------------------------------------------------------------------------------------------------------------*/ + Contract.Requires(fixture, nameof(fixture)); + + /*------------------------------------------------------------------------------------------------------------------------ + | Establish dependencies + \-----------------------------------------------------------------------------------------------------------------------*/ + _typeAccessor = fixture.TypeAccessor; + + } + + /*========================================================================================================================== + | TEST: GET MEMBERS: MIXED VALIDITY: RETURNS VALID MEMBERS + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Assembles a new from a that contains both valid and invalid members. + /// Ensures that only the valid members are returned via . + /// + [Fact] + public void GetMembers_MixedValidity_ReturnsValidMembers() { + + var members = _typeAccessor.GetMembers(); + + Assert.Equal(7, members.Count); + + } + + /*========================================================================================================================== + | TEST: GET MEMBERS: PROPERTIES: RETURNS PROPERTIES + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Assembles a new from a that contains both methods and properties. + /// Ensures that only the properties are returned when is called with + /// . + /// + [Fact] + public void GetMembers_Properties_ReturnsProperties() { + + var memberAccessors = _typeAccessor.GetMembers(MemberTypes.Property); + + Assert.Equal(5, memberAccessors.Count); + + } + + /*========================================================================================================================== + | TEST: GET MEMBERS: PROPERTY INFO: RETURNS PROPERTIES + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Establishes a and confirms that + /// functions. + /// + [Fact] + public void GetMembers_PropertyInfo_ReturnsProperties() { + + var typeAccessor = TypeAccessorCache.GetTypeAccessor(); + var properties = typeAccessor.GetMembers(); + + Assert.Contains(properties, p => p.Name.Equals(nameof(ContentTypeDescriptor.Key), StringComparison.Ordinal)); + Assert.Contains(properties, p => p.Name.Equals(nameof(ContentTypeDescriptor.AttributeDescriptors), StringComparison.Ordinal)); + Assert.DoesNotContain(properties, p => p.Name.Equals(nameof(ContentTypeDescriptor.IsTypeOf), StringComparison.Ordinal)); + Assert.DoesNotContain(properties, p => p.Name.Equals("InvalidPropertyName", StringComparison.Ordinal)); + + } + + /*========================================================================================================================== + | TEST: GET MEMBER: VALID MEMBER: RETURNS MEMBER ACCESSOR + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Ensures that returns the expected . + /// + [Fact] + public void GetMember_ValidMember_ReturnsMemberAccessor() { + + var memberAccessor = _typeAccessor.GetMember(nameof(MemberAccessorViewModel.NonNullableProperty))!; + + Assert.NotNull(memberAccessor); + Assert.Equal(nameof(MemberAccessorViewModel.NonNullableProperty), memberAccessor.Name); + Assert.False(memberAccessor.IsNullable); + + } + + /*========================================================================================================================== + | TEST: GET MEMBER: INVALID MEMBER: RETURNS NULL + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Ensures that returns the null if an invalid member is requests. + /// + [Fact] + public void GetMember_InvalidMember_ReturnsNull() { + + var memberAccessor = _typeAccessor.GetMember(nameof(MemberAccessorViewModel.InvalidGetMethod)); + + Assert.Null(memberAccessor); + + } + + /*========================================================================================================================== + | TEST: HAS GETTER: NAMES: RETURNS RESULT + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Ensures that returns true if the member is, in fact, gettable. + /// + [Theory] + [InlineData(nameof(MemberAccessorViewModel.GetMethod), true)] + [InlineData(nameof(MemberAccessorViewModel.InvalidGetMethod), false)] + [InlineData(nameof(MemberAccessorViewModel.SetMethod), false)] + [InlineData(nameof(MemberAccessorViewModel.NonNullableProperty), true)] + [InlineData(nameof(MemberAccessorViewModel.ReadOnlyProperty), true)] + [InlineData(nameof(MemberAccessorViewModel.WriteOnlyProperty), false)] + [InlineData("MissingProperty", false)] + public void HasGetter_Names_ReturnsResult(string name, bool result) { + Assert.Equal(result, _typeAccessor.HasGetter(name)); + } + + /*========================================================================================================================== + | TEST: HAS GETTABLE PROPERTY: RETURNS EXPECTED + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Establishes a and confirms that returns the expected value. + /// + [Fact] + public void HasGettableProperty_ReturnsExpected() { + + var typeAccessor1 = TypeAccessorCache.GetTypeAccessor(); + var typeAccessor2 = TypeAccessorCache.GetTypeAccessor(); + + Assert.True(typeAccessor1.HasGettableProperty(nameof(ContentTypeDescriptorTopicBindingModel.Key))); + Assert.False(typeAccessor1.HasGettableProperty(nameof(ContentTypeDescriptorTopicBindingModel.ContentTypes))); + Assert.False(typeAccessor1.HasGettableProperty("MissingProperty")); + Assert.True(typeAccessor2.HasGettableProperty(nameof(TopicReferenceTopicViewModel.TopicReference), typeof(TopicViewModel))); + + } + + /*========================================================================================================================== + | TEST: HAS GETTABLE PROPERTY: WITH ATTRIBUTE: RETURNS EXPECTED + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Establishes a with a required constraint and confirms that returns the expected value. + /// + [Fact] + public void HasGettableProperty_WithAttribute_ReturnsExpected() { + + var typeAccessor = TypeAccessorCache.GetTypeAccessor(); + + Assert.False(typeAccessor.HasGettableProperty(nameof(Topic.Key))); + Assert.True(typeAccessor.HasGettableProperty(nameof(Topic.View))); + + } + + /*========================================================================================================================== + | TEST: HAS GETTABLE METHOD: RETURNS EXPECTED + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Establishes a and confirms that + /// returns the expected value. + /// + [Fact] + public void HasGettableMethod_ReturnsExpected() { + + var typeAccessor = TypeAccessorCache.GetTypeAccessor(); + + Assert.True(typeAccessor.HasGettableMethod("GetMethod")); + Assert.False(typeAccessor.HasGettableMethod("SetMethod")); + Assert.False(typeAccessor.HasGettableMethod("MissingMethod")); + Assert.False(typeAccessor.HasGettableMethod("GetComplexMethod")); + Assert.True(typeAccessor.HasGettableMethod("GetComplexMethod", typeof(TopicViewModel))); + + } + + /*========================================================================================================================== + | TEST: HAS GETTABLE METHOD: WITH ATTRIBUTE: RETURNS EXPECTED + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Establishes a with a required constraint and confirms that returns the expected value. + /// + /// + /// In practice, we haven't encountered a need for this yet and, thus, don't have any semantically relevant attributes to + /// use in this situation. As a result, this example is a bit contrived. + /// + [Fact] + public void HasGettableMethod_WithAttribute_ReturnsExpected() { + + var typeAccessor = TypeAccessorCache.GetTypeAccessor(); + + Assert.True(typeAccessor.HasGettableMethod(nameof(MethodBasedViewModel.GetAnnotatedMethod))); + Assert.False(typeAccessor.HasGettableMethod(nameof(MethodBasedViewModel.GetMethod))); + + } + + /*========================================================================================================================== + | TEST: GET VALUE: NAMES: RETURNS RESULT + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Ensures that returns the expected default value. + /// + [Theory] + [InlineData(nameof(MemberAccessorViewModel.NonNullableProperty), 1)] + [InlineData(nameof(MemberAccessorViewModel.NullableProperty), 2)] + [InlineData(nameof(MemberAccessorViewModel.NonNullableReferenceGetter), typeof(MemberAccessorTest))] + [InlineData(nameof(MemberAccessorViewModel.GetMethod), 3)] + [InlineData(nameof(MemberAccessorViewModel.ReadOnlyProperty), null)] + public void GetValue_Names_ReturnsResult(string name, object result) { + + var sourceObject = new MemberAccessorViewModel() { + NonNullableProperty = 1, + NullableProperty = 2, + NonNullableReferenceGetter = typeof(MemberAccessorTest) + }; + + sourceObject.SetMethod(3); + + Assert.Equal(result, _typeAccessor.GetValue(sourceObject, name)); + } + + /*========================================================================================================================== + | TEST: HAS SETTER: NAMES: RETURNS RESULT + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Ensures that returns true if the member is, in fact, settable. + /// + [Theory] + [InlineData(nameof(MemberAccessorViewModel.SetMethod), true)] + [InlineData(nameof(MemberAccessorViewModel.InvalidSetMethod), false)] + [InlineData(nameof(MemberAccessorViewModel.GetMethod), false)] + [InlineData(nameof(MemberAccessorViewModel.NonNullableProperty), true)] + [InlineData(nameof(MemberAccessorViewModel.WriteOnlyProperty), true)] + [InlineData(nameof(MemberAccessorViewModel.ReadOnlyProperty), false)] + [InlineData("MissingProperty", false)] + public void HasSetter_Names_ReturnsResult(string name, bool result) { + Assert.Equal(result, _typeAccessor.HasSetter(name)); + } + + /*========================================================================================================================== + | TEST: HAS SETTABLE PROPERTY: RETURNS EXPECTED + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Establishes a and confirms that returns the expected value. + /// + [Fact] + public void HasSettableProperty_ReturnsExpected() { + + var typeAccessor1 = TypeAccessorCache.GetTypeAccessor(); + var typeAccessor2 = TypeAccessorCache.GetTypeAccessor(); + + Assert.True(typeAccessor1.HasSettableProperty(nameof(ContentTypeDescriptorTopicBindingModel.Key))); + Assert.False(typeAccessor1.HasSettableProperty(nameof(ContentTypeDescriptorTopicBindingModel.ContentTypes))); + Assert.False(typeAccessor1.HasSettableProperty("MissingProperty")); + Assert.True(typeAccessor2.HasSettableProperty(nameof(TopicReferenceTopicViewModel.TopicReference), typeof(TopicViewModel))); + + } + + /*========================================================================================================================== + | TEST: HAS SETTABLE METHOD: RETURNS EXPECTED + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Establishes a and confirms that + /// returns the expected value. + /// + [Fact] + public void HasSettableMethod_ReturnsExpected() { + + var typeAccessor = TypeAccessorCache.GetTypeAccessor(); + + Assert.True(typeAccessor.HasSettableMethod(nameof(MethodBasedViewModel.SetMethod))); + Assert.False(typeAccessor.HasSettableMethod(nameof(MethodBasedViewModel.GetMethod))); + Assert.False(typeAccessor.HasSettableMethod(nameof(MethodBasedViewModel.SetComplexMethod))); + Assert.False(typeAccessor.HasSettableMethod(nameof(MethodBasedViewModel.SetParametersMethod))); + Assert.False(typeAccessor.HasSettableMethod("MissingMethod")); + + } + + /*========================================================================================================================== + | TEST: SET VALUE: NAMES: SETS RESULTS + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Sets a variety of values on a using and ensures that they are, in fact, set correctly. + /// + [Fact] + public void SetValue_Names_SetsResults() { + + var sourceObject = new MemberAccessorViewModel() { + NullableProperty = 5 + }; + + _typeAccessor.SetValue(sourceObject, nameof(MemberAccessorViewModel.NullableProperty), null); + _typeAccessor.SetValue(sourceObject, nameof(MemberAccessorViewModel.NonNullableProperty), 1); + _typeAccessor.SetValue(sourceObject, nameof(MemberAccessorViewModel.NonNullableReferenceGetter), typeof(MemberAccessorTest)); + _typeAccessor.SetValue(sourceObject, nameof(MemberAccessorViewModel.SetMethod), 5); + //_typeAccessor.SetValue(sourceObject, nameof(MemberAccessorViewModel.WriteOnlyProperty), null); // Edge case is unsupported + + Assert.Null(sourceObject.NullableProperty); + Assert.Equal(1, sourceObject.NonNullableProperty); + Assert.Equal(typeof(MemberAccessorTest), sourceObject.NonNullableReferenceGetter); + Assert.Equal(5, sourceObject.GetMethod()); + + } + + /*========================================================================================================================== + | TEST: SET PROPERTY VALUE: KEY: SETS VALUE + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Establishes a and confirms that a key value can be properly set using the method. + /// + [Fact] + public void SetPropertyValue_Key_SetsValue() { + + var typeAccessor = TypeAccessorCache.GetTypeAccessor(); + var topic = new Topic("Test", "ContentType"); + + typeAccessor.SetPropertyValue(topic, "Key", "NewKey"); + + var key = typeAccessor.GetPropertyValue(topic, "Key")?.ToString(); + + Assert.Equal("NewKey", topic.Key); + Assert.Equal("NewKey", key); + + } + + /*========================================================================================================================== + | TEST: SET PROPERTY VALUE: NULL VALUE: SETS TO NULL + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Establishes a and confirms that the method sets the property to null. + /// + [Fact] + public void SetPropertyValue_NullValue_SetsToNull() { + + var typeAccessor = TypeAccessorCache.GetTypeAccessor(); + var model = new NullablePropertyTopicViewModel() { + NullableInteger = 5 + }; + + typeAccessor.SetPropertyValue(model, "NullableInteger", null); + + Assert.Null(model.NullableInteger); + + } + + /*========================================================================================================================== + | TEST: SET PROPERTY VALUE: EMPTY VALUE: SETS TO NULL + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Establishes a and confirms that the sets the target property value to null if the value is set to . + /// + [Fact] + public void SetPropertyValue_EmptyValue_SetsToNull() { + + var typeAccessor = TypeAccessorCache.GetTypeAccessor(); + var model = new NullablePropertyTopicViewModel() { + NullableInteger = 5 + }; + + typeAccessor.SetPropertyValue(model, "NullableInteger", "", true); + + Assert.Null(model.NullableInteger); + + } + + /*========================================================================================================================== + | TEST: SET PROPERTY VALUE: EMPTY VALUE: SETS DEFAULT + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Establishes a and confirms that the sets the value to its default if the value is set to and the + /// target property type is not nullable. + /// + [Fact] + public void SetPropertyValue_EmptyValue_SetsDefault() { + + var typeAccessor = TypeAccessorCache.GetTypeAccessor(); + var model = new NonNullablePropertyTopicViewModel(); + + typeAccessor.SetPropertyValue(model, "NonNullableInteger", "ABC", true); + + Assert.Equal(0, model.NonNullableInteger); + + } + + /*========================================================================================================================== + | TEST: SET PROPERTY VALUE: BOOLEAN: SETS VALUE + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Establishes a and confirms that a boolean value can be properly set using the method. + /// + [Fact] + public void SetPropertyValue_Boolean_SetsValue() { + + var typeAccessor = TypeAccessorCache.GetTypeAccessor(); + var topic = new Topic("Test", "ContentType"); + + typeAccessor.SetPropertyValue(topic, "IsHidden", "1", true); + + Assert.True(topic.IsHidden); + + } + + /*========================================================================================================================== + | TEST: SET PROPERTY VALUE: DATE/TIME: SETS VALUE + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Establishes a and confirms that a date/time value can be properly set using the method. + /// + [Fact] + public void SetPropertyValue_DateTime_SetsValue() { + + var typeAccessor = TypeAccessorCache.GetTypeAccessor(); + var topic = new Topic("Test", "ContentType"); + + typeAccessor.SetPropertyValue(topic, "LastModified", "June 3, 2008", true); + Assert.Equal(new(2008, 6, 3), topic.LastModified); + + typeAccessor.SetPropertyValue(topic, "LastModified", "2008-06-03", true); + Assert.Equal(new(2008, 6, 3), topic.LastModified); + + typeAccessor.SetPropertyValue(topic, "LastModified", "06/03/2008", true); + Assert.Equal(new(2008, 6, 3), topic.LastModified); + + } + + /*========================================================================================================================== + | TEST: SET PROPERTY VALUE: INVALID PROPERTY: THROWS EXCEPTION + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Establishes a and confirms that an invalid property being set via the method throws an . + /// + [Fact] + public void SetPropertyValue_InvalidProperty_ReturnsFalse() { + + var topic = new MemberAccessorViewModel(); + + Assert.Throws(() => + _typeAccessor.SetPropertyValue(topic, "InvalidProperty", "Invalid") + ); + + } + + /*========================================================================================================================== + | TEST: SET METHOD VALUE: VALID VALUE: SETS VALUE + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Establishes a and confirms that a value can be properly set using the method. + /// + [Fact] + public void SetMethodValue_ValidValue_SetsValue() { + + var source = new MemberAccessorViewModel(); + + _typeAccessor.SetMethodValue(source, nameof(MemberAccessorViewModel.SetMethod), "123", true); + + Assert.Equal(123, source.GetMethod()); + + } + + /*========================================================================================================================== + | TEST: SET METHOD VALUE: INVALID VALUE: DOESN'T SET VALUE + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Establishes a and confirms that a value set with an invalid value using the method returns false. + /// + [Fact] + public void SetMethodValue_InvalidValue_DoesNotSetValue() { + + var typeAccessor = TypeAccessorCache.GetTypeAccessor(); + var source = new MethodBasedViewModel(); + + typeAccessor.SetMethodValue(source, nameof(MethodBasedViewModel.SetMethod), "ABC", true); + + Assert.Equal(0, source.GetMethod()); + + } + + /*========================================================================================================================== + | TEST: SET METHOD VALUE: INVALID MEMBER: THROWS EXCEPTION + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Establishes a and confirms that setting an invalid method name using the method throws an exception. + /// + [Fact] + public void SetMethodValue_InvalidMember_ThrowsException() { + + var source = new MemberAccessorViewModel(); + + Assert.Throws(() => + _typeAccessor.SetMethodValue(source, "BogusMethod", "123") + ); + + } + + /*========================================================================================================================== + | TEST: SET METHOD VALUE: VALID REFENCE VALUE: SETS VALUE + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Establishes a and confirms that a reference value can be properly set using the method. + /// + [Fact] + public void SetMethodValue_ValidReferenceValue_SetsValue() { + + var typeAccessor = TypeAccessorCache.GetTypeAccessor(); + var source = new MethodBasedReferenceViewModel(); + var reference = new TopicViewModel(); + + typeAccessor.SetMethodValue(source, "SetMethod", reference); + + Assert.Equal(reference, source.GetMethod()); + + } + + /*========================================================================================================================== + | TEST: SET METHOD VALUE: INVALID REFERENCE VALUE: THROWS EXCEPTION + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Establishes a and confirms that a value set with an invalid value using the method throws an . + /// + [Fact] + public void SetMethodValue_InvalidReferenceValue_ThrowsException() { + + var typeAccessor = TypeAccessorCache.GetTypeAccessor(); + var source = new MethodBasedReferenceViewModel(); + var reference = new EmptyViewModel(); + + Assert.Throws(() => + typeAccessor.SetMethodValue(source, "SetMethod", reference) + ); + + } + + /*========================================================================================================================== + | TEST: SET METHOD VALUE: INVALID REFERENCE MEMBER: THROWS EXCEPTION + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Establishes a and confirms that setting an invalid method name using the method returns false. + /// + [Fact] + public void SetMethodValue_InvalidReferenceMember_ThrowsException() { + + var typeAccessor = TypeAccessorCache.GetTypeAccessor(); + var source = new MethodBasedViewModel(); + + Assert.Throws(() => + typeAccessor.SetMethodValue(source, "BogusMethod", new object()) + ); + + } + + /*========================================================================================================================== + | TEST: SET METHOD VALUE: NULL REFERENCE VALUE: DOESN'T SET VALUE + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Establishes a and confirms that a value set with an null value using the method returns false. + /// + [Fact] + public void SetMethodValue_NullReferenceValue_DoesNotSetValue() { + + var typeAccessor = TypeAccessorCache.GetTypeAccessor(); + var source = new MethodBasedReferenceViewModel(); + + typeAccessor.SetMethodValue(source, "SetMethod", (object?)null); + + Assert.Null(source.GetMethod()); + + } + + /*========================================================================================================================== + | TEST: MAYBE COMPATIBLE: CORRESPONDING TOPIC: CORRECTLY SET + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Establishes a and based on the and + /// confirms that the underlying instances are correctly marked as based on the association with the . + /// + [Fact] + public void MaybeCompatible_CorrespondingTopic_CorrectlySet() { + + var typeAccessor = TypeAccessorCache.GetTypeAccessor(); + + Assert.Equal(9, typeAccessor.GetMembers(MemberTypes.Property).Count(m => m.MaybeCompatible)); + Assert.Equal(2, typeAccessor.GetMembers(MemberTypes.Property).Count(m => !m.MaybeCompatible)); + Assert.Equal(9, typeAccessor.ConstructorParameters.Count(m => m.MaybeCompatible)); + Assert.Equal(2, typeAccessor.ConstructorParameters.Count(m => !m.MaybeCompatible)); + Assert.True(typeAccessor.GetMember(nameof(CustomTopicTopicViewModel.TopicReference))?.MaybeCompatible); + + } + + /*========================================================================================================================== + | TEST: SET PROPERTY VALUE: REFLECTION PERFORMANCE + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Sets properties via reflection n number of times; quick way of evaluating the relative performance impact of changes. + /// + /// + /// Unit tests should run quickly, so this isn't an optimal place to test performance. As such, the counter should be set + /// to a small number when not being actively tested. Nevertheless, this provides a convenient test harness for quickly + /// evaluating the performance impact of changes or optimizations without setting up a fully performance test. To adjust + /// the number of iterations, simply increment the "totalIterations" variable. + /// + [Fact] + public void SetPropertyValue_ReflectionPerformance() { + + var totalIterations = 1; + var typeAccessor = TypeAccessorCache.GetTypeAccessor(); + var topic = new Topic("Test", "ContentType"); + + int i; + for (i = 0; i < totalIterations; i++) { + typeAccessor.SetPropertyValue(topic, "Key", "Key" + i); + } + + Assert.Equal("Key" + (i-1), topic.Key); + + } + + /*========================================================================================================================== + | TEST: GET TYPE ACCESSOR: VALID TYPE: RETURNS TYPE ACCESSOR + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Calls the static to retrieve a given and ensures it is + /// correctly returned from the cache. + /// + /// + /// While the is separate from the class that this set of tests + /// is focused on, it is closely related, and the currently only + /// necessitates one unit test. As a result, it's being grouped in with the . + /// + [Fact] + public void GetTypeAccessor_ValidType_ReturnsTypeAccessor() { + + var typeAccessor = TypeAccessorCache.GetTypeAccessor(typeof(MemberAccessorViewModel)); + + Assert.NotNull(typeAccessor); + Assert.Equal(typeof(MemberAccessorViewModel), typeAccessor.Type); + + } + + } //Class +} //Namespace \ No newline at end of file diff --git a/OnTopic.Tests/TypeLookupServiceTest.cs b/OnTopic.Tests/TypeLookupServiceTest.cs index ccf38b25..9428e0a8 100644 --- a/OnTopic.Tests/TypeLookupServiceTest.cs +++ b/OnTopic.Tests/TypeLookupServiceTest.cs @@ -3,9 +3,6 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ -using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; using OnTopic.Lookup; using OnTopic.Metadata; using OnTopic.Tests.BindingModels; @@ -43,7 +40,7 @@ public void TypeCollection_Constructor_ContainsUniqueTypes() { }; var typeCollection = new TypeCollection(topics); - Assert.Equal(2, typeCollection.Count); + Assert.Equal(2, typeCollection.Count); Assert.Contains(typeof(CustomTopic), typeCollection); Assert.DoesNotContain(typeof(Topic), typeCollection); @@ -85,7 +82,7 @@ public void StaticLookupService_Lookup_ReturnsFallback() { }; var lookupService = new StaticTypeLookupService(topics); - Assert.Equal(typeof(FallbackViewModel), lookupService.Lookup(nameof(EmptyViewModel), nameof(FallbackViewModel))); + Assert.Equal(typeof(FallbackViewModel), lookupService.Lookup(nameof(EmptyViewModel), nameof(FallbackViewModel))); } @@ -104,7 +101,7 @@ public void StaticLookupService_AddOrReplace_ReturnsExpected() { lookupService.AddOrReplace(typeof(System.Diagnostics.Contracts.Contract)); lookupService.AddOrReplace(typeof(Internal.Diagnostics.Contract)); - Assert.Equal(typeof(Internal.Diagnostics.Contract), lookupService.Lookup(nameof(Internal.Diagnostics.Contract))); + Assert.Equal(typeof(Internal.Diagnostics.Contract), lookupService.Lookup(nameof(Internal.Diagnostics.Contract))); } @@ -157,9 +154,9 @@ public void CompositeTypeLookupService_Lookup_ReturnsFallback() { var lookupService = new CompositeTypeLookupService(lookupService1, lookupService2); - Assert.Equal(typeof(System.Diagnostics.Contracts.Contract), lookupService.Lookup("Contract")); - Assert.Equal(typeof(FallbackViewModel), lookupService.Lookup("Missing", "FallbackViewModel")); - Assert.Equal(typeof(FallbackViewModel), lookupService.Lookup("Missing")?? typeof(FallbackViewModel)); + Assert.Equal(typeof(System.Diagnostics.Contracts.Contract), lookupService.Lookup("Contract")); + Assert.Equal(typeof(FallbackViewModel), lookupService.Lookup("Missing", "FallbackViewModel")); + Assert.Equal(typeof(FallbackViewModel), lookupService.Lookup("Missing")?? typeof(FallbackViewModel)); } @@ -178,8 +175,8 @@ public void DefaultTopicLookupService_Lookup_ReturnsExpected() { }; var lookupService = new DefaultTopicLookupService(topics); - Assert.Equal(typeof(AttributeDescriptor), lookupService.Lookup(nameof(AttributeDescriptor))); - Assert.Equal(typeof(CustomTopic), lookupService.Lookup(nameof(CustomTopic))); + Assert.Equal(typeof(AttributeDescriptor), lookupService.Lookup(nameof(AttributeDescriptor))); + Assert.Equal(typeof(CustomTopic), lookupService.Lookup(nameof(CustomTopic))); Assert.Null(lookupService.Lookup("TextAttributeDescriptor")); } @@ -196,7 +193,7 @@ public void DynamicTopicBindingModelLookupService_Lookup_ReturnsExpected() { var lookupService = new DynamicTopicBindingModelLookupService(); - Assert.Equal(typeof(PageTopicBindingModel), lookupService.Lookup(nameof(PageTopicBindingModel))); + Assert.Equal(typeof(PageTopicBindingModel), lookupService.Lookup(nameof(PageTopicBindingModel))); Assert.Null(lookupService.Lookup("MissingTopicBindingModel")); } diff --git a/OnTopic.Tests/TypeMemberInfoCollectionTest.cs b/OnTopic.Tests/TypeMemberInfoCollectionTest.cs deleted file mode 100644 index a988b97a..00000000 --- a/OnTopic.Tests/TypeMemberInfoCollectionTest.cs +++ /dev/null @@ -1,624 +0,0 @@ -/*============================================================================================================================== -| Author Ignia, LLC -| Client Ignia, LLC -| Project Topics Library -\=============================================================================================================================*/ -using System; -using System.ComponentModel; -using System.Diagnostics.CodeAnalysis; -using System.Reflection; -using OnTopic.Attributes; -using OnTopic.Internal.Reflection; -using OnTopic.Metadata; -using OnTopic.Tests.BindingModels; -using OnTopic.Tests.ViewModels; -using OnTopic.ViewModels; -using Xunit; - -namespace OnTopic.Tests { - - /*============================================================================================================================ - | CLASS: TYPE COLLECTION TESTS - \---------------------------------------------------------------------------------------------------------------------------*/ - /// - /// Provides unit tests for the and classes. - /// - /// - /// These are internal collections and not accessible publicly. - /// - [ExcludeFromCodeCoverage] - public class MemberDispatcherTest { - - /*========================================================================================================================== - | TEST: CONSTRUCTOR: VALID TYPE: IDENTIFIES PROPERTY - \-------------------------------------------------------------------------------------------------------------------------*/ - /// - /// Establishes a based on a type, and confirms that the property collection returns - /// properties of the type. - /// - [Fact] - public void Constructor_ValidType_IdentifiesProperty() { - - var properties = new MemberInfoCollection(typeof(ContentTypeDescriptor)); - - Assert.True(properties.Contains("AttributeDescriptors")); //First class collection property - Assert.False(properties.Contains("InvalidPropertyName")); //Invalid property - - } - - /*========================================================================================================================== - | TEST: CONSTRUCTOR: VALID TYPE: IDENTIFIES DERIVED PROPERTY - \-------------------------------------------------------------------------------------------------------------------------*/ - /// - /// Establishes a based on a type, and confirms that the property collection returns - /// properties derived from the base class. - /// - [Fact] - public void Constructor_ValidType_IdentifiesDerivedProperty() { - - var properties = new MemberInfoCollection(typeof(ContentTypeDescriptor)); - - Assert.True(properties.Contains("Key")); //Inherited string property - - } - - /*========================================================================================================================== - | TEST: CONSTRUCTOR: VALID TYPE: IDENTIFIES METHOD - \-------------------------------------------------------------------------------------------------------------------------*/ - /// - /// Establishes a based on a type, and confirms that the property collection returns - /// methods of the type. - /// - [Fact] - public void Constructor_ValidType_IdentifiesMethod() { - - var properties = new MemberInfoCollection(typeof(ContentTypeDescriptor)); - - Assert.False(properties.Contains("IsTypeOf")); //This is a method, not a property - - } - - /*========================================================================================================================== - | TEST: ADD: DUPLICATE KEY: THROWS EXCEPTION - \-------------------------------------------------------------------------------------------------------------------------*/ - /// - /// Establishes a and confirms that throws an exception if a with a duplicate already exists. - /// functions. - /// - [Fact] - public void Add_DuplicateKey_ThrowsException() => - Assert.Throws(() => - new TypeMemberInfoCollection { - new(typeof(EmptyViewModel)), - new(typeof(EmptyViewModel)) - } - ); - - /*========================================================================================================================== - | TEST: HAS MEMBER: PROPERTY INFO: RETURNS EXPECTED - \-------------------------------------------------------------------------------------------------------------------------*/ - /// - /// Establishes a and confirms that returns true when appropriate. - /// functions. - /// - [Fact] - public void HasMember_PropertyInfo_ReturnsExpected() { - - var typeCollection = new TypeMemberInfoCollection(); - - Assert.True(typeCollection.HasMember(typeof(ContentTypeDescriptorTopicBindingModel), "ContentTypes")); - Assert.False(typeCollection.HasMember(typeof(ContentTypeDescriptorTopicBindingModel), "MissingProperty")); - Assert.False(typeCollection.HasMember(typeof(ContentTypeDescriptorTopicBindingModel), "Attributes")); - - } - - /*========================================================================================================================== - | TEST: HAS GETTABLE PROPERTY: RETURNS EXPECTED - \-------------------------------------------------------------------------------------------------------------------------*/ - /// - /// Establishes a and confirms that returns the expected value. - /// functions. - /// - [Fact] - public void HasGettableProperty_ReturnsExpected() { - - var dispatcher = new MemberDispatcher(); - - Assert.True(dispatcher.HasGettableProperty(typeof(ContentTypeDescriptorTopicBindingModel), "Key")); - Assert.False(dispatcher.HasGettableProperty(typeof(ContentTypeDescriptorTopicBindingModel), "ContentTypes")); - Assert.False(dispatcher.HasGettableProperty(typeof(ContentTypeDescriptorTopicBindingModel), "MissingProperty")); - Assert.True(dispatcher.HasGettableProperty(typeof(TopicReferenceTopicViewModel), "TopicReference", typeof(TopicViewModel))); - - } - - /*========================================================================================================================== - | TEST: HAS GETTABLE PROPERTY: WITH ATTRIBUTE: RETURNS EXPECTED - \-------------------------------------------------------------------------------------------------------------------------*/ - /// - /// Establishes a with a required constraint and confirms that returns the expected value. - /// functions. - /// - [Fact] - public void HasGettableProperty_WithAttribute_ReturnsExpected() { - - var dispatcher = new MemberDispatcher(typeof(AttributeSetterAttribute)); - - Assert.False(dispatcher.HasGettableProperty(typeof(Topic), nameof(Topic.Key))); - Assert.True(dispatcher.HasGettableProperty(typeof(Topic), nameof(Topic.View))); - - } - - /*========================================================================================================================== - | TEST: HAS SETTABLE PROPERTY: RETURNS EXPECTED - \-------------------------------------------------------------------------------------------------------------------------*/ - /// - /// Establishes a and confirms that returns the expected value. - /// functions. - /// - [Fact] - public void HasSettableProperty_ReturnsExpected() { - - var dispatcher = new MemberDispatcher(); - - Assert.True(dispatcher.HasSettableProperty(typeof(ContentTypeDescriptorTopicBindingModel), "Key")); - Assert.False(dispatcher.HasSettableProperty(typeof(ContentTypeDescriptorTopicBindingModel), "ContentTypes")); - Assert.False(dispatcher.HasSettableProperty(typeof(ContentTypeDescriptorTopicBindingModel), "MissingProperty")); - Assert.True(dispatcher.HasSettableProperty(typeof(TopicReferenceTopicViewModel), "TopicReference", typeof(TopicViewModel))); - - } - - /*========================================================================================================================== - | TEST: HAS GETTABLE METHOD: RETURNS EXPECTED - \-------------------------------------------------------------------------------------------------------------------------*/ - /// - /// Establishes a and confirms that returns the expected value. - /// functions. - /// - [Fact] - public void HasGettableMethod_ReturnsExpected() { - - var dispatcher = new MemberDispatcher(); - - Assert.True(dispatcher.HasGettableMethod(typeof(MethodBasedViewModel), "GetMethod")); - Assert.False(dispatcher.HasGettableMethod(typeof(MethodBasedViewModel), "SetMethod")); - Assert.False(dispatcher.HasGettableMethod(typeof(MethodBasedViewModel), "MissingMethod")); - Assert.False(dispatcher.HasGettableMethod(typeof(MethodBasedViewModel), "GetComplexMethod")); - Assert.True(dispatcher.HasGettableMethod(typeof(MethodBasedViewModel), "GetComplexMethod", typeof(TopicViewModel))); - - } - - /*========================================================================================================================== - | TEST: HAS GETTABLE METHOD: WITH ATTRIBUTE: RETURNS EXPECTED - \-------------------------------------------------------------------------------------------------------------------------*/ - /// - /// Establishes a with a required constraint and confirms that returns the expected value. - /// - /// - /// In practice, we haven't encountered a need for this yet and, thus, don't have any semantically relevant attributes to - /// use in this situation. As a result, this example is a bit contrived. - /// - [Fact] - public void HasGettableMethod_WithAttribute_ReturnsExpected() { - - var dispatcher = new MemberDispatcher(typeof(DisplayNameAttribute)); - - Assert.True(dispatcher.HasGettableMethod(typeof(MethodBasedViewModel), nameof(MethodBasedViewModel.GetAnnotatedMethod))); - Assert.False(dispatcher.HasGettableMethod(typeof(MethodBasedViewModel), nameof(MethodBasedViewModel.GetMethod))); - - } - - /*========================================================================================================================== - | TEST: HAS SETTABLE METHOD: RETURNS EXPECTED - \-------------------------------------------------------------------------------------------------------------------------*/ - /// - /// Establishes a and confirms that returns the expected value. - /// functions. - /// - [Fact] - public void HasSettableMethod_ReturnsExpected() { - - var dispatcher = new MemberDispatcher(); - - Assert.True(dispatcher.HasSettableMethod(typeof(MethodBasedViewModel), nameof(MethodBasedViewModel.SetMethod))); - Assert.False(dispatcher.HasSettableMethod(typeof(MethodBasedViewModel), nameof(MethodBasedViewModel.GetMethod))); - Assert.False(dispatcher.HasSettableMethod(typeof(MethodBasedViewModel), nameof(MethodBasedViewModel.SetComplexMethod))); - Assert.False(dispatcher.HasSettableMethod(typeof(MethodBasedViewModel), nameof(MethodBasedViewModel.SetParametersMethod))); - Assert.False(dispatcher.HasSettableMethod(typeof(MethodBasedViewModel), "MissingMethod")); - - } - - /*========================================================================================================================== - | TEST: GET MEMBERS: PROPERTY INFO: RETURNS PROPERTIES - \-------------------------------------------------------------------------------------------------------------------------*/ - /// - /// Establishes a and confirms that - /// functions. - /// - [Fact] - public void GetMembers_PropertyInfo_ReturnsProperties() { - - var types = new MemberDispatcher(); - - var properties = types.GetMembers(typeof(ContentTypeDescriptor)); - - Assert.True(properties.Contains("Key")); - Assert.True(properties.Contains("AttributeDescriptors")); - Assert.False(properties.Contains("IsTypeOf")); - Assert.False(properties.Contains("InvalidPropertyName")); - - } - - /*========================================================================================================================== - | TEST: GET MEMBER: PROPERTY INFO BY KEY: RETURNS VALUE - \-------------------------------------------------------------------------------------------------------------------------*/ - /// - /// Establishes a and confirms that correctly returns the expected properties. - /// - [Fact] - public void GetMember_PropertyInfoByKey_ReturnsValue() { - - var types = new MemberDispatcher(); - - Assert.NotNull(types.GetMember(typeof(ContentTypeDescriptor), "Key")); - Assert.NotNull(types.GetMember(typeof(ContentTypeDescriptor), "AttributeDescriptors")); - Assert.Null(types.GetMember(typeof(ContentTypeDescriptor), "InvalidPropertyName")); - - } - - /*========================================================================================================================== - | TEST: GET MEMBER: METHOD INFO BY KEY: RETURNS VALUE - \-------------------------------------------------------------------------------------------------------------------------*/ - /// - /// Establishes a and confirms that correctly returns the expected methods. - /// - [Fact] - public void GetMember_MethodInfoByKey_ReturnsValue() { - - var types = new MemberDispatcher(); - - Assert.NotNull(types.GetMember(typeof(ContentTypeDescriptor), "GetWebPath")); - Assert.Null(types.GetMember(typeof(ContentTypeDescriptor), "AttributeDescriptors")); - - } - - /*========================================================================================================================== - | TEST: GET MEMBER: GENERIC TYPE MISMATCH: RETURNS NULL - \-------------------------------------------------------------------------------------------------------------------------*/ - /// - /// Establishes a and confirms that does not return values if the types mismatch. - /// - [Fact] - public void GetMember_GenericTypeMismatch_ReturnsNull() { - - var types = new MemberDispatcher(); - - Assert.Null(types.GetMember(typeof(ContentTypeDescriptor), "IsTypeOf")); - Assert.Null(types.GetMember(typeof(ContentTypeDescriptor), "AttributeDescriptors")); - - } - - /*========================================================================================================================== - | TEST: SET PROPERTY VALUE: KEY: SETS VALUE - \-------------------------------------------------------------------------------------------------------------------------*/ - /// - /// Establishes a and confirms that a key value can be properly set using the method. - /// - [Fact] - public void SetPropertyValue_Key_SetsValue() { - - var types = new MemberDispatcher(); - var topic = new Topic("Test", "ContentType"); - - types.SetPropertyValue(topic, "Key", "NewKey"); - - var key = types.GetPropertyValue(topic, "Key", typeof(string))?.ToString(); - - Assert.Equal("NewKey", topic.Key); - Assert.Equal("NewKey", key); - - } - - /*========================================================================================================================== - | TEST: SET PROPERTY VALUE: NULL VALUE: SETS TO NULL - \-------------------------------------------------------------------------------------------------------------------------*/ - /// - /// Establishes a and confirms that the method sets the property to null. - /// - [Fact] - public void SetPropertyValue_NullValue_SetsToNull() { - - var types = new MemberDispatcher(); - var model = new NullablePropertyTopicViewModel() { - NullableInteger = 5 - }; - - types.SetPropertyValue(model, "NullableInteger", null); - - Assert.Null(model.NullableInteger); - - } - - /*========================================================================================================================== - | TEST: SET PROPERTY VALUE: EMPTY VALUE: SETS TO NULL - \-------------------------------------------------------------------------------------------------------------------------*/ - /// - /// Establishes a and confirms that the sets the target property value to null if the value is set to . - /// - [Fact] - public void SetPropertyValue_EmptyValue_SetsToNull() { - - var types = new MemberDispatcher(); - var model = new NullablePropertyTopicViewModel() { - NullableInteger = 5 - }; - - types.SetPropertyValue(model, "NullableInteger", ""); - - Assert.Null(model.NullableInteger); - - } - - /*========================================================================================================================== - | TEST: SET PROPERTY VALUE: EMPTY VALUE: SETS DEFAULT - \-------------------------------------------------------------------------------------------------------------------------*/ - /// - /// Establishes a and confirms that the sets the value to its default if the value is set to and the - /// target property type is not nullable. - /// - [Fact] - public void SetPropertyValue_EmptyValue_SetsDefault() { - - var types = new MemberDispatcher(); - var model = new NonNullablePropertyTopicViewModel(); - - types.SetPropertyValue(model, "NonNullableInteger", "ABC"); - - Assert.Equal(0, model.NonNullableInteger); - - } - - /*========================================================================================================================== - | TEST: SET PROPERTY VALUE: BOOLEAN: SETS VALUE - \-------------------------------------------------------------------------------------------------------------------------*/ - /// - /// Establishes a and confirms that a boolean value can be properly set using the method. - /// - [Fact] - public void SetPropertyValue_Boolean_SetsValue() { - - var types = new MemberDispatcher(); - var topic = new Topic("Test", "ContentType"); - - types.SetPropertyValue(topic, "IsHidden", "1"); - - Assert.True(topic.IsHidden); - - } - - /*========================================================================================================================== - | TEST: SET PROPERTY VALUE: DATE/TIME: SETS VALUE - \-------------------------------------------------------------------------------------------------------------------------*/ - /// - /// Establishes a and confirms that a date/time value can be properly set using the method. - /// - [Fact] - public void SetPropertyValue_DateTime_SetsValue() { - - var types = new MemberDispatcher(); - var topic = new Topic("Test", "ContentType"); - - types.SetPropertyValue(topic, "LastModified", "June 3, 2008"); - Assert.Equal(new(2008, 6, 3), topic.LastModified); - - types.SetPropertyValue(topic, "LastModified", "2008-06-03"); - Assert.Equal(new(2008, 6, 3), topic.LastModified); - - types.SetPropertyValue(topic, "LastModified", "06/03/2008"); - Assert.Equal(new(2008, 6, 3), topic.LastModified); - - } - - /*========================================================================================================================== - | TEST: SET PROPERTY VALUE: INVALID PROPERTY: THROWS EXCEPTION - \-------------------------------------------------------------------------------------------------------------------------*/ - /// - /// Establishes a and confirms that an invalid property being set via the method throws an . - /// - [Fact] - public void SetPropertyValue_InvalidProperty_ReturnsFalse() { - - var types = new MemberDispatcher(); - var topic = new Topic("Test", "ContentType"); - - Assert.Throws(() => - types.SetPropertyValue(topic, "InvalidProperty", "Invalid") - ); - - } - - /*========================================================================================================================== - | TEST: SET METHOD VALUE: VALID VALUE: SETS VALUE - \-------------------------------------------------------------------------------------------------------------------------*/ - /// - /// Establishes a and confirms that a value can be properly set using the method. - /// - [Fact] - public void SetMethodValue_ValidValue_SetsValue() { - - var types = new MemberDispatcher(); - var source = new MethodBasedViewModel(); - - types.SetMethodValue(source, "SetMethod", "123"); - - Assert.Equal(123, source.GetMethod()); - - } - - /*========================================================================================================================== - | TEST: SET METHOD VALUE: INVALID VALUE: DOESN'T SET VALUE - \-------------------------------------------------------------------------------------------------------------------------*/ - /// - /// Establishes a and confirms that a value set with an invalid value using the method returns false. - /// - [Fact] - public void SetMethodValue_InvalidValue_DoesNotSetValue() { - - var types = new MemberDispatcher(); - var source = new MethodBasedViewModel(); - - types.SetMethodValue(source, "SetMethod", "ABC"); - - Assert.Equal(0, source.GetMethod()); - - } - - /*========================================================================================================================== - | TEST: SET METHOD VALUE: INVALID MEMBER: THROWS EXCEPTION - \-------------------------------------------------------------------------------------------------------------------------*/ - /// - /// Establishes a and confirms that setting an invalid method name using the method throws an exception. - /// - [Fact] - public void SetMethodValue_InvalidMember_ThrowsException() { - - var types = new MemberDispatcher(); - var source = new MethodBasedViewModel(); - - Assert.Throws(() => - types.SetMethodValue(source, "BogusMethod", "123") - ); - - } - - /*========================================================================================================================== - | TEST: SET METHOD VALUE: VALID REFENCE VALUE: SETS VALUE - \-------------------------------------------------------------------------------------------------------------------------*/ - /// - /// Establishes a and confirms that a reference value can be properly set using the method. - /// - [Fact] - public void SetMethodValue_ValidReferenceValue_SetsValue() { - - var types = new MemberDispatcher(); - var source = new MethodBasedReferenceViewModel(); - var reference = new TopicViewModel(); - - types.SetMethodValue(source, "SetMethod", reference); - - Assert.Equal(reference, source.GetMethod()); - - } - - /*========================================================================================================================== - | TEST: SET METHOD VALUE: INVALID REFERENCE VALUE: THROWS EXCEPTION - \-------------------------------------------------------------------------------------------------------------------------*/ - /// - /// Establishes a and confirms that a value set with an invalid value using the method throws an . - /// - [Fact] - public void SetMethodValue_InvalidReferenceValue_ThrowsException() { - - var types = new MemberDispatcher(); - var source = new MethodBasedReferenceViewModel(); - var reference = new EmptyViewModel(); - - Assert.Throws(() => - types.SetMethodValue(source, "SetMethod", reference) - ); - - } - - /*========================================================================================================================== - | TEST: SET METHOD VALUE: INVALID REFERENCE MEMBER: THROWS EXCEPTION - \-------------------------------------------------------------------------------------------------------------------------*/ - /// - /// Establishes a and confirms that setting an invalid method name using the method returns false. - /// - [Fact] - public void SetMethodValue_InvalidReferenceMember_ThrowsException() { - - var types = new MemberDispatcher(); - var source = new MethodBasedViewModel(); - - Assert.Throws(() => - types.SetMethodValue(source, "BogusMethod", new object()) - ); - - } - - /*========================================================================================================================== - | TEST: SET METHOD VALUE: NULL REFERENCE VALUE: DOESN'T SET VALUE - \-------------------------------------------------------------------------------------------------------------------------*/ - /// - /// Establishes a and confirms that a value set with an null value using the method returns false. - /// - [Fact] - public void SetMethodValue_NullReferenceValue_DoesNotSetValue() { - - var types = new MemberDispatcher(); - var source = new MethodBasedReferenceViewModel(); - - types.SetMethodValue(source, "SetMethod", (object?)null); - - Assert.Null(source.GetMethod()); - - } - - /*========================================================================================================================== - | TEST: SET PROPERTY VALUE: REFLECTION PERFORMANCE - \-------------------------------------------------------------------------------------------------------------------------*/ - /// - /// Sets properties via reflection n number of times; quick way of evaluating the relative performance impact of changes. - /// - /// - /// Unit tests should run quickly, so this isn't an optimal place to test performance. As such, the counter should be set - /// to a small number when not being actively tested. Nevertheless, this provides a convenient test harness for quickly - /// evaluating the performance impact of changes or optimizations without setting up a fully performance test. To adjust - /// the number of iterations, simply increment the "totalIterations" variable. - /// - [Fact] - public void SetPropertyValue_ReflectionPerformance() { - - var totalIterations = 1; - var types = new MemberDispatcher(); - var topic = new Topic("Test", "ContentType"); - - int i; - for (i = 0; i < totalIterations; i++) { - types.SetPropertyValue(topic, "Key", "Key" + i); - } - - Assert.Equal("Key" + (i-1), topic.Key); - - } - - } //Class -} //Namespace \ No newline at end of file diff --git a/OnTopic.Tests/ViewModels/AmbiguousRelationTopicViewModel.cs b/OnTopic.Tests/ViewModels/AmbiguousRelationTopicViewModel.cs index d3fdbb6b..68c39f6e 100644 --- a/OnTopic.Tests/ViewModels/AmbiguousRelationTopicViewModel.cs +++ b/OnTopic.Tests/ViewModels/AmbiguousRelationTopicViewModel.cs @@ -3,8 +3,6 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ -using System.Collections.ObjectModel; -using OnTopic.Mapping.Annotations; namespace OnTopic.Tests.ViewModels { diff --git a/OnTopic.Tests/ViewModels/AscendentTopicViewModel.cs b/OnTopic.Tests/ViewModels/AscendentTopicViewModel.cs index f215a9a6..364d2030 100644 --- a/OnTopic.Tests/ViewModels/AscendentTopicViewModel.cs +++ b/OnTopic.Tests/ViewModels/AscendentTopicViewModel.cs @@ -3,7 +3,6 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ -using OnTopic.Mapping.Annotations; namespace OnTopic.Tests.ViewModels { diff --git a/OnTopic.Tests/ViewModels/AttributeDictionaryConstructorTopicViewModel.cs b/OnTopic.Tests/ViewModels/AttributeDictionaryConstructorTopicViewModel.cs new file mode 100644 index 00000000..b10b88cf --- /dev/null +++ b/OnTopic.Tests/ViewModels/AttributeDictionaryConstructorTopicViewModel.cs @@ -0,0 +1,46 @@ +/*============================================================================================================================== +| Author Ignia, LLC +| Client Ignia, LLC +| Project Topics Library +\=============================================================================================================================*/ + +namespace OnTopic.Tests.ViewModels { + + /*============================================================================================================================ + | VIEW MODEL: ATTRIBUTE DICTIONARY CONSTRUCTOR + \---------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Provides a strongly-typed data transfer object for testing a constructor with a . + /// + /// + /// This is a sample class intended for test purposes only; it is not designed for use in a production environment. + /// + public record AttributeDictionaryConstructorTopicViewModel: PageTopicViewModel { + + /*========================================================================================================================== + | CONSTRUCTOR + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Initializes a new with an dictionary. + /// + /// An of attribute values. + public AttributeDictionaryConstructorTopicViewModel(AttributeDictionary attributes) : base(attributes) { + Contract.Requires(attributes, nameof(attributes)); + MappedProperty = attributes.GetValue(nameof(MappedProperty)); + } + + /// + /// Initializes a new with no parameters. + /// + public AttributeDictionaryConstructorTopicViewModel() { } + + /*========================================================================================================================== + | PROPERTIES + \-------------------------------------------------------------------------------------------------------------------------*/ + public string? MappedProperty { get; init; } + public string? UnmappedProperty { get; init; } + + + } //Class +} //Namespace \ No newline at end of file diff --git a/OnTopic.Tests/ViewModels/CircularTopicViewModel.cs b/OnTopic.Tests/ViewModels/CircularTopicViewModel.cs index ad767a93..b0ffbaa5 100644 --- a/OnTopic.Tests/ViewModels/CircularTopicViewModel.cs +++ b/OnTopic.Tests/ViewModels/CircularTopicViewModel.cs @@ -3,8 +3,6 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ -using System.Collections.ObjectModel; -using OnTopic.Mapping.Annotations; namespace OnTopic.Tests.ViewModels { diff --git a/OnTopic.Tests/ViewModels/CompatiblePropertyTopicViewModel.cs b/OnTopic.Tests/ViewModels/CompatiblePropertyTopicViewModel.cs index d301008f..9bf36ae8 100644 --- a/OnTopic.Tests/ViewModels/CompatiblePropertyTopicViewModel.cs +++ b/OnTopic.Tests/ViewModels/CompatiblePropertyTopicViewModel.cs @@ -3,9 +3,6 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ -using System; -using System.Collections.ObjectModel; -using System.Diagnostics.CodeAnalysis; using OnTopic.Metadata; namespace OnTopic.Tests.ViewModels { diff --git a/OnTopic.Tests/ViewModels/ConstructedTopicViewModel.cs b/OnTopic.Tests/ViewModels/ConstructedTopicViewModel.cs index 640f61b0..f6371267 100644 --- a/OnTopic.Tests/ViewModels/ConstructedTopicViewModel.cs +++ b/OnTopic.Tests/ViewModels/ConstructedTopicViewModel.cs @@ -3,8 +3,6 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ -using System.Collections.ObjectModel; -using OnTopic.Mapping.Annotations; namespace OnTopic.Tests.ViewModels { diff --git a/OnTopic.Tests/ViewModels/ConvertPropertyViewModel.cs b/OnTopic.Tests/ViewModels/ConvertPropertyViewModel.cs index 6c7ae55f..8f67dddb 100644 --- a/OnTopic.Tests/ViewModels/ConvertPropertyViewModel.cs +++ b/OnTopic.Tests/ViewModels/ConvertPropertyViewModel.cs @@ -4,7 +4,6 @@ | Project Topics Library \=============================================================================================================================*/ using OnTopic.Mapping; -using OnTopic.Mapping.Annotations; namespace OnTopic.Tests.ViewModels { diff --git a/OnTopic.Tests/ViewModels/CustomTopicTopicViewModel.cs b/OnTopic.Tests/ViewModels/CustomTopicTopicViewModel.cs new file mode 100644 index 00000000..1e77b1ec --- /dev/null +++ b/OnTopic.Tests/ViewModels/CustomTopicTopicViewModel.cs @@ -0,0 +1,152 @@ +/*============================================================================================================================== +| Author Ignia, LLC +| Client Ignia, LLC +| Project Topics Library +\=============================================================================================================================*/ +using OnTopic.Internal.Reflection; +using OnTopic.Tests.Entities; + +namespace OnTopic.Tests.ViewModels { + + /*============================================================================================================================ + | VIEW MODEL: CUSTOM TOPIC + \---------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Provides a topic view model which maps to the . + /// + /// + /// + /// This is a sample class intended for test purposes only; it is not designed for use in a production environment. + /// + /// + /// Typically, topics wouldn't end in "Topic" (outside of ). Because does, + /// the corresponding topic view model must be named (with two Topic). + /// Otherwise, when the attempts to match it to its implied content type, it will strip the + /// TopicViewModel, per conventions, and discover that there isn't a match for Custom. This is a peculiarity + /// of the test case, and not something we'd expect in real-life scenarios. + /// + /// + public record CustomTopicTopicViewModel { + + /*========================================================================================================================== + | CONSTRUCTOR + \-------------------------------------------------------------------------------------------------------------------------*/ + public CustomTopicTopicViewModel( + string title, + string webPath, + string? textAttribute, + bool booleanAttribute, + string booleanAsStringAttribute, + int numericAttribute, + string nonNullableAttribute, + DateTime dateTimeAttribute, + Topic? topicReference, + string? unmatchedMember, + DateTime? isHidden + ) { + Title = title; + WebPath = webPath; + TextAttribute = textAttribute; + BooleanAttribute = booleanAttribute; + BooleanAsStringAttribute = booleanAsStringAttribute; + NumericAttribute = numericAttribute; + NonNullableAttribute = nonNullableAttribute; + DateTimeAttribute = dateTimeAttribute; + TopicReference = topicReference; + UnmatchedMember = unmatchedMember; + IsHidden = isHidden; + } + + /*========================================================================================================================== + | TITLE + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Provides a reference to the property. + /// + public string Title { get; init; } + + /*========================================================================================================================== + | WEB PATH + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Provides the root-relative web path of the Topic, based on an assumption that the root topic is bound to the root of + /// the site. + /// + public string WebPath { get; init; } + + /*========================================================================================================================== + | TEXT ATTRIBUTE + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Provides a text property which is intended to be mapped to a text attribute. + /// + public string? TextAttribute { get; init; } + + /*========================================================================================================================== + | BOOLEAN ATTRIBUTE + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Provides a Boolean property which is intended to be mapped to a Boolean attribute. + /// + public bool BooleanAttribute { get; init; } + + /*========================================================================================================================== + | BOOLEAN AS STRING ATTRIBUTE + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Provides a string property which is intended to be mapped to a Boolean attribute. + /// + public string BooleanAsStringAttribute { get; init; } + + /*========================================================================================================================== + | NUMERIC ATTRIBUTE + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Provides a numeric property which is intended to be mapped to a numeric attribute. + /// + public int NumericAttribute { get; init; } + + /*========================================================================================================================== + | NON-NULLABLE ATTRIBUTE + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Provides a property which does not permit null values as a means of ensuring the business logic is enforced when + /// removing values. + /// + public string NonNullableAttribute { get; init; } + + /*========================================================================================================================== + | DATE/TIME ATTRIBUTE + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Provides a date/time property which is intended to be mapped to a date/time attribute. + /// + public DateTime DateTimeAttribute { get; init; } + + /*========================================================================================================================== + | TOPIC REFERENCE + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Provides a topic reference property which is intended to be mapped to a topic reference. + /// + public Topic? TopicReference { get; init; } + + /*========================================================================================================================== + | UNMATCHED MEMBER + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Provides a member that doesn't match any members of either or . + /// + public string? UnmatchedMember { get; init; } + + /*========================================================================================================================== + | MISMATCHED MEMBER + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Provides a member that corresponds to a property on , but with an incompatible type. + /// + public DateTime? IsHidden { get; init; } + + + } //Class +} //Namespace \ No newline at end of file diff --git a/OnTopic.Tests/ViewModels/DefaultPropertyTopicViewModel.cs b/OnTopic.Tests/ViewModels/DefaultPropertyTopicViewModel.cs index 4bc87a1f..4c42ab7d 100644 --- a/OnTopic.Tests/ViewModels/DefaultPropertyTopicViewModel.cs +++ b/OnTopic.Tests/ViewModels/DefaultPropertyTopicViewModel.cs @@ -4,7 +4,6 @@ | Project Topics Library \=============================================================================================================================*/ using System.ComponentModel; -using OnTopic.ViewModels; namespace OnTopic.Tests.ViewModels { diff --git a/OnTopic.Tests/ViewModels/DescendentSpecializedTopicViewModel.cs b/OnTopic.Tests/ViewModels/DescendentSpecializedTopicViewModel.cs index d472d9a3..0bf607ff 100644 --- a/OnTopic.Tests/ViewModels/DescendentSpecializedTopicViewModel.cs +++ b/OnTopic.Tests/ViewModels/DescendentSpecializedTopicViewModel.cs @@ -3,8 +3,6 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ -using System.Collections.Generic; -using OnTopic.Mapping.Annotations; namespace OnTopic.Tests.ViewModels { diff --git a/OnTopic.Tests/ViewModels/DescendentTopicViewModel.cs b/OnTopic.Tests/ViewModels/DescendentTopicViewModel.cs index 0b29920d..58e0dd64 100644 --- a/OnTopic.Tests/ViewModels/DescendentTopicViewModel.cs +++ b/OnTopic.Tests/ViewModels/DescendentTopicViewModel.cs @@ -3,8 +3,6 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ -using OnTopic.Mapping.Annotations; -using OnTopic.ViewModels; namespace OnTopic.Tests.ViewModels { diff --git a/OnTopic.Tests/ViewModels/DisableMappingTopicViewModel.cs b/OnTopic.Tests/ViewModels/DisableMappingTopicViewModel.cs index e1d55f84..90612729 100644 --- a/OnTopic.Tests/ViewModels/DisableMappingTopicViewModel.cs +++ b/OnTopic.Tests/ViewModels/DisableMappingTopicViewModel.cs @@ -3,7 +3,6 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ -using OnTopic.Mapping.Annotations; namespace OnTopic.Tests.ViewModels { diff --git a/OnTopic.Tests/ViewModels/FilteredContentTypeTopicViewModel.cs b/OnTopic.Tests/ViewModels/FilteredContentTypeTopicViewModel.cs index 0c613841..72347cb2 100644 --- a/OnTopic.Tests/ViewModels/FilteredContentTypeTopicViewModel.cs +++ b/OnTopic.Tests/ViewModels/FilteredContentTypeTopicViewModel.cs @@ -3,8 +3,6 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ -using OnTopic.Mapping.Annotations; -using OnTopic.ViewModels; namespace OnTopic.Tests.ViewModels { diff --git a/OnTopic.Tests/ViewModels/FilteredInvalidTopicViewModel.cs b/OnTopic.Tests/ViewModels/FilteredInvalidTopicViewModel.cs index 9a2d2049..970ac750 100644 --- a/OnTopic.Tests/ViewModels/FilteredInvalidTopicViewModel.cs +++ b/OnTopic.Tests/ViewModels/FilteredInvalidTopicViewModel.cs @@ -3,8 +3,6 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ -using OnTopic.Mapping.Annotations; -using OnTopic.ViewModels; namespace OnTopic.Tests.ViewModels { diff --git a/OnTopic.Tests/ViewModels/FilteredTopicViewModel.cs b/OnTopic.Tests/ViewModels/FilteredTopicViewModel.cs index 4b87fc24..ce57e5f5 100644 --- a/OnTopic.Tests/ViewModels/FilteredTopicViewModel.cs +++ b/OnTopic.Tests/ViewModels/FilteredTopicViewModel.cs @@ -3,8 +3,6 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ -using OnTopic.Mapping.Annotations; -using OnTopic.ViewModels; namespace OnTopic.Tests.ViewModels { diff --git a/OnTopic.Tests/ViewModels/FlattenChildrenTopicViewModel.cs b/OnTopic.Tests/ViewModels/FlattenChildrenTopicViewModel.cs index 211606d4..5dbfc6e6 100644 --- a/OnTopic.Tests/ViewModels/FlattenChildrenTopicViewModel.cs +++ b/OnTopic.Tests/ViewModels/FlattenChildrenTopicViewModel.cs @@ -3,8 +3,6 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ -using System.Collections.ObjectModel; -using OnTopic.Mapping.Annotations; namespace OnTopic.Tests.ViewModels { diff --git a/OnTopic.Tests/ViewModels/InheritedPropertyTopicViewModel.cs b/OnTopic.Tests/ViewModels/InheritedPropertyTopicViewModel.cs index 3d38fc19..7730bb43 100644 --- a/OnTopic.Tests/ViewModels/InheritedPropertyTopicViewModel.cs +++ b/OnTopic.Tests/ViewModels/InheritedPropertyTopicViewModel.cs @@ -3,7 +3,6 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ -using OnTopic.Mapping.Annotations; namespace OnTopic.Tests.ViewModels { diff --git a/OnTopic.Tests/ViewModels/InitializedTopicViewModel.cs b/OnTopic.Tests/ViewModels/InitializedTopicViewModel.cs index 2a557e42..14f23ea0 100644 --- a/OnTopic.Tests/ViewModels/InitializedTopicViewModel.cs +++ b/OnTopic.Tests/ViewModels/InitializedTopicViewModel.cs @@ -4,7 +4,6 @@ | Project Topics Library \=============================================================================================================================*/ using System.ComponentModel; -using OnTopic.ViewModels; namespace OnTopic.Tests.ViewModels { diff --git a/OnTopic.Tests/ViewModels/LoadTestingViewModel.cs b/OnTopic.Tests/ViewModels/LoadTestingViewModel.cs new file mode 100644 index 00000000..59c1f226 --- /dev/null +++ b/OnTopic.Tests/ViewModels/LoadTestingViewModel.cs @@ -0,0 +1,85 @@ +/*============================================================================================================================== +| Author Ignia, LLC +| Client Ignia, LLC +| Project Topics Library +\=============================================================================================================================*/ +using OnTopic.Mapping; + +namespace OnTopic.Tests.ViewModels { + + /*============================================================================================================================ + | VIEW MODEL: LOAD TESTING + \---------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Provides a simple view model with a series of properties that can be used for load testing the . + /// + /// + /// This is a sample class intended for test purposes only; it is not designed for use in a production environment. + /// + public class LoadTestingViewModel: KeyOnlyTopicViewModel { + + /*========================================================================================================================== + | CONSTRUCTOR + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Initializes a new with an dictionary. + /// + /// An of attribute values. + public LoadTestingViewModel(AttributeDictionary attributes) { + Contract.Requires(attributes); + Property0 = attributes.GetInteger("Property0"); + Property1 = attributes.GetInteger("Property1"); + Property2 = attributes.GetInteger("Property2"); + Property3 = attributes.GetInteger("Property3"); + Property4 = attributes.GetInteger("Property4"); + Property5 = attributes.GetInteger("Property5"); + Property6 = attributes.GetInteger("Property6"); + Property7 = attributes.GetInteger("Property7"); + Property8 = attributes.GetInteger("Property8"); + Property9 = attributes.GetInteger("Property9"); + Property10 = attributes.GetInteger("Property10"); + Property11 = attributes.GetInteger("Property11"); + Property12 = attributes.GetInteger("Property12"); + Property13 = attributes.GetInteger("Property13"); + Property14 = attributes.GetInteger("Property14"); + Property15 = attributes.GetInteger("Property15"); + Property16 = attributes.GetInteger("Property16"); + Property17 = attributes.GetInteger("Property17"); + Property18 = attributes.GetInteger("Property18"); + Property19 = attributes.GetInteger("Property19"); + Property20 = attributes.GetInteger("Property20"); + } + + /// + /// Initializes a new with no parameters. + /// + public LoadTestingViewModel() { } + + /*========================================================================================================================== + | PROPERTIES + \-------------------------------------------------------------------------------------------------------------------------*/ + public int? Property0 { get; init; } + public int? Property1 { get; init; } + public int? Property2 { get; init; } + public int? Property3 { get; init; } + public int? Property4 { get; init; } + public int? Property5 { get; init; } + public int? Property6 { get; init; } + public int? Property7 { get; init; } + public int? Property8 { get; init; } + public int? Property9 { get; init; } + public int? Property10 { get; init; } + public int? Property11 { get; init; } + public int? Property12 { get; init; } + public int? Property13 { get; init; } + public int? Property14 { get; init; } + public int? Property15 { get; init; } + public int? Property16 { get; init; } + public int? Property17 { get; init; } + public int? Property18 { get; init; } + public int? Property19 { get; init; } + public int? Property20 { get; init; } + + } //Class +} //Namespace \ No newline at end of file diff --git a/OnTopic.Tests/ViewModels/MapAsTopicViewModel.cs b/OnTopic.Tests/ViewModels/MapAsTopicViewModel.cs index 2871ccd9..5a677a7a 100644 --- a/OnTopic.Tests/ViewModels/MapAsTopicViewModel.cs +++ b/OnTopic.Tests/ViewModels/MapAsTopicViewModel.cs @@ -3,8 +3,6 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ -using System.Collections.ObjectModel; -using OnTopic.Mapping.Annotations; namespace OnTopic.Tests.ViewModels { diff --git a/OnTopic.Tests/ViewModels/MapToParentTopicViewModel.cs b/OnTopic.Tests/ViewModels/MapToParentTopicViewModel.cs index f14db390..ba024085 100644 --- a/OnTopic.Tests/ViewModels/MapToParentTopicViewModel.cs +++ b/OnTopic.Tests/ViewModels/MapToParentTopicViewModel.cs @@ -3,7 +3,6 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ -using OnTopic.Mapping.Annotations; namespace OnTopic.Tests.ViewModels { diff --git a/OnTopic.Tests/ViewModels/MemberAccessorViewModel.cs b/OnTopic.Tests/ViewModels/MemberAccessorViewModel.cs new file mode 100644 index 00000000..29dcc35c --- /dev/null +++ b/OnTopic.Tests/ViewModels/MemberAccessorViewModel.cs @@ -0,0 +1,43 @@ +/*============================================================================================================================== +| Author Ignia, LLC +| Client Ignia, LLC +| Project Topics Library +\=============================================================================================================================*/ + +#pragma warning disable CA1024 // Use properties where appropriate +#pragma warning disable CA1044 // Properties should not be write only +#pragma warning disable CA1822 // Mark members as static + +namespace OnTopic.Tests.ViewModels { + + /*============================================================================================================================ + | VIEW MODEL: MEMBER ACCESSOR + \---------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Provides a strongly-typed data transfer object for testing settable methods and gettable methods. + /// + /// + /// This is a sample class intended for test purposes only; it is not designed for use in a production environment. + /// + [ExcludeFromCodeCoverage] + public class MemberAccessorViewModel { + + private int? _methodValue; + + public MemberAccessorViewModel() { } + public int? NullableProperty { get; set; } + public int NonNullableProperty { get; set; } + public Type NonNullableReferenceGetter { get; set; } = typeof(MemberAccessorViewModel); + public int? ReadOnlyProperty { get; } + public int WriteOnlyProperty { set { } } + public int? GetMethod() => _methodValue; + public int InvalidGetMethod(int value) => value; + public void SetMethod(int? value) => _methodValue = value; + public void InvalidSetMethod(int value, int newValue) => _ = value == newValue; + + } //Class +} //Namespace + +#pragma warning restore CA1024 // Use properties where appropriate +#pragma warning restore CA1822 // Mark members as static +#pragma warning restore CA1044 // Properties should not be write only \ No newline at end of file diff --git a/OnTopic.Tests/ViewModels/Metadata/ContentTypeDescriptorTopicViewModel.cs b/OnTopic.Tests/ViewModels/Metadata/ContentTypeDescriptorTopicViewModel.cs index 9e6b9686..3734a05d 100644 --- a/OnTopic.Tests/ViewModels/Metadata/ContentTypeDescriptorTopicViewModel.cs +++ b/OnTopic.Tests/ViewModels/Metadata/ContentTypeDescriptorTopicViewModel.cs @@ -3,8 +3,6 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ -using System.Collections.ObjectModel; -using OnTopic.Mapping.Annotations; namespace OnTopic.Tests.ViewModels.Metadata { diff --git a/OnTopic.Tests/ViewModels/MetadataLookupTopicViewModel.cs b/OnTopic.Tests/ViewModels/MetadataLookupTopicViewModel.cs index cce94c39..77c41b9b 100644 --- a/OnTopic.Tests/ViewModels/MetadataLookupTopicViewModel.cs +++ b/OnTopic.Tests/ViewModels/MetadataLookupTopicViewModel.cs @@ -3,8 +3,6 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ -using OnTopic.Mapping.Annotations; -using OnTopic.ViewModels; namespace OnTopic.Tests.ViewModels { diff --git a/OnTopic.Tests/ViewModels/MethodBasedReferenceViewModel.cs b/OnTopic.Tests/ViewModels/MethodBasedReferenceViewModel.cs index 94190c76..486f7472 100644 --- a/OnTopic.Tests/ViewModels/MethodBasedReferenceViewModel.cs +++ b/OnTopic.Tests/ViewModels/MethodBasedReferenceViewModel.cs @@ -3,7 +3,6 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ -using OnTopic.ViewModels; namespace OnTopic.Tests.ViewModels { diff --git a/OnTopic.Tests/ViewModels/MethodBasedViewModel.cs b/OnTopic.Tests/ViewModels/MethodBasedViewModel.cs index e58640a1..c8e15ecb 100644 --- a/OnTopic.Tests/ViewModels/MethodBasedViewModel.cs +++ b/OnTopic.Tests/ViewModels/MethodBasedViewModel.cs @@ -4,8 +4,6 @@ | Project Topics Library \=============================================================================================================================*/ using System.ComponentModel; -using System.Diagnostics.CodeAnalysis; -using OnTopic.ViewModels; #pragma warning disable CA1822 // Mark members as static #pragma warning disable CA1024 // Use properties where appropriate diff --git a/OnTopic.Tests/ViewModels/MinimumLengthPropertyTopicViewModel.cs b/OnTopic.Tests/ViewModels/MinimumLengthPropertyTopicViewModel.cs index 7009b478..2d6bcdb2 100644 --- a/OnTopic.Tests/ViewModels/MinimumLengthPropertyTopicViewModel.cs +++ b/OnTopic.Tests/ViewModels/MinimumLengthPropertyTopicViewModel.cs @@ -4,7 +4,6 @@ | Project Topics Library \=============================================================================================================================*/ using System.ComponentModel.DataAnnotations; -using OnTopic.ViewModels; namespace OnTopic.Tests.ViewModels { diff --git a/OnTopic.Tests/ViewModels/NestedTopicViewModel.cs b/OnTopic.Tests/ViewModels/NestedTopicViewModel.cs index 849015da..2181b738 100644 --- a/OnTopic.Tests/ViewModels/NestedTopicViewModel.cs +++ b/OnTopic.Tests/ViewModels/NestedTopicViewModel.cs @@ -3,7 +3,6 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ -using System.Collections.ObjectModel; namespace OnTopic.Tests.ViewModels { diff --git a/OnTopic.Tests/ViewModels/NonNullablePropertyTopicViewModel.cs b/OnTopic.Tests/ViewModels/NonNullablePropertyTopicViewModel.cs index 0b7bff67..0333631a 100644 --- a/OnTopic.Tests/ViewModels/NonNullablePropertyTopicViewModel.cs +++ b/OnTopic.Tests/ViewModels/NonNullablePropertyTopicViewModel.cs @@ -3,7 +3,6 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ -using System; namespace OnTopic.Tests.ViewModels { diff --git a/OnTopic.Tests/ViewModels/NullCollectionTopicViewModel.cs b/OnTopic.Tests/ViewModels/NullCollectionTopicViewModel.cs index d06cfc60..d1f15901 100644 --- a/OnTopic.Tests/ViewModels/NullCollectionTopicViewModel.cs +++ b/OnTopic.Tests/ViewModels/NullCollectionTopicViewModel.cs @@ -3,13 +3,8 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ -using System; -using System.Collections.Generic; -using System.Collections.ObjectModel; using OnTopic.Mapping; -using OnTopic.Mapping.Annotations; using OnTopic.Tests.BindingModels; -using OnTopic.ViewModels; #pragma warning disable CA2227 // Collection properties should be read only #pragma warning disable CA1034 // Nested types should not be visible diff --git a/OnTopic.Tests/ViewModels/NullablePropertyTopicViewModel.cs b/OnTopic.Tests/ViewModels/NullablePropertyTopicViewModel.cs index 43158fe6..745e38ee 100644 --- a/OnTopic.Tests/ViewModels/NullablePropertyTopicViewModel.cs +++ b/OnTopic.Tests/ViewModels/NullablePropertyTopicViewModel.cs @@ -3,7 +3,6 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ -using System; namespace OnTopic.Tests.ViewModels { diff --git a/OnTopic.Tests/ViewModels/ProgressiveTopicViewModel.cs b/OnTopic.Tests/ViewModels/ProgressiveTopicViewModel.cs index adaa3b24..98036bf3 100644 --- a/OnTopic.Tests/ViewModels/ProgressiveTopicViewModel.cs +++ b/OnTopic.Tests/ViewModels/ProgressiveTopicViewModel.cs @@ -3,7 +3,6 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ -using OnTopic.Mapping.Annotations; namespace OnTopic.Tests.ViewModels { diff --git a/OnTopic.Tests/ViewModels/PropertyAliasTopicViewModel.cs b/OnTopic.Tests/ViewModels/PropertyAliasTopicViewModel.cs index cd452a78..c927e8ab 100644 --- a/OnTopic.Tests/ViewModels/PropertyAliasTopicViewModel.cs +++ b/OnTopic.Tests/ViewModels/PropertyAliasTopicViewModel.cs @@ -3,7 +3,6 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ -using OnTopic.Mapping.Annotations; namespace OnTopic.Tests.ViewModels { diff --git a/OnTopic.Tests/ViewModels/RedundantTopicViewModel.cs b/OnTopic.Tests/ViewModels/RedundantTopicViewModel.cs index 46ecdf04..33469337 100644 --- a/OnTopic.Tests/ViewModels/RedundantTopicViewModel.cs +++ b/OnTopic.Tests/ViewModels/RedundantTopicViewModel.cs @@ -3,8 +3,6 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ -using System.Collections.ObjectModel; -using OnTopic.Mapping.Annotations; namespace OnTopic.Tests.ViewModels { diff --git a/OnTopic.Tests/ViewModels/RelationTopicViewModel.cs b/OnTopic.Tests/ViewModels/RelationTopicViewModel.cs index 88c00a9f..8836825e 100644 --- a/OnTopic.Tests/ViewModels/RelationTopicViewModel.cs +++ b/OnTopic.Tests/ViewModels/RelationTopicViewModel.cs @@ -3,8 +3,6 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ -using System.Collections.ObjectModel; -using OnTopic.Mapping.Annotations; namespace OnTopic.Tests.ViewModels { diff --git a/OnTopic.Tests/ViewModels/RelationWithChildrenTopicViewModel.cs b/OnTopic.Tests/ViewModels/RelationWithChildrenTopicViewModel.cs index c71d59ae..f97076b4 100644 --- a/OnTopic.Tests/ViewModels/RelationWithChildrenTopicViewModel.cs +++ b/OnTopic.Tests/ViewModels/RelationWithChildrenTopicViewModel.cs @@ -3,8 +3,6 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ -using System.Collections.ObjectModel; -using OnTopic.Mapping.Annotations; namespace OnTopic.Tests.ViewModels { diff --git a/OnTopic.Tests/ViewModels/TopicAssociationsTopicViewModel.cs b/OnTopic.Tests/ViewModels/TopicAssociationsTopicViewModel.cs index 51226b16..91fd72e0 100644 --- a/OnTopic.Tests/ViewModels/TopicAssociationsTopicViewModel.cs +++ b/OnTopic.Tests/ViewModels/TopicAssociationsTopicViewModel.cs @@ -3,8 +3,6 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ -using OnTopic.Mapping.Annotations; -using OnTopic.ViewModels; namespace OnTopic.Tests.ViewModels { diff --git a/OnTopic.Tests/ViewModels/TopicReferenceTopicViewModel.cs b/OnTopic.Tests/ViewModels/TopicReferenceTopicViewModel.cs index ea0f4c28..49f66c16 100644 --- a/OnTopic.Tests/ViewModels/TopicReferenceTopicViewModel.cs +++ b/OnTopic.Tests/ViewModels/TopicReferenceTopicViewModel.cs @@ -3,7 +3,6 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ -using OnTopic.ViewModels; namespace OnTopic.Tests.ViewModels { diff --git a/OnTopic.ViewModels/BindingModels/AssociatedTopicBindingModel.cs b/OnTopic.ViewModels/BindingModels/AssociatedTopicBindingModel.cs index 2775f732..d9905ffa 100644 --- a/OnTopic.ViewModels/BindingModels/AssociatedTopicBindingModel.cs +++ b/OnTopic.ViewModels/BindingModels/AssociatedTopicBindingModel.cs @@ -3,9 +3,7 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ -using System.ComponentModel.DataAnnotations; using OnTopic.Mapping.Reverse; -using OnTopic.Models; namespace OnTopic.ViewModels.BindingModels { diff --git a/OnTopic.ViewModels/BindingModels/RelatedTopicBindingModel.cs b/OnTopic.ViewModels/BindingModels/RelatedTopicBindingModel.cs index 0470b6c2..224de9f7 100644 --- a/OnTopic.ViewModels/BindingModels/RelatedTopicBindingModel.cs +++ b/OnTopic.ViewModels/BindingModels/RelatedTopicBindingModel.cs @@ -3,10 +3,7 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ -using System; -using System.Diagnostics.CodeAnalysis; using OnTopic.Mapping.Reverse; -using OnTopic.Models; namespace OnTopic.ViewModels.BindingModels { diff --git a/OnTopic.ViewModels/NavigationTopicViewModel.cs b/OnTopic.ViewModels/NavigationTopicViewModel.cs index b0c2640c..a5fbb718 100644 --- a/OnTopic.ViewModels/NavigationTopicViewModel.cs +++ b/OnTopic.ViewModels/NavigationTopicViewModel.cs @@ -3,10 +3,7 @@ | Client Ignia | Project Website \=============================================================================================================================*/ -using System; using System.Collections.ObjectModel; -using System.ComponentModel.DataAnnotations; -using OnTopic.Models; namespace OnTopic.ViewModels { @@ -49,7 +46,7 @@ public sealed record NavigationTopicViewModel : INavigationTopicViewModel + /// [Required] public string WebPath { get; init; } = default!; diff --git a/OnTopic.ViewModels/OnTopic.ViewModels.csproj b/OnTopic.ViewModels/OnTopic.ViewModels.csproj index b870435d..539d2bb3 100644 --- a/OnTopic.ViewModels/OnTopic.ViewModels.csproj +++ b/OnTopic.ViewModels/OnTopic.ViewModels.csproj @@ -14,15 +14,15 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/OnTopic.ViewModels/Properties/AssemblyInfo.cs b/OnTopic.ViewModels/Properties/AssemblyInfo.cs index 97e2bcdd..060c4086 100644 --- a/OnTopic.ViewModels/Properties/AssemblyInfo.cs +++ b/OnTopic.ViewModels/Properties/AssemblyInfo.cs @@ -3,7 +3,19 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ -using System; + +/*============================================================================================================================== +| USING DIRECTIVES (GLOBAL) +\-----------------------------------------------------------------------------------------------------------------------------*/ +global using System.ComponentModel.DataAnnotations; +global using System.Diagnostics.CodeAnalysis; +global using OnTopic.Attributes; +global using OnTopic.Internal.Diagnostics; +global using OnTopic.Models; + +/*============================================================================================================================== +| USING DIRECTIVES (LOCAL) +\-----------------------------------------------------------------------------------------------------------------------------*/ using System.Runtime.InteropServices; /*============================================================================================================================== diff --git a/OnTopic.ViewModels/TopicViewModel.cs b/OnTopic.ViewModels/TopicViewModel.cs index ca78be04..edf7a5e6 100644 --- a/OnTopic.ViewModels/TopicViewModel.cs +++ b/OnTopic.ViewModels/TopicViewModel.cs @@ -3,11 +3,7 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ -using System; -using System.ComponentModel.DataAnnotations; -using System.Diagnostics.CodeAnalysis; using OnTopic.Mapping.Annotations; -using OnTopic.Models; namespace OnTopic.ViewModels { @@ -24,6 +20,24 @@ namespace OnTopic.ViewModels { /// public record TopicViewModel: ITopicViewModel, ICoreTopicViewModel, IAssociatedTopicBindingModel, ITopicBindingModel { + /*========================================================================================================================== + | CONSTRUCTOR + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Initializes a new with an dictionary. + /// + /// An of attribute values. + public TopicViewModel(AttributeDictionary attributes) { + Contract.Requires(attributes, nameof(attributes)); + IsHidden = attributes.GetBoolean("IsHidden")?? IsHidden; + View = attributes.GetValue("View"); + } + + /// + /// Initializes a new with no parameters. + /// + public TopicViewModel() { } + /*========================================================================================================================== | ID \-------------------------------------------------------------------------------------------------------------------------*/ @@ -76,7 +90,7 @@ public record TopicViewModel: ITopicViewModel, ICoreTopicViewModel, IAssociatedT \-------------------------------------------------------------------------------------------------------------------------*/ /// [ExcludeFromCodeCoverage] - [Obsolete("The IsHidden property is no longer supported by TopicViewModel.", true)] + [Obsolete($"The {nameof(IsHidden)} property is no longer supported by {nameof(TopicViewModel)}.", true)] [DisableMapping] public bool IsHidden { get; init; } diff --git a/OnTopic.ViewModels/TopicViewModelLookupService.cs b/OnTopic.ViewModels/TopicViewModelLookupService.cs index 2e218905..c30b6714 100644 --- a/OnTopic.ViewModels/TopicViewModelLookupService.cs +++ b/OnTopic.ViewModels/TopicViewModelLookupService.cs @@ -3,8 +3,6 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ -using System; -using System.Collections.Generic; using System.Reflection; using OnTopic.Lookup; @@ -50,6 +48,7 @@ public TopicViewModelLookupService(IEnumerable? types = null) : base(types \-----------------------------------------------------------------------------------------------------------------------*/ TryAdd(typeof(ItemTopicViewModel)); TryAdd(typeof(ContentItemTopicViewModel)); + TryAdd(typeof(CacheProfileTopicViewModel)); TryAdd(typeof(LookupListItemTopicViewModel)); TryAdd(typeof(SlideTopicViewModel)); @@ -58,9 +57,7 @@ public TopicViewModelLookupService(IEnumerable? types = null) : base(types >------------------------------------------------------------------------------------------------------------------------- | These will be removed in the next major version of OnTopic. \-----------------------------------------------------------------------------------------------------------------------*/ -#pragma warning disable CS0618 // Type or member is obsolete TryAdd(typeof(ListTopicViewModel)); - #pragma warning restore CS0618 // Type or member is obsolete /*------------------------------------------------------------------------------------------------------------------------ | Add support types diff --git a/OnTopic.ViewModels/_collections/TopicViewModelCollection{TItem}.cs b/OnTopic.ViewModels/_collections/TopicViewModelCollection{TItem}.cs index 061d5e96..e95d9bb9 100644 --- a/OnTopic.ViewModels/_collections/TopicViewModelCollection{TItem}.cs +++ b/OnTopic.ViewModels/_collections/TopicViewModelCollection{TItem}.cs @@ -3,13 +3,8 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ -using System; -using System.Collections.Generic; using System.Collections.ObjectModel; -using System.Diagnostics.CodeAnalysis; -using System.Linq; using OnTopic.Internal.Diagnostics; -using OnTopic.Models; namespace OnTopic.ViewModels { diff --git a/OnTopic.ViewModels/_contentTypes/ContentListTopicViewModel.cs b/OnTopic.ViewModels/_contentTypes/ContentListTopicViewModel.cs index 89d8112e..f9102c81 100644 --- a/OnTopic.ViewModels/_contentTypes/ContentListTopicViewModel.cs +++ b/OnTopic.ViewModels/_contentTypes/ContentListTopicViewModel.cs @@ -19,6 +19,24 @@ namespace OnTopic.ViewModels { /// public record ContentListTopicViewModel: PageTopicViewModel { + /*========================================================================================================================== + | CONSTRUCTOR + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Initializes a new with an dictionary. + /// + /// An of attribute values. + public ContentListTopicViewModel(AttributeDictionary attributes): base(attributes) { + Contract.Requires(attributes, nameof(attributes)); + IsIndexed = attributes.GetBoolean(nameof(IsIndexed))?? IsIndexed; + IndexLabel = attributes.GetValue(nameof(IndexLabel))?? IndexLabel; + } + + /// + /// Initializes a new with no parameters. + /// + public ContentListTopicViewModel() { } + /*========================================================================================================================== | CONTENT ITEMS \-------------------------------------------------------------------------------------------------------------------------*/ diff --git a/OnTopic.ViewModels/_contentTypes/IndexTopicViewModel.cs b/OnTopic.ViewModels/_contentTypes/IndexTopicViewModel.cs index 215adeb0..21d701b1 100644 --- a/OnTopic.ViewModels/_contentTypes/IndexTopicViewModel.cs +++ b/OnTopic.ViewModels/_contentTypes/IndexTopicViewModel.cs @@ -19,6 +19,20 @@ namespace OnTopic.ViewModels { /// public record IndexTopicViewModel: PageTopicViewModel { + /*========================================================================================================================== + | CONSTRUCTOR + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Initializes a new with an dictionary. + /// + /// An of attribute values. + public IndexTopicViewModel(AttributeDictionary attributes): base(attributes) { } + + /// + /// Initializes a new with no parameters. + /// + public IndexTopicViewModel() { } + } //Class } //Namespace \ No newline at end of file diff --git a/OnTopic.ViewModels/_contentTypes/PageGroupTopicViewModel.cs b/OnTopic.ViewModels/_contentTypes/PageGroupTopicViewModel.cs index 175c32c3..e2d53f2f 100644 --- a/OnTopic.ViewModels/_contentTypes/PageGroupTopicViewModel.cs +++ b/OnTopic.ViewModels/_contentTypes/PageGroupTopicViewModel.cs @@ -19,6 +19,20 @@ namespace OnTopic.ViewModels { /// public record PageGroupTopicViewModel : SectionTopicViewModel { + /*========================================================================================================================== + | CONSTRUCTOR + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Initializes a new with an dictionary. + /// + /// An of attribute values. + public PageGroupTopicViewModel(AttributeDictionary attributes): base(attributes) { } + + /// + /// Initializes a new with no parameters. + /// + public PageGroupTopicViewModel() { } + } //Class } //Namespace \ No newline at end of file diff --git a/OnTopic.ViewModels/_contentTypes/PageTopicViewModel.cs b/OnTopic.ViewModels/_contentTypes/PageTopicViewModel.cs index d3919208..1d25a950 100644 --- a/OnTopic.ViewModels/_contentTypes/PageTopicViewModel.cs +++ b/OnTopic.ViewModels/_contentTypes/PageTopicViewModel.cs @@ -3,7 +3,6 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ -using OnTopic.Models; namespace OnTopic.ViewModels { @@ -20,6 +19,29 @@ namespace OnTopic.ViewModels { /// public record PageTopicViewModel: TopicViewModel, INavigableTopicViewModel { + /*========================================================================================================================== + | CONSTRUCTOR + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Initializes a new with an dictionary. + /// + /// An of attribute values. + public PageTopicViewModel(AttributeDictionary attributes): base(attributes) { + Contract.Requires(attributes, nameof(attributes)); + ShortTitle = attributes.GetValue(nameof(ShortTitle)); + Subtitle = attributes.GetValue(nameof(Subtitle)); + MetaTitle = attributes.GetValue(nameof(MetaTitle)); + MetaDescription = attributes.GetValue(nameof(MetaDescription)); + MetaKeywords = attributes.GetValue(nameof(MetaKeywords)); + NoIndex = attributes.GetBoolean(nameof(NoIndex))?? NoIndex; + Body = attributes.GetValue(nameof(Body)); + } + + /// + /// Initializes a new with no parameters. + /// + public PageTopicViewModel() { } + /*========================================================================================================================== | SHORT TITLE \-------------------------------------------------------------------------------------------------------------------------*/ diff --git a/OnTopic.ViewModels/_contentTypes/SectionTopicViewModel.cs b/OnTopic.ViewModels/_contentTypes/SectionTopicViewModel.cs index 8edc18a8..ddd96c81 100644 --- a/OnTopic.ViewModels/_contentTypes/SectionTopicViewModel.cs +++ b/OnTopic.ViewModels/_contentTypes/SectionTopicViewModel.cs @@ -3,7 +3,6 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ -using System; namespace OnTopic.ViewModels { @@ -20,6 +19,23 @@ namespace OnTopic.ViewModels { /// public record SectionTopicViewModel : TopicViewModel { + /*========================================================================================================================== + | CONSTRUCTOR + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Initializes a new with an dictionary. + /// + /// An of attribute values. + public SectionTopicViewModel(AttributeDictionary attributes): base(attributes) { + Contract.Requires(attributes, nameof(attributes)); + HeaderImageUrl = attributes.GetUri(nameof(HeaderImageUrl)); + } + + /// + /// Initializes a new with no parameters. + /// + public SectionTopicViewModel() { } + /*========================================================================================================================== | HEADER IMAGE \-------------------------------------------------------------------------------------------------------------------------*/ diff --git a/OnTopic.ViewModels/_contentTypes/SlideshowTopicViewModel.cs b/OnTopic.ViewModels/_contentTypes/SlideshowTopicViewModel.cs index 5d2574fd..9991e555 100644 --- a/OnTopic.ViewModels/_contentTypes/SlideshowTopicViewModel.cs +++ b/OnTopic.ViewModels/_contentTypes/SlideshowTopicViewModel.cs @@ -19,6 +19,23 @@ namespace OnTopic.ViewModels { /// public record SlideshowTopicViewModel: ContentListTopicViewModel { + /*========================================================================================================================== + | CONSTRUCTOR + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Initializes a new with an dictionary. + /// + /// An of attribute values. + public SlideshowTopicViewModel(AttributeDictionary attributes): base(attributes) { + Contract.Requires(attributes, nameof(attributes)); + TransitionEffect = attributes.GetValue(nameof(TransitionEffect)); + } + + /// + /// Initializes a new with no parameters. + /// + public SlideshowTopicViewModel() { } + /*========================================================================================================================== | TRANSITION EFFECT \-------------------------------------------------------------------------------------------------------------------------*/ diff --git a/OnTopic.ViewModels/_contentTypes/VideoTopicViewModel.cs b/OnTopic.ViewModels/_contentTypes/VideoTopicViewModel.cs index 85aa78b5..c88fc669 100644 --- a/OnTopic.ViewModels/_contentTypes/VideoTopicViewModel.cs +++ b/OnTopic.ViewModels/_contentTypes/VideoTopicViewModel.cs @@ -3,8 +3,6 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ -using System; -using System.ComponentModel.DataAnnotations; namespace OnTopic.ViewModels { @@ -21,6 +19,24 @@ namespace OnTopic.ViewModels { /// public record VideoTopicViewModel: PageTopicViewModel { + /*========================================================================================================================== + | CONSTRUCTOR + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Initializes a new with an dictionary. + /// + /// An of attribute values. + public VideoTopicViewModel(AttributeDictionary attributes): base(attributes) { + Contract.Requires(attributes, nameof(attributes)); + VideoUrl = attributes.GetUri(nameof(VideoUrl))!; + PosterUrl = attributes.GetUri(nameof(PosterUrl)); + } + + /// + /// Initializes a new with no parameters. + /// + public VideoTopicViewModel() { } + /*========================================================================================================================== | VIDEO URL \-------------------------------------------------------------------------------------------------------------------------*/ diff --git a/OnTopic.ViewModels/_items/CacheProfileTopicViewModel.cs b/OnTopic.ViewModels/_items/CacheProfileTopicViewModel.cs new file mode 100644 index 00000000..e4d78421 --- /dev/null +++ b/OnTopic.ViewModels/_items/CacheProfileTopicViewModel.cs @@ -0,0 +1,89 @@ +/*============================================================================================================================== +| Author Ignia, LLC +| Client Ignia, LLC +| Project Topics Library +\=============================================================================================================================*/ + +namespace OnTopic.ViewModels { + + /*============================================================================================================================ + | VIEW MODEL: CACHE PROFILE + \---------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Provides a strongly-typed model for feeding views with information about a CacheProfile topic, as referenced by + /// the and its derivatives. + /// + /// + /// Typically, view models should be created as part of the presentation layer. The namespace contains + /// default implementations that can be used directly, used as base classes, or overwritten at the presentation level. They + /// are supplied for convenience to model factory default settings for out-of-the-box content types. + /// + public record CacheProfileTopicViewModel: ItemTopicViewModel { + + /*========================================================================================================================== + | CONSTRUCTOR + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Initializes a new with an dictionary. + /// + /// An of attribute values. + public CacheProfileTopicViewModel(AttributeDictionary attributes): base(attributes) { + Contract.Requires(attributes, nameof(attributes)); + Duration = attributes.GetInteger(nameof(Duration)); + Location = attributes.GetValue(nameof(Location)); + NoStore = attributes.GetBoolean(nameof(NoStore)); + VaryByHeader = attributes.GetValue(nameof(VaryByHeader)); + VaryByQueryKeys = attributes.GetValue(nameof(VaryByQueryKeys)); + } + + /// + /// Initializes a new with no parameters. + /// + public CacheProfileTopicViewModel() { } + + /*========================================================================================================================== + | DURATION + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Gets or sets the duration in seconds for which the response is cached. If this property is set, the "max-age" in the + /// "Cache-control" header is set in the response. + /// + public int? Duration { get; init; } + + /*========================================================================================================================== + | LOCATION + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Gets or sets the location where the data from a particular URL must be cached. If this property is set, the + /// "Cache-control" header is set in the response. + /// + public string? Location { get; init; } + + /*========================================================================================================================== + | NO STORE + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Gets or sets the value which determines whether the data should be stored or not. When set to true, it sets the + /// "Cache-control" header in the response to "no-store". Ignores the "Location" parameter for values other than "None". + /// Ignores the "Duration" parameter. + /// + public bool? NoStore { get; init; } + + /*========================================================================================================================== + | VARY BY HEADER + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Gets or sets which header the cache should vary by, if appropriate. + /// + public string? VaryByHeader { get; init; } + + /*========================================================================================================================== + | VARY BY QUERY KEYS + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Gets or sets which query string parameter keys the cache should vary by, if appropriate. Should be comma delimited. + /// + public string? VaryByQueryKeys { get; init; } + + } //Class +} //Namespace \ No newline at end of file diff --git a/OnTopic.ViewModels/_items/ContentItemTopicViewModel.cs b/OnTopic.ViewModels/_items/ContentItemTopicViewModel.cs index 587163e6..2c8d7b3d 100644 --- a/OnTopic.ViewModels/_items/ContentItemTopicViewModel.cs +++ b/OnTopic.ViewModels/_items/ContentItemTopicViewModel.cs @@ -3,7 +3,6 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ -using System; namespace OnTopic.ViewModels { @@ -21,6 +20,26 @@ namespace OnTopic.ViewModels { /// public record ContentItemTopicViewModel: ItemTopicViewModel { + /*========================================================================================================================== + | CONSTRUCTOR + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Initializes a new with an dictionary. + /// + /// An of attribute values. + public ContentItemTopicViewModel(AttributeDictionary attributes): base(attributes) { + Contract.Requires(attributes, nameof(attributes)); + Description = attributes.GetValue(nameof(Description))!; + LearnMoreUrl = attributes.GetUri(nameof(LearnMoreUrl)); + ThumbnailImage = attributes.GetUri(nameof(ThumbnailImage)); + Category = attributes.GetValue(nameof(Category)); + } + + /// + /// Initializes a new with no parameters. + /// + public ContentItemTopicViewModel() { } + /*========================================================================================================================== | DESCRIPTION \-------------------------------------------------------------------------------------------------------------------------*/ diff --git a/OnTopic.ViewModels/_items/ItemTopicViewModel.cs b/OnTopic.ViewModels/_items/ItemTopicViewModel.cs index 5c0dd63c..51c04ab7 100644 --- a/OnTopic.ViewModels/_items/ItemTopicViewModel.cs +++ b/OnTopic.ViewModels/_items/ItemTopicViewModel.cs @@ -19,6 +19,19 @@ namespace OnTopic.ViewModels { /// public record ItemTopicViewModel : TopicViewModel { + /*========================================================================================================================== + | CONSTRUCTOR + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Initializes a new with an dictionary. + /// + /// An of attribute values. + public ItemTopicViewModel(AttributeDictionary attributes): base(attributes) { } + + /// + /// Initializes a new with no parameters. + /// + public ItemTopicViewModel() { } } //Class } //Namespace \ No newline at end of file diff --git a/OnTopic.ViewModels/_items/ListTopicViewModel.cs b/OnTopic.ViewModels/_items/ListTopicViewModel.cs index 15b1e076..24cbe106 100644 --- a/OnTopic.ViewModels/_items/ListTopicViewModel.cs +++ b/OnTopic.ViewModels/_items/ListTopicViewModel.cs @@ -3,8 +3,8 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ -using System; using System.Collections; +using OnTopic.Mapping.Annotations; namespace OnTopic.ViewModels { @@ -28,8 +28,8 @@ namespace OnTopic.ViewModels { /// /// [Obsolete( - "There should not be a need for a LookupListItem view model. If these must be referenced, prefer using e.g. [MapAs()] " + - "to specify a common view model.", + $"There should not be a need for a LookupListItem view model. If these must be referenced, prefer using e.g. " + + $"{nameof(MapAsAttribute)} to specify a common view model.", false )] public record ListTopicViewModel: ContentItemTopicViewModel { diff --git a/OnTopic.ViewModels/_items/LookupListItemTopicViewModel.cs b/OnTopic.ViewModels/_items/LookupListItemTopicViewModel.cs index 20845298..79c92fb7 100644 --- a/OnTopic.ViewModels/_items/LookupListItemTopicViewModel.cs +++ b/OnTopic.ViewModels/_items/LookupListItemTopicViewModel.cs @@ -19,6 +19,19 @@ namespace OnTopic.ViewModels { /// public record LookupListItemTopicViewModel: ItemTopicViewModel { + /*========================================================================================================================== + | CONSTRUCTOR + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Initializes a new with an dictionary. + /// + /// An of attribute values. + public LookupListItemTopicViewModel(AttributeDictionary attributes): base(attributes) { } + + /// + /// Initializes a new with no parameters. + /// + public LookupListItemTopicViewModel() { } } //Class } //Namespace \ No newline at end of file diff --git a/OnTopic.ViewModels/_items/SlideTopicViewModel.cs b/OnTopic.ViewModels/_items/SlideTopicViewModel.cs index be402f25..a81ae307 100644 --- a/OnTopic.ViewModels/_items/SlideTopicViewModel.cs +++ b/OnTopic.ViewModels/_items/SlideTopicViewModel.cs @@ -20,6 +20,19 @@ namespace OnTopic.ViewModels { /// public record SlideTopicViewModel: ContentItemTopicViewModel { + /*========================================================================================================================== + | CONSTRUCTOR + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Initializes a new with an dictionary. + /// + /// An of attribute values. + public SlideTopicViewModel(AttributeDictionary attributes): base(attributes) { } + + /// + /// Initializes a new with no parameters. + /// + public SlideTopicViewModel() { } } //Class } //Namespace \ No newline at end of file diff --git a/OnTopic/Associations/ReferenceSetterAttribute.cs b/OnTopic/Associations/ReferenceSetterAttribute.cs index 9f5446f4..e7fe15db 100644 --- a/OnTopic/Associations/ReferenceSetterAttribute.cs +++ b/OnTopic/Associations/ReferenceSetterAttribute.cs @@ -3,7 +3,6 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ -using System; using OnTopic.Collections.Specialized; namespace OnTopic.Associations { @@ -19,15 +18,15 @@ namespace OnTopic.Associations { /// /// When a call is made to the code will check to see if a property with the same name as the reference key exists, and - /// whether that property is decorated with the (i.e., [ReferenceSetter] - /// ). If is, then the update will be routed through that property. This ensures that business logic is enforced by - /// local properties, instead of allowing business logic to be potentially bypassed by writing directly to the collection. + /// whether that property is decorated with the (i.e., [ReferenceSetter]). + /// If is, then the update will be routed through that property. This ensures that business logic is enforced by local + /// properties, instead of allowing business logic to be potentially bypassed by writing directly to the collection. /// /// /// As an example, the property is adorned with the . - /// As a result, if a client calls topic.References.SetValue("BaseTopic", topic), then that update will be - /// routed through , thus enforcing any validation. + /// As a result, if a client calls topic.References.SetValue("BaseTopic", topic), then that update will be routed + /// through , thus enforcing any validation. /// /// /// To ensure this logic, it is critical that implementers of ensure that the diff --git a/OnTopic/Associations/TopicReferenceCollection.cs b/OnTopic/Associations/TopicReferenceCollection.cs index 1f21b47a..8b967256 100644 --- a/OnTopic/Associations/TopicReferenceCollection.cs +++ b/OnTopic/Associations/TopicReferenceCollection.cs @@ -3,9 +3,7 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ -using System; using OnTopic.Collections.Specialized; -using OnTopic.Internal.Diagnostics; using OnTopic.Repositories; namespace OnTopic.Associations { diff --git a/OnTopic/Associations/TopicReferenceRecord.cs b/OnTopic/Associations/TopicReferenceRecord.cs index 36803df6..994f4ce6 100644 --- a/OnTopic/Associations/TopicReferenceRecord.cs +++ b/OnTopic/Associations/TopicReferenceRecord.cs @@ -3,7 +3,6 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ -using System; using OnTopic.Collections.Specialized; using OnTopic.Metadata; using OnTopic.Repositories; diff --git a/OnTopic/Associations/TopicRelationshipMultiMap.cs b/OnTopic/Associations/TopicRelationshipMultiMap.cs index 1a77a8f8..19965744 100644 --- a/OnTopic/Associations/TopicRelationshipMultiMap.cs +++ b/OnTopic/Associations/TopicRelationshipMultiMap.cs @@ -3,10 +3,7 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ -using System; -using System.Diagnostics.CodeAnalysis; using OnTopic.Collections.Specialized; -using OnTopic.Internal.Diagnostics; using OnTopic.Querying; using OnTopic.Repositories; @@ -77,7 +74,7 @@ public void Clear(string relationshipKey) { /// [ExcludeFromCodeCoverage] - [Obsolete("The ClearTopics(relationshipKey) method has been renamed to Clear(relationshipKey).", true)] + [Obsolete($"The {nameof(ClearTopics)} method has been renamed to {nameof(Clear)}.", true)] public void ClearTopics(string relationshipKey) => Clear(relationshipKey); /*========================================================================================================================== @@ -149,12 +146,12 @@ internal bool Remove(string relationshipKey, Topic topic, bool isIncoming) { /// [ExcludeFromCodeCoverage] - [Obsolete("The RemoveTopic() method has been renamed to Remove().", true)] + [Obsolete($"The {nameof(RemoveTopic)} method has been renamed to {nameof(Remove)}.", true)] public bool RemoveTopic(string relationshipKey, Topic topic) => Remove(relationshipKey, topic); /// [ExcludeFromCodeCoverage] - [Obsolete("The RemoveTopic() method has been renamed to Remove().", true)] + [Obsolete($"The {nameof(RemoveTopic)} method has been renamed to {nameof(Remove)}.", true)] public bool RemoveTopic(string relationshipKey, Topic topic, bool isIncoming) => Remove(relationshipKey, topic, isIncoming); @@ -232,12 +229,12 @@ internal void SetValue(string relationshipKey, Topic topic, bool? markDirty, boo /// [ExcludeFromCodeCoverage] - [Obsolete("The SetTopic() method has been renamed to SetValue().", true)] + [Obsolete($"The {nameof(SetTopic)} method has been renamed to {nameof(SetValue)}.", true)] public void SetTopic(string relationshipKey, Topic topic, bool? isDirty = null) => SetValue(relationshipKey, topic, isDirty); /// [ExcludeFromCodeCoverage] - [Obsolete("The SetTopic() method has been renamed to SetValue().", true)] + [Obsolete($"The {nameof(SetTopic)} method has been renamed to {nameof(SetValue)}.", true)] public void SetTopic(string relationshipKey, Topic topic, bool? isDirty, bool isIncoming) => SetValue(relationshipKey, topic, isDirty, isIncoming); diff --git a/OnTopic/Attributes/AttributeCollection.cs b/OnTopic/Attributes/AttributeCollection.cs index 2b94afb7..1678f111 100644 --- a/OnTopic/Attributes/AttributeCollection.cs +++ b/OnTopic/Attributes/AttributeCollection.cs @@ -3,8 +3,6 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ -using System; -using System.Linq; using OnTopic.Collections.Specialized; using OnTopic.Repositories; @@ -23,6 +21,14 @@ namespace OnTopic.Attributes { /// public class AttributeCollection : TrackedRecordCollection { + /*========================================================================================================================== + | PRIVATE VARIABLES + \-------------------------------------------------------------------------------------------------------------------------*/ + private static readonly List _excludedAttributes = new() { + nameof(Topic.Title), + nameof(Topic.LastModified) + }; + /*========================================================================================================================== | CONSTRUCTOR \-------------------------------------------------------------------------------------------------------------------------*/ @@ -54,7 +60,6 @@ internal AttributeCollection(Topic parentTopic) : base(parentTopic) { /*========================================================================================================================== | METHOD: IS DIRTY \-------------------------------------------------------------------------------------------------------------------------*/ - /// /// Determine if any attributes in the are dirty. /// @@ -135,5 +140,40 @@ public void SetValue( } } + /*========================================================================================================================== + | METHOD: AS ATTRIBUTE DICTIONARY + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Gets an based on the of the current . Optionall includes attributes from any s that the derives from. + /// + /// + /// The method will exclude attributes which correspond to properties on + /// which contain specialized getter logic, such as and . + /// + /// + /// Determines if attributes from the should be included. Defaults to false. + /// + /// A new containing attributes + public AttributeDictionary AsAttributeDictionary(bool inheritFromBase = false) { + var sourceAttributes = (AttributeCollection?)this; + var attributes = new AttributeDictionary(); + var count = 0; + while (sourceAttributes is not null && ++count < 5) { + foreach (var attribute in sourceAttributes) { + if (count is 1 || !attributes.ContainsKey(attribute.Key)) { + attributes.TryAdd(attribute.Key, attribute.Value); + } + } + sourceAttributes = inheritFromBase? sourceAttributes.AssociatedTopic.BaseTopic?.Attributes : null; + } + foreach (var attribute in _excludedAttributes) { + attributes.Remove(attribute); + } + return attributes; + } + } //Class } //Namespace \ No newline at end of file diff --git a/OnTopic/Attributes/AttributeCollectionExtensions.cs b/OnTopic/Attributes/AttributeCollectionExtensions.cs index 56639408..ffbc9975 100644 --- a/OnTopic/Attributes/AttributeCollectionExtensions.cs +++ b/OnTopic/Attributes/AttributeCollectionExtensions.cs @@ -3,10 +3,8 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ -using System; using System.Globalization; using OnTopic.Collections.Specialized; -using OnTopic.Internal.Diagnostics; using OnTopic.Repositories; namespace OnTopic.Attributes { @@ -168,6 +166,43 @@ public static DateTime GetDateTime( )?? defaultValue; } + /*========================================================================================================================== + | METHOD: GET URI + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Gets a named attribute from the Attributes dictionary with a specified default value, an optional setting for enabling + /// of inheritance, and an optional setting for searching through base topics for values. Return as a URI. + /// + /// The instance of the this extension is bound to. + /// The string identifier for the . + /// A string value to which to fall back in the case the value is not found. + /// + /// Boolean indicator nothing whether to search through the topic's parents in order to get the value. + /// + /// + /// Boolean indicator nothing whether to search through any of the topic's s in order to get + /// the value. + /// + /// The value for the attribute as a DateTime object. + public static Uri? GetUri( + this AttributeCollection attributes, + string name, + Uri? defaultValue = default, + bool inheritFromParent = false, + bool inheritFromBase = true + ) { + Contract.Requires(attributes); + Contract.Requires(!String.IsNullOrWhiteSpace(name), nameof(name)); + return AttributeValueConverter.Convert( + attributes.GetValue( + name, + null, + inheritFromParent, + inheritFromBase ? 5 : 0 + ) + )?? defaultValue; + } + /*========================================================================================================================== | METHOD: SET BOOLEAN \-------------------------------------------------------------------------------------------------------------------------*/ @@ -332,5 +367,47 @@ public static void SetDateTime( isDirty ); + /*========================================================================================================================== + | METHOD: SET URI + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Helper method that either adds a new object or updates the value of an existing one, + /// depending on whether that value already exists. + /// + /// The instance of the this extension is bound to. + /// The string identifier for the . + /// The value for the . + /// + /// Specified whether the value should be marked as . By default, it will be marked + /// as dirty if the value is new or has changed from a previous value. By setting this parameter, that behavior is + /// overwritten to accept whatever value is submitted. This can be used, for instance, to prevent an update from being + /// persisted to the data store on . + /// + /// + /// !String.IsNullOrWhiteSpace(key) + /// + /// + /// !String.IsNullOrWhiteSpace(value) + /// + /// + /// !value.Contains(" ") + /// + public static void SetUri( + this AttributeCollection attributes, + string key, + Uri value, + bool? isDirty = null + ) => attributes?.SetValue( + key, + value?.ToString(), + isDirty + ); + } //Class } //Namespace \ No newline at end of file diff --git a/OnTopic/Attributes/AttributeDictionary.cs b/OnTopic/Attributes/AttributeDictionary.cs new file mode 100644 index 00000000..e2b98918 --- /dev/null +++ b/OnTopic/Attributes/AttributeDictionary.cs @@ -0,0 +1,102 @@ +/*============================================================================================================================== +| Author Ignia, LLC +| Client Ignia, LLC +| Project Topics Library +\=============================================================================================================================*/ +using OnTopic.Mapping; + +namespace OnTopic.Attributes { + + /*============================================================================================================================ + | CLASS: ATTRIBUTE DICTIONARY + \---------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Provides a light-weight dictionary of attribute values. + /// + /// + /// The is used by the to support self-constructed + /// models that don't require the use of the to set the values of scalar properties. Any + /// model that has a primary constructor accepting a single will be initialized using + /// a containing all attributes from not only the current , but also + /// any s it references. + /// + public class AttributeDictionary: Dictionary { + + /*========================================================================================================================== + | CONSTRUCTOR + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + public AttributeDictionary(): base(StringComparer.OrdinalIgnoreCase) { } + + /*========================================================================================================================== + | METHOD: GET VALUE + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Gets an attribute value as a string from the based on the . + /// + /// The key of the attribute to retrieve. + /// + public string? GetValue(string attributeKey) { + Contract.Requires(!String.IsNullOrWhiteSpace(attributeKey), nameof(attributeKey)); + TryGetValue(attributeKey, out var value); + return String.IsNullOrWhiteSpace(value)? null : value; + } + + /*========================================================================================================================== + | METHOD: GET BOOLEAN VALUE + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Gets an attribute value as a Boolean from the based on the . + /// + /// The string identifier for the . + /// The value for the attribute as a boolean. + public bool? GetBoolean(string attributeKey) => AttributeValueConverter.Convert(GetValue(attributeKey)); + + /*========================================================================================================================== + | METHOD: GET INTEGER + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Gets an attribute value as an integer from the based on the . + /// + /// The string identifier for the . + /// The value for the attribute as an integer. + public int? GetInteger(string attributeKey) => AttributeValueConverter.Convert(GetValue(attributeKey)); + + /*========================================================================================================================== + | METHOD: GET DOUBLE + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Gets an attribute value as a double from the based on the . + /// + /// The string identifier for the . + /// The value for the attribute as a double. + public double? GetDouble(string attributeKey) => AttributeValueConverter.Convert(GetValue(attributeKey)); + + /*========================================================================================================================== + | METHOD: GET DATETIME + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Gets an attribute value as a date/time from the based on the . + /// + /// The string identifier for the . + /// The value for the attribute as a DateTime object. + public DateTime? GetDateTime(string attributeKey) => AttributeValueConverter.Convert(GetValue(attributeKey)); + + /*========================================================================================================================== + | METHOD: GET URI + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Gets an attribute value as a URI from the based on the . + /// + /// The string identifier for the . + /// The value for the attribute as a Uri object. + public Uri? GetUri(string attributeKey) => AttributeValueConverter.Convert(GetValue(attributeKey)); + + } //Class +} //Namespace \ No newline at end of file diff --git a/OnTopic/Attributes/AttributeRecord.cs b/OnTopic/Attributes/AttributeRecord.cs index dfac2859..1b911c8e 100644 --- a/OnTopic/Attributes/AttributeRecord.cs +++ b/OnTopic/Attributes/AttributeRecord.cs @@ -3,7 +3,6 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ -using System; using OnTopic.Collections.Specialized; using OnTopic.Metadata; using OnTopic.Repositories; diff --git a/OnTopic/Attributes/AttributeSetterAttribute.cs b/OnTopic/Attributes/AttributeSetterAttribute.cs index 8e7bcb89..0068f903 100644 --- a/OnTopic/Attributes/AttributeSetterAttribute.cs +++ b/OnTopic/Attributes/AttributeSetterAttribute.cs @@ -3,7 +3,6 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ -using System; using OnTopic.Collections; using OnTopic.Collections.Specialized; @@ -20,9 +19,9 @@ namespace OnTopic.Attributes { /// /// When a call is made to , the /// code will check to see if a property with the same name as the attribute key exists, and whether that property is - /// decorated with the (i.e., [AttributeSetter]). If it is, then the - /// update will be routed through that property. This ensures that business logic is enforced by local properties, instead - /// of allowing business logic to be potentially bypassed by writing directly to the + /// decorated with the (i.e., [AttributeSetter]). If it is, then the update + /// will be routed through that property. This ensures that business logic is enforced by local properties, instead of + /// allowing business logic to be potentially bypassed by writing directly to the /// collection. /// /// diff --git a/OnTopic/Attributes/AttributeValueConverter.cs b/OnTopic/Attributes/AttributeValueConverter.cs index 7bb82937..e890cb42 100644 --- a/OnTopic/Attributes/AttributeValueConverter.cs +++ b/OnTopic/Attributes/AttributeValueConverter.cs @@ -3,7 +3,6 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ -using System; using System.Collections.ObjectModel; using System.Globalization; using OnTopic.Mapping; @@ -128,25 +127,23 @@ internal static class AttributeValueConverter { /*------------------------------------------------------------------------------------------------------------------------ | Handle type-specific rules \-----------------------------------------------------------------------------------------------------------------------*/ - var type = value.GetType(); - - if (type.Equals(typeof(string))) { - return (string)value; + if (value is string stringValue) { + return stringValue; } - else if (type.Equals(typeof(bool)) || type.Equals(typeof(bool?))) { - return ((bool)value) ? "1" : "0"; + else if (value is bool boolValue) { + return boolValue ? "1" : "0"; } - else if (type.Equals(typeof(int)) || type.Equals(typeof(int?))) { - return ((int)value).ToString(CultureInfo.InvariantCulture); + else if (value is int intValue) { + return intValue.ToString(CultureInfo.InvariantCulture); } - else if (type.Equals(typeof(double)) || type.Equals(typeof(double?))) { - return ((double)value).ToString(CultureInfo.InvariantCulture); + else if (value is double doubleValue) { + return doubleValue.ToString(CultureInfo.InvariantCulture); } - else if (type.Equals(typeof(DateTime)) || type.Equals(typeof(DateTime?))) { - return ((DateTime)value).ToString(CultureInfo.InvariantCulture); + else if (value is DateTime dateTimeValue) { + return dateTimeValue.ToString(CultureInfo.InvariantCulture); } - else if (type.Equals(typeof(Uri))) { - return ((Uri)value).ToString(); + else if (value is Uri uriValue) { + return uriValue.ToString(); } /*------------------------------------------------------------------------------------------------------------------------ diff --git a/OnTopic/Collections/KeyedTopicCollection.cs b/OnTopic/Collections/KeyedTopicCollection.cs index 089f54c5..1edae63f 100644 --- a/OnTopic/Collections/KeyedTopicCollection.cs +++ b/OnTopic/Collections/KeyedTopicCollection.cs @@ -3,7 +3,6 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ -using System.Collections.Generic; namespace OnTopic.Collections { diff --git a/OnTopic/Collections/KeyedTopicCollection{T}.cs b/OnTopic/Collections/KeyedTopicCollection{T}.cs index a099867b..885780d6 100644 --- a/OnTopic/Collections/KeyedTopicCollection{T}.cs +++ b/OnTopic/Collections/KeyedTopicCollection{T}.cs @@ -3,11 +3,7 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ -using System; -using System.Collections.Generic; using System.Collections.ObjectModel; -using System.Diagnostics.CodeAnalysis; -using OnTopic.Internal.Diagnostics; namespace OnTopic.Collections { @@ -50,7 +46,7 @@ public KeyedTopicCollection(IEnumerable? topics = null) : base(StringComparer /// [ExcludeFromCodeCoverage] - [Obsolete("The GetTopic() method has been renamed to GetValue().", true)] + [Obsolete($"The {nameof(GetTopic)} method has been renamed to {nameof(GetValue)}.", true)] public T? GetTopic(string key) => GetValue(key); /*========================================================================================================================== diff --git a/OnTopic/Collections/ReadOnlyKeyedTopicCollection.cs b/OnTopic/Collections/ReadOnlyKeyedTopicCollection.cs index c93b0173..8e2bcd57 100644 --- a/OnTopic/Collections/ReadOnlyKeyedTopicCollection.cs +++ b/OnTopic/Collections/ReadOnlyKeyedTopicCollection.cs @@ -3,7 +3,6 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ -using System.Collections.Generic; namespace OnTopic.Collections { diff --git a/OnTopic/Collections/ReadOnlyKeyedTopicCollection{T}.cs b/OnTopic/Collections/ReadOnlyKeyedTopicCollection{T}.cs index e0bad7cf..aca8270b 100644 --- a/OnTopic/Collections/ReadOnlyKeyedTopicCollection{T}.cs +++ b/OnTopic/Collections/ReadOnlyKeyedTopicCollection{T}.cs @@ -3,11 +3,7 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ -using System; -using System.Collections.Generic; using System.Collections.ObjectModel; -using System.Diagnostics.CodeAnalysis; -using OnTopic.Internal.Diagnostics; namespace OnTopic.Collections { @@ -65,7 +61,7 @@ public class ReadOnlyKeyedTopicCollection : ReadOnlyCollection where T : T /// [ExcludeFromCodeCoverage] - [Obsolete("The GetTopic() method has been renamed to GetValue().", true)] + [Obsolete($"The {nameof(GetTopic)} method has been renamed to {nameof(GetValue)}.", true)] public T? GetTopic(string key) => GetValue(key); /*========================================================================================================================== diff --git a/OnTopic/Collections/ReadOnlyTopicCollection.cs b/OnTopic/Collections/ReadOnlyTopicCollection.cs index bd37ae6c..c0a50758 100644 --- a/OnTopic/Collections/ReadOnlyTopicCollection.cs +++ b/OnTopic/Collections/ReadOnlyTopicCollection.cs @@ -3,10 +3,7 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ -using System; -using System.Collections.Generic; using System.Collections.ObjectModel; -using System.Diagnostics.CodeAnalysis; namespace OnTopic.Collections { @@ -49,7 +46,8 @@ public class ReadOnlyTopicCollection : ReadOnlyCollection { /// [ExcludeFromCodeCoverage] [Obsolete( - "The GetTopic() method is not implemented on ReadOnlyTopicCollection. Use ReadOnlyKeyedTopicCollection instead.", + $"The {nameof(GetValue)} method is not implemented on {nameof(ReadOnlyTopicCollection)}. Use " + + $"{nameof(ReadOnlyKeyedTopicCollection)} instead.", true )] public Topic? GetValue(string key) => throw new NotImplementedException(); @@ -60,7 +58,8 @@ public class ReadOnlyTopicCollection : ReadOnlyCollection { /// [ExcludeFromCodeCoverage] [Obsolete( - "Indexing by key is not implemented on ReadOnlyTopicCollection. Use ReadOnlyKeyedTopicCollection instead.", + $"Indexing by key is not implemented on {nameof(ReadOnlyTopicCollection)}. Use {nameof(ReadOnlyKeyedTopicCollection)} " + + $"instead.", true )] public Topic this[string key] => throw new ArgumentOutOfRangeException(nameof(key)); diff --git a/OnTopic/Collections/Specialized/KeyValuesPair{TKey,TValue}.cs b/OnTopic/Collections/Specialized/KeyValuesPair{TKey,TValue}.cs index f3dda410..f17924a1 100644 --- a/OnTopic/Collections/Specialized/KeyValuesPair{TKey,TValue}.cs +++ b/OnTopic/Collections/Specialized/KeyValuesPair{TKey,TValue}.cs @@ -3,8 +3,6 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ -using System.Collections.Generic; -using OnTopic.Internal.Diagnostics; namespace OnTopic.Collections.Specialized { diff --git a/OnTopic/Collections/Specialized/ReadOnlyTopicMultiMap.cs b/OnTopic/Collections/Specialized/ReadOnlyTopicMultiMap.cs index 08bb0f7f..fd219369 100644 --- a/OnTopic/Collections/Specialized/ReadOnlyTopicMultiMap.cs +++ b/OnTopic/Collections/Specialized/ReadOnlyTopicMultiMap.cs @@ -3,13 +3,8 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ -using System; using System.Collections; -using System.Collections.Generic; using System.Collections.ObjectModel; -using System.Diagnostics.CodeAnalysis; -using System.Linq; -using OnTopic.Internal.Diagnostics; namespace OnTopic.Collections.Specialized { @@ -111,7 +106,7 @@ public ReadOnlyTopicCollection GetValues(string key) { /// [ExcludeFromCodeCoverage] - [Obsolete("The GetTopics() method has been renamed to GetValues().", true)] + [Obsolete($"The {nameof(GetTopics)} method has been renamed to {nameof(GetValues)}.", true)] public ReadOnlyTopicCollection GetTopics(string key) => GetValues(key); /*========================================================================================================================== @@ -138,12 +133,12 @@ public ReadOnlyTopicCollection GetAllValues(string contentType) => /// [ExcludeFromCodeCoverage] - [Obsolete("The GetAllTopics() method has been renamed to GetAllValues().", true)] + [Obsolete($"The {nameof(GetAllTopics)} method has been renamed to {nameof(GetAllValues)}.", true)] public ReadOnlyTopicCollection GetAllTopics(string key) => GetAllValues(key); /// [ExcludeFromCodeCoverage] - [Obsolete("The GetAllTopics() method has been renamed to GetAllValues().", true)] + [Obsolete($"The {nameof(GetAllTopics)} method has been renamed to {nameof(GetAllValues)}.", true)] public ReadOnlyTopicCollection GetAllTopics() => GetAllValues(); /*========================================================================================================================== diff --git a/OnTopic/Collections/Specialized/TopicIndex.cs b/OnTopic/Collections/Specialized/TopicIndex.cs index 5c2ea8b5..67dbfbe6 100644 --- a/OnTopic/Collections/Specialized/TopicIndex.cs +++ b/OnTopic/Collections/Specialized/TopicIndex.cs @@ -3,7 +3,6 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ -using System.Collections.Generic; namespace OnTopic.Collections.Specialized { diff --git a/OnTopic/Collections/Specialized/TopicMultiMap.cs b/OnTopic/Collections/Specialized/TopicMultiMap.cs index 262caa6d..18cf2b76 100644 --- a/OnTopic/Collections/Specialized/TopicMultiMap.cs +++ b/OnTopic/Collections/Specialized/TopicMultiMap.cs @@ -3,10 +3,7 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ -using System; using System.Collections.ObjectModel; -using System.Diagnostics.CodeAnalysis; -using OnTopic.Internal.Diagnostics; namespace OnTopic.Collections.Specialized { @@ -61,7 +58,7 @@ public TopicCollection GetValues(string key) { /// [ExcludeFromCodeCoverage] - [Obsolete("The GetTopics() method has been renamed to GetValues().", true)] + [Obsolete($"The {nameof(GetTopics)} method has been renamed to {nameof(GetValues)}.", true)] public TopicCollection GetTopics(string key) => GetValues(key); /*========================================================================================================================== diff --git a/OnTopic/Collections/Specialized/TrackedRecordCollection{TItem,TValue,TAttribute}.cs b/OnTopic/Collections/Specialized/TrackedRecordCollection{TItem,TValue,TAttribute}.cs index 7fa031ee..52c553de 100644 --- a/OnTopic/Collections/Specialized/TrackedRecordCollection{TItem,TValue,TAttribute}.cs +++ b/OnTopic/Collections/Specialized/TrackedRecordCollection{TItem,TValue,TAttribute}.cs @@ -3,12 +3,7 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ -using System; -using System.Collections.Generic; using System.Collections.ObjectModel; -using System.Diagnostics.CodeAnalysis; -using System.Linq; -using OnTopic.Internal.Diagnostics; using OnTopic.Internal.Reflection; using OnTopic.Repositories; @@ -50,11 +45,6 @@ public abstract class TrackedRecordCollection : /// A reference to the topic that the current collection is bound to. internal TrackedRecordCollection(Topic parentTopic) : base(StringComparer.OrdinalIgnoreCase) { - /*------------------------------------------------------------------------------------------------------------------------ - | Validate parameters - \-----------------------------------------------------------------------------------------------------------------------*/ - Contract.Requires(parentTopic, nameof(parentTopic)); - /*------------------------------------------------------------------------------------------------------------------------ | Set properties \-----------------------------------------------------------------------------------------------------------------------*/ @@ -269,7 +259,7 @@ public void MarkClean(string key, DateTime? version) { value = this[key].Value; } - if (value is not null && value.ToString().Length == 0) { + if (value is "") { value = null; } diff --git a/OnTopic/Collections/Specialized/TrackedRecord{T}.cs b/OnTopic/Collections/Specialized/TrackedRecord{T}.cs index d47c2f9b..ab74e5ee 100644 --- a/OnTopic/Collections/Specialized/TrackedRecord{T}.cs +++ b/OnTopic/Collections/Specialized/TrackedRecord{T}.cs @@ -3,11 +3,6 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ -using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using OnTopic.Attributes; -using OnTopic.Internal.Diagnostics; using OnTopic.Repositories; namespace OnTopic.Collections.Specialized { diff --git a/OnTopic/Collections/TopicCollection.cs b/OnTopic/Collections/TopicCollection.cs index 4a5e386a..7c4df021 100644 --- a/OnTopic/Collections/TopicCollection.cs +++ b/OnTopic/Collections/TopicCollection.cs @@ -3,11 +3,7 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ -using System; -using System.Collections.Generic; using System.Collections.ObjectModel; -using System.Diagnostics.CodeAnalysis; -using System.Linq; namespace OnTopic.Collections { @@ -39,11 +35,15 @@ public class TopicCollection : Collection { public ReadOnlyTopicCollection AsReadOnly() => new(this); /*========================================================================================================================== - | METHOD: GET TOPIC + | METHOD: GET VALUE \-------------------------------------------------------------------------------------------------------------------------*/ /// [ExcludeFromCodeCoverage] - [Obsolete("The GetTopic() method is not implemented on TopicCollection. Use KeyedTopicCollection instead.", true)] + [Obsolete( + $"The {nameof(GetValue)} method is not implemented on {nameof(TopicCollection)}. Use {nameof(KeyedTopicCollection)} " + + $"instead.", + true + )] public Topic? GetValue(string key) => throw new NotImplementedException(); /*========================================================================================================================== @@ -51,7 +51,10 @@ public class TopicCollection : Collection { \-------------------------------------------------------------------------------------------------------------------------*/ /// [ExcludeFromCodeCoverage] - [Obsolete("Indexing by key is not implemented on the TopicCollection. Use the KeyedTopicCollection instead.",true)] + [Obsolete( + $"Indexing by key is not implemented on the {nameof(TopicCollection)}. Use the {nameof(KeyedTopicCollection)} instead.", + true + )] public Topic this[string key] => throw new ArgumentOutOfRangeException(nameof(key)); } //Class diff --git a/OnTopic/Internal/Diagnostics/Contract.cs b/OnTopic/Internal/Diagnostics/Contract.cs index 5f8421a0..0e425c53 100644 --- a/OnTopic/Internal/Diagnostics/Contract.cs +++ b/OnTopic/Internal/Diagnostics/Contract.cs @@ -3,8 +3,6 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ -using System; -using System.Diagnostics.CodeAnalysis; using System.Reflection; using Microsoft; @@ -26,14 +24,14 @@ namespace OnTopic.Internal.Diagnostics { /// /// is not a replacement for Microsoft's Code Contract analysis and rewrite modules. Instead, it /// aims to maintain basic synatactical and functional support for the most basic (and important) features of the rewrite - /// module, such as Contract.Requires(), which is necessary to ensure paramater validation in many methods + /// module, such as Contract.Requires(), which is necessary to ensure paramater validation in many methods /// throughout the OnTopic library. does not seek to functionally reproduce Code Contract's - /// Ensures(), Assume(), or Invariant()—those methods are not implemented. + /// Ensures(), Assume(), or Invariant()—those methods are not implemented. /// /// - /// C# 8.0 will introduce nullable reference types. This largely mitigates the need for Contract.Requires(). - /// As such, this library can be seen as a temporary bridge to maintain parameter validation until C# 8.0 is released. At - /// that point, nullable reference types should be used in preference for many of the Contract.Requires() + /// C# 8.0 will introduce nullable reference types. This largely mitigates the need for Contract.Requires(). As + /// such, this library can be seen as a temporary bridge to maintain parameter validation until C# 8.0 is released. At + /// that point, nullable reference types should be used in preference for many of the Contract.Requires() /// calls—though, acknowledging, that there are some conditions that won't be satisfied by that (e.g., range checks). /// /// @@ -88,7 +86,7 @@ public static void Requires([ValidatedNotNull, NotNull]object? requiredObject, s /// public static void Requires(bool isValid, string? errorMessage = null) where T : Exception, new() { if (isValid) return; - if (errorMessage is null || errorMessage.Length == 0) { + if (String.IsNullOrEmpty(errorMessage)) { throw new T(); } try { diff --git a/OnTopic/Internal/Reflection/ItemMetadata.cs b/OnTopic/Internal/Reflection/ItemMetadata.cs new file mode 100644 index 00000000..cad4d710 --- /dev/null +++ b/OnTopic/Internal/Reflection/ItemMetadata.cs @@ -0,0 +1,201 @@ +/*============================================================================================================================== +| Author Ignia, LLC +| Client Ignia, LLC +| Project Topics Library +\=============================================================================================================================*/ +using System.Collections; +using System.Reflection; +using OnTopic.Mapping; +using OnTopic.Mapping.Internal; +using OnTopic.Mapping.Reverse; + +namespace OnTopic.Internal.Reflection { + + /*============================================================================================================================ + | CLASS: ITEM METADATA + \---------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Provides metadata associated with a given parameter, method, or property. + /// + internal abstract class ItemMetadata { + + /*========================================================================================================================== + | PRIVATE VARIABLES + \-------------------------------------------------------------------------------------------------------------------------*/ + private readonly static List _listTypes = new(); + private readonly ICustomAttributeProvider _attributeProvider; + private readonly Type _type = default!; + private List _customAttributes = default!; + private ItemConfiguration? _itemConfiguration; + + /*========================================================================================================================== + | CONSTRUCTOR + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Establishes a new instance of a with required dependencies. + /// + static ItemMetadata() { + _listTypes.Add(typeof(IEnumerable<>)); + _listTypes.Add(typeof(ICollection<>)); + _listTypes.Add(typeof(IList<>)); + } + + /*========================================================================================================================== + | CONSTRUCTOR + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Initializes a new instance of the class associated with a or instance. + /// + /// The of the or . + /// + /// The or associated with the . + /// + internal ItemMetadata(string name, ICustomAttributeProvider attributeProvider) { + + /*------------------------------------------------------------------------------------------------------------------------ + | Set Fields + \-----------------------------------------------------------------------------------------------------------------------*/ + _attributeProvider = attributeProvider; + + /*------------------------------------------------------------------------------------------------------------------------ + | Set Properties + \-----------------------------------------------------------------------------------------------------------------------*/ + Name = name; + + } + + /*========================================================================================================================== + | NAME + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + internal string Name { get; } + + /*========================================================================================================================== + | TYPE + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Gets the associated with this member. For properties and get methods, this is the return type. For + /// set methods, this is the type of the parameter. + /// + /// + /// Ideally, the would be provided as part of the constructor. + /// Unfortunately, however, the logic for setting this type varies based on whether it is a parameter, a methor, or a + /// property. As such, it makes more sense for this logic to be implemented in derived classes. To facilitate this, the + /// property is provided with an initter, which will automatically set , , and when it is set. If this is not done properly, dependency classes will + /// not work properly, and will likely fail. Since there are only two expected derived classes— and —this shouldn't be a problem. To help avoid this scenario, a is thrown with instructions in the unexpected case that is not set. + /// + public Type Type { + get { + return _type?? throw new ArgumentNullException( + nameof(Type), + $"This {nameof(Type)} property must be initialized by classes derived by {nameof(ItemMetadata)}" + ); + } + init { + _type = value; + IsNullable = !Type.IsValueType || Nullable.GetUnderlyingType(Type) != null; + IsList = isList(); + IsConvertible = AttributeValueConverter.IsConvertible(Type); + bool isList() + => typeof(IList).IsAssignableFrom(Type) || Type.IsGenericType && _listTypes.Contains(Type.GetGenericTypeDefinition()); + } + } + + /*========================================================================================================================== + | CONFIGURATION + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Gets a reference to the configuration settings for the current item, based on the annotations configured in the code. + /// + /// + /// The class identifies annotations used by the and the + /// . While it can be called on any , it is only expected to + /// offer a benefit for model classes intended for mapping. + /// + internal ItemConfiguration Configuration { + get { + if (_itemConfiguration is null) { + _itemConfiguration = new(this); + } + return _itemConfiguration; + } + } + + /*========================================================================================================================== + | IS NULLABLE? + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Determine if the member accepts null values. + /// + /// + /// If the is a reference type, then it will always accept null values; this doesn't detect C# 9.0 + /// nullable reference types. + /// + internal bool IsNullable { get; init; } + + /*========================================================================================================================== + | IS LIST? + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Determine if the member is a , , , or . + /// + internal bool IsList { get; init; } + + /*========================================================================================================================== + | IS CONVERTIBLE? + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Determine if the member is of a type that can be converted using the class. + /// + internal bool IsConvertible { get; init; } + + /*========================================================================================================================== + | MAYBE COMPATIBLE? + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Determine if the item corresponds to a member on the class (or derivative) associated with the + /// parent . + /// + /// + /// + /// The does not guarantee that a corresponding compatible property will be found. + /// First, it cannot know what the source will be, so it relies only evaluates topics who match the + /// naming convention (e.g., {ContentType}TopicViewModel maps to {ContentType}), and otherwise falls back + /// to ; if a model is mapped to a different derivative of , additional matches + /// may be available. Second, it evaluates whether the member is of a type that can be assigned to + /// the model member, or otherwise converted—i.e., that the member is a string, and the model + /// member is one of the . This provides a hint for avoiding type + /// checks in cases we don't expect to be compatible, while deferring to the caller to provide more sophisticated checks + /// based on the actual . + /// + /// + /// The property must be set from the , which is best positioned + /// to efficiently determine the corresponding attributes. To do this, it must rely on attributes accessed via the property. As a result, the must be set after the instance has been constructed, instead of during initialization like the other properties. + /// That isn't ideal, but isn't easy to avoid without making each aware of the parent . + /// + /// + internal bool MaybeCompatible { get; set; } + + /*========================================================================================================================== + | CUSTOM ATTRIBUTES + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Provides a cached list of custom attributes associated with member. + /// + internal List CustomAttributes { + get { + _customAttributes ??= _attributeProvider.GetCustomAttributes(true).OfType().ToList(); + return _customAttributes; + } + } + + } //Class +} //Namespace \ No newline at end of file diff --git a/OnTopic/Internal/Reflection/MemberAccessor.cs b/OnTopic/Internal/Reflection/MemberAccessor.cs new file mode 100644 index 00000000..a7ad9bcc --- /dev/null +++ b/OnTopic/Internal/Reflection/MemberAccessor.cs @@ -0,0 +1,458 @@ +/*============================================================================================================================== +| Author Ignia, LLC +| Client Ignia, LLC +| Project Topics Library +\=============================================================================================================================*/ +using System.ComponentModel.DataAnnotations; +using System.Reflection; + +namespace OnTopic.Internal.Reflection { + + /*============================================================================================================================ + | CLASS: MEMBER ACCESSOR + \---------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Provides metadata and accessor methods for getting and setting data, as appropriate, from properties and methods. + /// + /// + /// This provides comparatively low-level access. It doesn't make any attempt to validate that e.g. the value submitted is + /// compatible with the property or method parameter type being set, nor does it make any effort to provide explicit + /// conversions. Those capabilities will be handled by higher-level libraries. + /// + internal class MemberAccessor: ItemMetadata { + + /*========================================================================================================================== + | PRIVATE VARIABLES + \-------------------------------------------------------------------------------------------------------------------------*/ + private Action? _setter; + private Func? _getter; + + /*========================================================================================================================== + | CONSTRUCTOR + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Initializes a new instance of the class associated with a + /// instance. + /// + /// The associated with the . + internal MemberAccessor(MemberInfo memberInfo): base(memberInfo.Name, memberInfo) { + + /*------------------------------------------------------------------------------------------------------------------------ + | Validate parameters + \-----------------------------------------------------------------------------------------------------------------------*/ + Contract.Requires( + memberInfo is PropertyInfo or MethodInfo, + $"The {nameof(memberInfo)} parameter must be of type {nameof(PropertyInfo)} or {nameof(MethodInfo)}." + ); + + /*------------------------------------------------------------------------------------------------------------------------ + | Set Properties + \-----------------------------------------------------------------------------------------------------------------------*/ + MemberInfo = memberInfo; + MemberType = MemberInfo.MemberType; + Type = GetType(memberInfo); + + } + + /*========================================================================================================================== + | MEMBER INFO + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Provides access to the underlying upon which the is based. + /// + internal MemberInfo MemberInfo { get; } + + /*========================================================================================================================== + | MEMBER TYPE + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + internal MemberTypes MemberType { get; } + + /*========================================================================================================================== + | CAN READ? + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Determine if the member can be used to retrieve a value. + /// + /// + /// Unlike the the , the property can be set for either properties + /// or methods. To be readable by , a method must have a return type and no parameters. + /// + internal bool CanRead { get; private set; } + + /*========================================================================================================================== + | CAN WRITE? + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Determine if the member can be used to set a value. + /// + /// + /// Unlike the the , the property can be set for either + /// properties or methods. To be writable by , a method must be void and have exactly one + /// parameter. + /// + internal bool CanWrite { get; private set; } + + /*========================================================================================================================== + | METHOD: IS SETTABLE? + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Determines whether the current member is settable, either assuming the list of , + /// or provided a specific . + /// + /// The to evaluate against . + /// + /// Determines whether a fallback to is permitted. + /// + internal bool IsSettable(Type? sourceType = null, bool allowConversion = false) { + if (sourceType is not null && (sourceType == Type || Type.IsAssignableFrom(sourceType))) { + return true; + } + if (allowConversion) { + var isSourceConvertible = sourceType is null || AttributeValueConverter.IsConvertible(sourceType); + return IsConvertible && isSourceConvertible; + } + return false; + } + + /*========================================================================================================================== + | GET VALUE + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Retrieves the value of the given member from a object. + /// + /// + /// The method can retrieve values from either property getters or methods which accept no + /// parameters and have a return type. + /// + /// The object from which the member value should be retrieved. + /// The value of the member, if available. + internal object? GetValue(object source) { + + /*------------------------------------------------------------------------------------------------------------------------ + | Validate parameters + \-----------------------------------------------------------------------------------------------------------------------*/ + if (MemberInfo.DeclaringType != source.GetType() && !MemberInfo.DeclaringType.IsAssignableFrom(source.GetType())) { + throw new ArgumentException( + $"The {nameof(MemberAccessor)} for {MemberInfo.DeclaringType} cannot be used to access a member of {source.GetType()}", + nameof(source) + ); + } + + /*------------------------------------------------------------------------------------------------------------------------ + | Validate member type + \-----------------------------------------------------------------------------------------------------------------------*/ + if (!CanRead) { + return null; + } + + /*------------------------------------------------------------------------------------------------------------------------ + | Retrieve value + \-----------------------------------------------------------------------------------------------------------------------*/ + return Getter?.Invoke(source); + + } + + /*========================================================================================================================== + | SET VALUE + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Sets the value of the member on the object to the . + /// + /// + /// + /// The method can set values on either property setters or methods + /// which accept one parameter and don't have a return type. + /// + /// + /// The method makes no attempt to validate whether is compatible with the target property or method parameter type. If it is not compatible, the underlying + /// reflection library will throw an exception. It is expected that callers will validate the types before calling this + /// method. + /// + /// + /// The object on which the member should be set. + /// The value that the member should be set to. + /// + /// Determines whether a fallback to is permitted. + /// + internal void SetValue(object target, object? value, bool allowConversion = false) { + + /*------------------------------------------------------------------------------------------------------------------------ + | Validate parameters + \-----------------------------------------------------------------------------------------------------------------------*/ + if (MemberInfo.DeclaringType != target.GetType() && !MemberInfo.DeclaringType.IsAssignableFrom(target.GetType())) { + throw new ArgumentException( + $"The {nameof(MemberAccessor)} for {MemberInfo.DeclaringType} cannot be used to set a member of {target.GetType()}", + nameof(target) + ); + } + + /*------------------------------------------------------------------------------------------------------------------------ + | Validate value + \-----------------------------------------------------------------------------------------------------------------------*/ + var valueObject = value; + var sourceType = value?.GetType(); + + if (!CanWrite) { + return; + } + else if (value is null && !IsNullable) { + return; + } + else if (value is null || Type.IsAssignableFrom(sourceType)) { + //Proceed with conversion + } + else if (allowConversion && value is string) { + valueObject = AttributeValueConverter.Convert(value as string, Type); + } + + if (valueObject is null && !IsNullable) { + return; + } + + /*------------------------------------------------------------------------------------------------------------------------ + | Set value + \-----------------------------------------------------------------------------------------------------------------------*/ + Setter?.Invoke(target, valueObject); + + } + + /*========================================================================================================================== + | METHOD: VALIDATE + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Given a target DTO, will automatically identify any attributes that derive from and + /// ensure that their conditions are satisfied. + /// + /// The target DTO to validate the current property on. + internal void Validate(object target) { + foreach (ValidationAttribute validator in CustomAttributes.OfType()) { + validator.Validate(GetValue(target), Name); + } + } + + /*========================================================================================================================== + | EXCLUDED MEMBERS + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Provides a list of member names automatically generated by the compiler for record types, but which aren't + /// relevant to mapping and should be excluded. + /// + private static List ExcludedMembers { get; } = new List() { + "EqualityContract", + "GetHashCode", + "ToString", + "$" + }; + + + /*========================================================================================================================== + | IS VALID? + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Ensures that a given instance is compatible with the . + /// + /// + /// This is used by the to validate whether a should be created + /// for a given . For example, constructors, private members, and methods with more than one + /// parameter will return false. + /// + internal static Func IsValid => memberInfo => { + + /*------------------------------------------------------------------------------------------------------------------------ + | Exclude unsupported types and members + \-----------------------------------------------------------------------------------------------------------------------*/ + if (memberInfo.DeclaringType == typeof(object)) return false; + if (memberInfo is not MethodInfo and not PropertyInfo) return false; + if (memberInfo.Name.Contains("et_", StringComparison.Ordinal)) return false; + if (ExcludedMembers.Contains(memberInfo.Name)) return false; + + /*------------------------------------------------------------------------------------------------------------------------ + | Validate properties + \-----------------------------------------------------------------------------------------------------------------------*/ + if (memberInfo is PropertyInfo) return true; + + /*------------------------------------------------------------------------------------------------------------------------ + | Establish method variables + \-----------------------------------------------------------------------------------------------------------------------*/ + var methodInfo = (MethodInfo)memberInfo; + var parameters = methodInfo.GetParameters(); + + /*------------------------------------------------------------------------------------------------------------------------ + | Validate get methods + \-----------------------------------------------------------------------------------------------------------------------*/ + if (methodInfo.ReturnType != typeof(void)) { + return parameters.Length == 0; + } + + /*------------------------------------------------------------------------------------------------------------------------ + | Validate set methods + \-----------------------------------------------------------------------------------------------------------------------*/ + return parameters.Length == 1; + + }; + + /*========================================================================================================================== + | GET TYPE + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Given a reference, will evaluate features of the underlying and + /// use them to set the properties of this instance. + /// + /// + /// The method is private, and intended exclusively to break up the functionality + /// required by the constructor. + /// + /// The source that this instance is based on. + /// + private Type GetType(MemberInfo memberInfo) { + + /*------------------------------------------------------------------------------------------------------------------------ + | Set type as return type of property + \-----------------------------------------------------------------------------------------------------------------------*/ + if (memberInfo is PropertyInfo propertyInfo) { + CanRead = propertyInfo.CanRead; + CanWrite = propertyInfo.CanWrite; + return propertyInfo.PropertyType; + } + + /*------------------------------------------------------------------------------------------------------------------------ + | Validate method + \-----------------------------------------------------------------------------------------------------------------------*/ + var methodInfo = (MethodInfo)memberInfo; + var parameters = methodInfo.GetParameters(); + + /*------------------------------------------------------------------------------------------------------------------------ + | Set type as return type of method + \-----------------------------------------------------------------------------------------------------------------------*/ + if (methodInfo.ReturnType != typeof(void)) { + Contract.Assume( + parameters.Length == 0, + $"The '{memberInfo.Name}()' method must not expect any parameters if the return type is not void." + ); + CanRead = true; + return methodInfo.ReturnType; + } + + /*------------------------------------------------------------------------------------------------------------------------ + | Set type as first parameter of method + \-----------------------------------------------------------------------------------------------------------------------*/ + Contract.Assume( + parameters.Length == 1, + $"The '{memberInfo.Name}()' method must have one—and exactly one—parameter if the return type is void. This parameter " + + $"will be used as the value of the setter." + ); + + CanWrite = true; + return parameters[0].ParameterType; + + } + + /*========================================================================================================================== + | GETTER + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Provides a strongly typed delegate for accessing either a property getter or a method with a return type and no + /// parameters. + /// + /// + /// The value is cached alongside this instance of the to improve performance across multiple + /// calls. + /// + private Func? Getter { + get { + if (_getter is null) { + var delegateType = typeof(Func<,>).MakeGenericType(MemberInfo.DeclaringType, Type); + var delegateGetter = (Delegate?)null; + if (MemberInfo is PropertyInfo propertInfo) { + delegateGetter = propertInfo.GetGetMethod().CreateDelegate(delegateType); + } + else if (MemberInfo is MethodInfo methodInfo) { + delegateGetter = methodInfo.CreateDelegate(delegateType); + } + var getterWithTypes = GetterDelegateMethod.MakeGenericMethod(MemberInfo.DeclaringType, Type); + _getter = (Func)getterWithTypes.Invoke(null, new[] { delegateGetter }); + } + return _getter; + } + } + + /*========================================================================================================================== + | SETTER + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Provides a strongly typed delegate for accessing either a property setter or a void method with a single parameter. + /// + /// + /// The value is cached alongside this instance of the to improve performance across multiple + /// calls. + /// + private Action? Setter { + get { + if (_setter is null) { + var delegateType = typeof(Action<,>).MakeGenericType(MemberInfo.DeclaringType, Type); + var delegateSetter = (Delegate?)null; + if (MemberInfo is PropertyInfo propertInfo) { + delegateSetter = propertInfo.GetSetMethod().CreateDelegate(delegateType); + } + else if (MemberInfo is MethodInfo methodInfo) { + delegateSetter = methodInfo.CreateDelegate(delegateType); + } + var setterWithTypes = SetterDelegateMethod.MakeGenericMethod(MemberInfo.DeclaringType, Type); + _setter = (Action)setterWithTypes.Invoke(null, new[] { delegateSetter }); + } + return _setter; + } + } + + /*========================================================================================================================== + | CREATE SETTER DELEGATE SIGNATURE + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Establishes a generic signature for a setter action. + /// + /// The object containing the member to set the value on. + /// The value that the member should be set to. + /// The strongly typed used to set the value. + /// + /// A loosely typed that wraps the strongly typed . + /// + private static Action CreateSetterDelegateSignature(Action deleg) + => (instance, value) => deleg((TClass)instance, (TValue?)value); + + /*========================================================================================================================== + | CREATE GETTER DELEGATE SIGNATURE + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Establishes a generic signature for a getter action. + /// + /// The object containing the member to retrieve. + /// The return value of the member. + /// The strongly typed used to retrieve the value. + /// + /// A loosely typed that wraps the strongly typed . + /// + private static Func CreateGetterDelegateSignature(Func deleg) + => instance => deleg((TClass)instance); + + /*========================================================================================================================== + | SETTER DELEGATE METHOD REFERENCES + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Provides a reference to the method. + /// + private static readonly MethodInfo SetterDelegateMethod = + typeof(MemberAccessor).GetMethod(nameof(CreateSetterDelegateSignature), BindingFlags.NonPublic | BindingFlags.Static)!; + + /*========================================================================================================================== + | GETTER DELEGATE METHOD REFERENCES + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Provides a reference to the method. + /// + private static readonly MethodInfo GetterDelegateMethod = + typeof(MemberAccessor).GetMethod(nameof(CreateGetterDelegateSignature), BindingFlags.NonPublic | BindingFlags.Static)!; + + } //Class +} //Namespace \ No newline at end of file diff --git a/OnTopic/Internal/Reflection/MemberDispatcher.cs b/OnTopic/Internal/Reflection/MemberDispatcher.cs deleted file mode 100644 index 7f070224..00000000 --- a/OnTopic/Internal/Reflection/MemberDispatcher.cs +++ /dev/null @@ -1,348 +0,0 @@ -/*============================================================================================================================== -| Author Ignia, LLC -| Client Ignia, LLC -| Project Topics Library -\=============================================================================================================================*/ -using System; -using System.Collections.ObjectModel; -using System.Linq; -using System.Reflection; -using OnTopic.Attributes; -using OnTopic.Internal.Diagnostics; - -namespace OnTopic.Internal.Reflection { - - /*============================================================================================================================ - | CLASS: MEMBER DISPATCHER - \---------------------------------------------------------------------------------------------------------------------------*/ - /// - /// The provides methods that simplify late-binding access to properties and methods. - /// - /// - /// - /// The allows properties and members to be looked up and called based on string - /// representations of both the member names as well as, optionally, the values. String values can be deserialized into - /// various value formats supported by . - /// - /// - /// For retrieving values, the typical workflow is for a caller to check either or , followed by or to retrieve the value. - /// - /// - /// For setting values, the typical workflow is for a caller to check either or , followed by or to retrieve the value. In these - /// scenarios, the will attempt to deserialize the value parameter from to the type expected by the corresponding property or method. Typically, this will be a , , , or . - /// - /// - /// Alternatively, setters can call or , in which case the final value parameter will be set the target property, or passed - /// as the parameter of the method without any attempt to convert it. Obviously, this requires that the target type be - /// assignable from the value object. - /// - /// - /// The is an internal service intended to meet the specific needs of OnTopic, and comes - /// with certain limitations. It only supports setting values of methods with a single parameter, which is assumed to - /// correspond to the value parameter. It will only operate against the first overload of a method, and/or the most - /// derived version of a member. - /// - /// - internal class MemberDispatcher { - - /*========================================================================================================================== - | PRIVATE VARIABLES - \-------------------------------------------------------------------------------------------------------------------------*/ - private readonly Type? _attributeFlag; - private readonly TypeMemberInfoCollection _memberInfoCache = new(); - - /*========================================================================================================================== - | CONSTRUCTOR (STATIC) - \-------------------------------------------------------------------------------------------------------------------------*/ - /// - /// Initializes static properties on . - /// - static MemberDispatcher() {} - - /*========================================================================================================================== - | CONSTRUCTOR - \-------------------------------------------------------------------------------------------------------------------------*/ - /// - /// Initializes a new instance of the class. - /// - /// - /// An optional which properties must have defined to be considered writable. - /// - internal MemberDispatcher(Type? attributeFlag = null) : base() { - _attributeFlag = attributeFlag; - } - - /*========================================================================================================================== - | METHOD: GET MEMBERS {T} - \-------------------------------------------------------------------------------------------------------------------------*/ - /// - /// Returns a collection of objects associated with a specific type. - /// - /// - /// If the collection cannot be found locally, it will be created. - /// - /// The type for which the members should be retrieved. - internal MemberInfoCollection GetMembers(Type type) where T : MemberInfo => _memberInfoCache.GetMembers(type); - - /*========================================================================================================================== - | METHOD: GET MEMBER {T} - \-------------------------------------------------------------------------------------------------------------------------*/ - /// - /// Used reflection to identify a local member by a given name, and returns the associated - /// instance. - /// - internal T? GetMember(Type type, string name) where T : MemberInfo => _memberInfoCache.GetMember(type, name); - - /*========================================================================================================================== - | METHOD: HAS SETTABLE PROPERTY - \-------------------------------------------------------------------------------------------------------------------------*/ - /// - /// Used reflection to identify if a local property is available and settable. - /// - /// - /// Will return false if the property is not available. - /// - /// The on which the property is defined. - /// The name of the property to assess. - /// Optional, the expected. - internal bool HasSettableProperty(Type type, string name, Type? targetType = null) { - var property = GetMember(type, name); - return ( - property is not null and { CanWrite: true } && - IsSettableType(property.PropertyType, targetType) && - (_attributeFlag is null || Attribute.IsDefined(property, _attributeFlag)) - ); - } - - /*========================================================================================================================== - | METHOD: SET PROPERTY VALUE - \-------------------------------------------------------------------------------------------------------------------------*/ - /// - /// Uses reflection to call a property, assuming that it is a) writable, and b) of type , , or , or is otherwise compatible with the type. - /// - /// The object on which the property is defined. - /// The name of the property to assess. - /// The value to set on the property. - internal void SetPropertyValue(object target, string name, object? value) { - - Contract.Requires(target, nameof(target)); - Contract.Requires(name, nameof(name)); - - var property = GetMember(target.GetType(), name); - - Contract.Assume(property, $"The {name} property could not be retrieved."); - - var valueObject = value; - - if (valueObject is string) { - valueObject = AttributeValueConverter.Convert(value as string, property.PropertyType); - } - - property.SetValue(target, valueObject); - - } - - /*========================================================================================================================== - | METHOD: HAS GETTABLE PROPERTY - \-------------------------------------------------------------------------------------------------------------------------*/ - /// - /// Used reflection to identify if a local property is available and gettable. - /// - /// - /// Will return false if the property is not available. - /// - /// The on which the property is defined. - /// The name of the property to assess. - /// Optional, the expected. - internal bool HasGettableProperty(Type type, string name, Type? targetType = null) { - var property = GetMember(type, name); - return ( - property is not null and { CanRead: true } && - IsSettableType(property.PropertyType, targetType) && - (_attributeFlag is null || Attribute.IsDefined(property, _attributeFlag)) - ); - } - - /*========================================================================================================================== - | METHOD: GET PROPERTY VALUE - \-------------------------------------------------------------------------------------------------------------------------*/ - /// - /// Uses reflection to call a property, assuming that it is a) readable, and b) of type , - /// , or . - /// - /// The object instance on which the property is defined. - /// The name of the property to assess. - /// Optional, the expected. - internal object? GetPropertyValue(object target, string name, Type? targetType = null) { - - /*------------------------------------------------------------------------------------------------------------------------ - | Validate parameters - \-----------------------------------------------------------------------------------------------------------------------*/ - Contract.Requires(target, nameof(target)); - Contract.Requires(name, nameof(name)); - - /*------------------------------------------------------------------------------------------------------------------------ - | Validate member type - \-----------------------------------------------------------------------------------------------------------------------*/ - if (!HasGettableProperty(target.GetType(), name, targetType)) { - return null; - } - - /*------------------------------------------------------------------------------------------------------------------------ - | Retrieve value - \-----------------------------------------------------------------------------------------------------------------------*/ - var property = GetMember(target.GetType(), name); - - Contract.Assume(property, $"The {name} property could not be retrieved."); - - return property.GetValue(target); - - } - - /*========================================================================================================================== - | METHOD: HAS SETTABLE METHOD - \-------------------------------------------------------------------------------------------------------------------------*/ - /// - /// Used reflection to identify if a local method is available and settable. - /// - /// - /// Will return false if the method is not available. Methods are only considered settable if they have one parameter of - /// a settable type. Be aware that this will return false if the method has additional parameters, even if those - /// additional parameters are optional. - /// - /// The on which the method is defined. - /// The name of the method to assess. - /// Optional, the expected. - internal bool HasSettableMethod(Type type, string name, Type? targetType = null) { - var method = GetMember(type, name); - return ( - method is not null && - method.GetParameters().Length is 1 && - IsSettableType(method.GetParameters().First().ParameterType, targetType) && - (_attributeFlag is null || Attribute.IsDefined(method, _attributeFlag)) - ); - } - - /*========================================================================================================================== - | METHOD: SET METHOD VALUE - \-------------------------------------------------------------------------------------------------------------------------*/ - /// - /// Uses reflection to call a method, assuming that the parameter value is compatible with the - /// type. - /// - /// - /// Be aware that this will only succeed if the method has a single parameter of a settable type. If additional parameters - /// are present it will return false, even if those additional parameters are optional. - /// - /// The object instance on which the method is defined. - /// The name of the method to assess. - /// The value to set the method to. - internal void SetMethodValue(object target, string name, object? value) { - - /*------------------------------------------------------------------------------------------------------------------------ - | Validate parameters - \-----------------------------------------------------------------------------------------------------------------------*/ - Contract.Requires(target, nameof(target)); - Contract.Requires(name, nameof(name)); - - /*------------------------------------------------------------------------------------------------------------------------ - | Set value - \-----------------------------------------------------------------------------------------------------------------------*/ - var method = GetMember(target.GetType(), name); - - Contract.Assume(method, $"The {name}() method could not be retrieved."); - - var valueObject = value; - - if (valueObject is string) { - valueObject = AttributeValueConverter.Convert(valueObject as string, method.GetParameters().First().ParameterType); - } - - method.Invoke(target, new object?[] { valueObject }); - - } - - /*========================================================================================================================== - | METHOD: HAS GETTABLE METHOD - \-------------------------------------------------------------------------------------------------------------------------*/ - /// - /// Used reflection to identify if a local method is available and gettable. - /// - /// - /// Will return false if the method is not available. Methods are only considered gettable if they have no parameters and - /// their return value is a settable type. - /// - /// The on which the method is defined. - /// The name of the method to assess. - /// Optional, the expected. - internal bool HasGettableMethod(Type type, string name, Type? targetType = null) { - var method = GetMember(type, name); - return ( - method is not null && - !method.GetParameters().Any() && - IsSettableType(method.ReturnType, targetType) && - (_attributeFlag is null || Attribute.IsDefined(method, _attributeFlag)) - ); - } - - /*========================================================================================================================== - | METHOD: GET METHOD VALUE - \-------------------------------------------------------------------------------------------------------------------------*/ - /// - /// Uses reflection to call a method, assuming that it has no parameters. - /// - /// The object instance on which the method is defined. - /// The name of the method to assess. - /// Optional, the expected. - internal object? GetMethodValue(object target, string name, Type? targetType = null) { - - /*------------------------------------------------------------------------------------------------------------------------ - | Validate parameters - \-----------------------------------------------------------------------------------------------------------------------*/ - Contract.Requires(target, nameof(target)); - Contract.Requires(name, nameof(name)); - - /*------------------------------------------------------------------------------------------------------------------------ - | Validate member type - \-----------------------------------------------------------------------------------------------------------------------*/ - if (!HasGettableMethod(target.GetType(), name, targetType)) { - return null; - } - - /*------------------------------------------------------------------------------------------------------------------------ - | Retrieve value - \-----------------------------------------------------------------------------------------------------------------------*/ - var method = GetMember(target.GetType(), name); - - Contract.Assume(method, $"The method '{name}' could not be found on the '{target.GetType()}' class."); - - return method.Invoke(target, Array.Empty()); - - } - - /*========================================================================================================================== - | METHOD: IS SETTABLE TYPE? - \-------------------------------------------------------------------------------------------------------------------------*/ - /// - /// Determines whether a given type is settable, either assuming the list of , or - /// provided a specific . - /// - private static bool IsSettableType(Type sourceType, Type? targetType = null) { - - if (targetType is not null) { - return sourceType.IsAssignableFrom(targetType); - } - return AttributeValueConverter.IsConvertible(sourceType); - - } - - } //Class -} //Namespace \ No newline at end of file diff --git a/OnTopic/Internal/Reflection/MemberInfoCollection.cs b/OnTopic/Internal/Reflection/MemberInfoCollection.cs deleted file mode 100644 index 9801aa57..00000000 --- a/OnTopic/Internal/Reflection/MemberInfoCollection.cs +++ /dev/null @@ -1,45 +0,0 @@ -/*============================================================================================================================== -| Author Ignia, LLC -| Client Ignia, LLC -| Project Topics Library -\=============================================================================================================================*/ -using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.Reflection; - -namespace OnTopic.Internal.Reflection { - - /*============================================================================================================================ - | CLASS: MEMBER INFO COLLECTION - \---------------------------------------------------------------------------------------------------------------------------*/ - /// - /// Provides keyed access to a collection of instances. - /// - internal class MemberInfoCollection : MemberInfoCollection { - - /*========================================================================================================================== - | CONSTRUCTOR - \-------------------------------------------------------------------------------------------------------------------------*/ - /// - /// Initializes a new instance of the class associated with a - /// name. - /// - /// The associated with the collection. - internal MemberInfoCollection(Type type) : base(type) { - } - - /// - /// Initializes a new instance of the class associated with a - /// name and prepopulates it with a predetermined set of instances. - /// - /// The associated with the collection. - /// - /// An of instances to populate the collection. - /// - [ExcludeFromCodeCoverage] - internal MemberInfoCollection(Type type, IEnumerable members) : base(type, members) { - } - - } //Class -} //Namespace \ No newline at end of file diff --git a/OnTopic/Internal/Reflection/MemberInfoCollection{T}.cs b/OnTopic/Internal/Reflection/MemberInfoCollection{T}.cs deleted file mode 100644 index d3c244ee..00000000 --- a/OnTopic/Internal/Reflection/MemberInfoCollection{T}.cs +++ /dev/null @@ -1,121 +0,0 @@ -/*============================================================================================================================== -| Author Ignia, LLC -| Client Ignia, LLC -| Project Topics Library -\=============================================================================================================================*/ -using System; -using System.Collections.Generic; -using System.Collections.ObjectModel; -using System.Diagnostics.CodeAnalysis; -using System.Linq; -using System.Reflection; -using OnTopic.Internal.Diagnostics; - -namespace OnTopic.Internal.Reflection { - - /*============================================================================================================================ - | CLASS: MEMBER INFO COLLECTION {T} - \---------------------------------------------------------------------------------------------------------------------------*/ - /// - /// Provides keyed access to a collection of instances. - /// - internal class MemberInfoCollection : KeyedCollection where T : MemberInfo { - - /*========================================================================================================================== - | CONSTRUCTOR - \-------------------------------------------------------------------------------------------------------------------------*/ - /// - /// Initializes a new instance of the class associated with a - /// name. - /// - /// The associated with the collection. - internal MemberInfoCollection(Type type) : base(StringComparer.OrdinalIgnoreCase) { - Contract.Requires(type); - Type = type; - foreach ( - var member - in type.GetMembers( - BindingFlags.Instance | - BindingFlags.FlattenHierarchy | - BindingFlags.NonPublic | - BindingFlags.Public - ).Where(m => typeof(T).IsAssignableFrom(m.GetType())) - ) { - if (!Contains(member.Name)) { - Add((T)member); - } - } - } - - /// - /// Initializes a new instance of the class associated with a - /// name and prepopulates it with a predetermined set of instances. - /// - /// The associated with the collection. - /// - /// An of instances to populate the collection. - /// - internal MemberInfoCollection(Type type, IEnumerable members) : base(StringComparer.OrdinalIgnoreCase) { - Contract.Requires(type); - Contract.Requires(members); - Type = type; - foreach (var member in members) { - Add(member); - } - } - - /*========================================================================================================================== - | OVERRIDE: INSERT ITEM - \-------------------------------------------------------------------------------------------------------------------------*/ - /// Fires any time an item is added to the collection. - /// - /// Compared to the base implementation, will throw a specific error if a duplicate key is - /// inserted. This conveniently provides the name of the and the 's , so it's clear what is being duplicated. - /// - /// The zero-based index at which should be inserted. - /// The instance to insert. - /// The Type '{Type.Name}' already contains the MemberInfo '{item.Name}' - [ExcludeFromCodeCoverage] - protected override sealed void InsertItem(int index, T item) { - - /*------------------------------------------------------------------------------------------------------------------------ - | Validate parameters - \-----------------------------------------------------------------------------------------------------------------------*/ - Contract.Requires(item, nameof(item)); - - /*------------------------------------------------------------------------------------------------------------------------ - | Insert Item - \-----------------------------------------------------------------------------------------------------------------------*/ - if (!Contains(item.Name)) { - base.InsertItem(index, item); - } - else { - throw new ArgumentException($"The Type '{Type.Name}' already contains the MemberInfo '{item.Name}'", nameof(item)); - } - } - - /*========================================================================================================================== - | PROPERTY: TYPE - \-------------------------------------------------------------------------------------------------------------------------*/ - /// - /// Returns the type associated with this collection. - /// - internal Type Type { get; } - - /*========================================================================================================================== - | OVERRIDE: GET KEY FOR ITEM - \-------------------------------------------------------------------------------------------------------------------------*/ - /// - /// Method must be overridden for the EntityCollection to extract the keys from the items. - /// - /// The object from which to extract the key. - /// The key for the specified collection item. - [ExcludeFromCodeCoverage] - protected override sealed string GetKeyForItem(T item) { - Contract.Requires(item, "The item must be available in order to derive its key."); - return item.Name; - } - - } //Class -} //Namespace \ No newline at end of file diff --git a/OnTopic/Internal/Reflection/ParameterMetadata.cs b/OnTopic/Internal/Reflection/ParameterMetadata.cs new file mode 100644 index 00000000..54700560 --- /dev/null +++ b/OnTopic/Internal/Reflection/ParameterMetadata.cs @@ -0,0 +1,40 @@ +/*============================================================================================================================== +| Author Ignia, LLC +| Client Ignia, LLC +| Project Topics Library +\=============================================================================================================================*/ +using System.Reflection; + +namespace OnTopic.Internal.Reflection { + + /*============================================================================================================================ + | CLASS: PARAMETER METADATA + \---------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Provides metadata associated with a given parameter. + /// + internal class ParameterMetadata: ItemMetadata { + + /*========================================================================================================================== + | CONSTRUCTOR + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Initializes a new instance of the class associated with a + /// instance. + /// + /// The associated with this instance. + internal ParameterMetadata(ParameterInfo parameterInfo): base (parameterInfo.Name, parameterInfo) { + ParameterInfo = parameterInfo; + Type = parameterInfo.ParameterType; + } + + /*========================================================================================================================== + | PARAMETER INFO + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Gets the associated with this instance of . + /// + public ParameterInfo ParameterInfo { get; } + + } //Class +} //Namespace \ No newline at end of file diff --git a/OnTopic/Internal/Reflection/TopicPropertyDispatcher{TItem,TValue,TAttributeType}.cs b/OnTopic/Internal/Reflection/TopicPropertyDispatcher{TItem,TValue,TAttributeType}.cs index f4195de9..cab4eb62 100644 --- a/OnTopic/Internal/Reflection/TopicPropertyDispatcher{TItem,TValue,TAttributeType}.cs +++ b/OnTopic/Internal/Reflection/TopicPropertyDispatcher{TItem,TValue,TAttributeType}.cs @@ -3,12 +3,8 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ -using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; using System.Reflection; using System.Runtime.ExceptionServices; -using OnTopic.Attributes; using OnTopic.Collections.Specialized; using OnTopic.Associations; @@ -90,11 +86,6 @@ internal class TopicPropertyDispatcher where TValue: class { - /*========================================================================================================================== - | STATIC VARIABLES - \-------------------------------------------------------------------------------------------------------------------------*/ - static readonly MemberDispatcher _typeCache = new(typeof(TAttributeType)); - /*========================================================================================================================== | PRIVATE VARIABLES \-------------------------------------------------------------------------------------------------------------------------*/ @@ -187,8 +178,8 @@ internal bool Register(string itemKey, TItem? initialValue) { type = typeof(TValue); } if ( - _typeCache.HasSettableProperty(_associatedTopic.GetType(), itemKey, type) && - !PropertyCache.ContainsKey(itemKey) + !PropertyCache.ContainsKey(itemKey) && + TypeAccessorCache.GetTypeAccessor(_associatedTopic.GetType()).HasSettableProperty(itemKey, type) ) { PropertyCache.Add(itemKey, initialValue); return true; @@ -258,7 +249,8 @@ internal bool Enforce(string itemKey, TItem? initialObject) { } else if (Register(itemKey, initialObject)) { try { - _typeCache.SetPropertyValue(_associatedTopic, itemKey, initialObject?.Value); + var typeAccessor = TypeAccessorCache.GetTypeAccessor(_associatedTopic.GetType()); + typeAccessor.SetPropertyValue(_associatedTopic, itemKey, initialObject?.Value, true); } catch (TargetInvocationException ex) { if (PropertyCache.ContainsKey(itemKey)) { diff --git a/OnTopic/Internal/Reflection/TypeAccessor.cs b/OnTopic/Internal/Reflection/TypeAccessor.cs new file mode 100644 index 00000000..ca25a816 --- /dev/null +++ b/OnTopic/Internal/Reflection/TypeAccessor.cs @@ -0,0 +1,455 @@ +/*============================================================================================================================== +| Author Ignia, LLC +| Client Ignia, LLC +| Project Topics Library +\=============================================================================================================================*/ +using System.Reflection; + +namespace OnTopic.Internal.Reflection { + + /*============================================================================================================================ + | CLASS: MEMBER ACCESSOR + \---------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Provides access to a collection of instances related to a specific in + /// order to simplify common access scenarios for properties and methods. + /// + /// + /// + /// For retrieving values, the typical workflow is for a caller to check either or , followed by or to retrieve the value. + /// + /// + /// For setting values, the typical workflow is for a caller to check either or , followed by or to retrieve the value. In these + /// scenarios, the will attempt to deserialize the value parameter from to the type expected by the corresponding property or method. Typically, this will be a , + /// , , or . + /// + /// + /// Alternatively, setters can call or , in which case the final value parameter will be set the + /// target property, or passed as the parameter of the method without any attempt to convert it. Obviously, this requires + /// that the target type be assignable from the value object. + /// + /// + /// The is an internal service intended to meet the specific needs of OnTopic, and comes with + /// certain limitations. It only supports setting values of methods with a single parameter, which is assumed to + /// correspond to the value parameter. It will only operate against the first overload of a method, and/or the most + /// derived version of a member. + /// + /// + internal class TypeAccessor { + + /*========================================================================================================================== + | PRIVATE VARIABLES + \-------------------------------------------------------------------------------------------------------------------------*/ + private readonly Dictionary _members; + + /*========================================================================================================================== + | CONSTRUCTOR + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Initializes a new instance of a based on a given . + /// + /// + /// The constructor automatically identifies each supported on the + /// and adds an associated to the of each. + /// + /// + internal TypeAccessor(Type type) { + + /*------------------------------------------------------------------------------------------------------------------------ + | Initialize fields, properties + \-----------------------------------------------------------------------------------------------------------------------*/ + _members = new(StringComparer.OrdinalIgnoreCase); + Type = type; + + /*------------------------------------------------------------------------------------------------------------------------ + | Get members from type + \-----------------------------------------------------------------------------------------------------------------------*/ + var members = type.GetMembers( + BindingFlags.Instance | + BindingFlags.FlattenHierarchy | + BindingFlags.NonPublic | + BindingFlags.Public + ); + foreach (var member in members.Where(t => MemberAccessor.IsValid(t))) { + if (!_members.ContainsKey(member.Name)) { + _members.Add(member.Name, new(member)); + } + } + + /*------------------------------------------------------------------------------------------------------------------------ + | Get parameters from primary constructor + \-----------------------------------------------------------------------------------------------------------------------*/ + foreach (var parameter in GetPrimaryConstructor().GetParameters()) { + ConstructorParameters.Add(new(parameter)); + } + + /*------------------------------------------------------------------------------------------------------------------------ + | Identify expected topic, and set MaybeCompatible if a corresponding property exists + \-----------------------------------------------------------------------------------------------------------------------*/ + SetItemCompatibility(); + + } + + /*========================================================================================================================== + | TYPE + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Gets the return type of a getter or the argument type of a setter. + /// + internal Type Type { get; } + + /*========================================================================================================================== + | CONSTRUCTOR PARAMETERS + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Gets a list of instances derived from the primary constructor of the associated with this . + /// + internal List ConstructorParameters { get; } = new(); + + /*========================================================================================================================== + | GET MEMBERS + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Retrieves a list of properties and methods supported by the . + /// + /// Optionally filters the list of members by a list. + /// A list of instances. + internal List GetMembers(MemberTypes memberTypes = MemberTypes.All) => + _members.Values.Where(m => memberTypes == MemberTypes.All || memberTypes.HasFlag(m.MemberType)).ToList(); + + /// + /// Retrieves a list of properties and methods as objects, instead of s. + /// + /// A list of instances. + internal IEnumerable GetMembers() where T : MemberInfo + => GetMembers().Select(m => m.MemberInfo).Where(t => t is T).Cast(); + + /*========================================================================================================================== + | GET MEMBER + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Retrieves a property or method supported by the by . + /// + /// The name of the member to retrieve, derived from . + /// A instance for getting or setting values on a given member. + internal MemberAccessor? GetMember(string memberName) => _members.GetValueOrDefault(memberName); + + /*========================================================================================================================== + | METHOD: GET PRIMARY CONSTRUCTOR + \-------------------------------------------------------------------------------------------------------------------------*/ + internal ConstructorInfo GetPrimaryConstructor() => + Type.GetConstructors( + BindingFlags.Instance | + BindingFlags.FlattenHierarchy | + BindingFlags.Public + ).FirstOrDefault(); + + /*========================================================================================================================== + | HAS GETTER? + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Determines if a with a getter exists for a member with the given . + /// + /// The name of the member to assess, derived from . + /// True if a gettable member exists; otherwise false. + internal bool HasGetter(string memberName) => GetMember(memberName)?.CanRead ?? false; + + /*========================================================================================================================== + | METHOD: HAS GETTABLE PROPERTY + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Used reflection to identify if a local property is available and gettable. + /// + /// + /// Will return false if the property is not available. + /// + /// The name of the property to assess, derived from . + /// Optional, the expected. + /// Optional, the expected on the property. + internal bool HasGettableProperty(string propertyName, Type? targetType = null, Type? attributeFlag = null) { + var property = GetMember(propertyName); + return ( + property is not null and { CanRead: true, MemberType: MemberTypes.Property } && + property.IsSettable(targetType, true) && + (attributeFlag is null || Attribute.IsDefined(property.MemberInfo, attributeFlag)) + ); + } + + /// + /// The expected on the property. + internal bool HasGettableProperty(string propertyName, Type? targetType = null) where T : Attribute + => HasGettableProperty(propertyName, targetType, typeof(T)); + + /*========================================================================================================================== + | METHOD: HAS GETTABLE METHOD + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Used reflection to identify if a local method is available and gettable. + /// + /// + /// Will return false if the method is not available. Methods are only considered gettable if they have no parameters and + /// their return value is a settable type. + /// + /// The name of the method to assess, derived from . + /// Optional, the expected. + /// Optional, the expected on the property. + internal bool HasGettableMethod(string methodName, Type? targetType = null, Type? attributeFlag = null) { + var method = GetMember(methodName); + return ( + method is not null and { CanRead: true, MemberType: MemberTypes.Method } && + method.IsSettable(targetType, true) && + (attributeFlag is null || Attribute.IsDefined(method.MemberInfo, attributeFlag)) + ); + } + + /// + /// The expected on the property. + internal bool HasGettableMethod(string name, Type? targetType = null) where T : Attribute + => HasGettableMethod(name, targetType, typeof(T)); + + /*========================================================================================================================== + | GET VALUE + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Retrieves the value from a member named from the supplied + /// object. + /// + /// The instance from which the value should be retrieved. + /// The name of the member to retrieve, derived from . + /// The value returned from the member. + internal object? GetValue(object source, string memberName) { + + /*------------------------------------------------------------------------------------------------------------------------ + | Retrieve member + \-----------------------------------------------------------------------------------------------------------------------*/ + var member = GetMember(memberName); + + if (member is null) { + return null; + } + + /*------------------------------------------------------------------------------------------------------------------------ + | Retrieve value + \-----------------------------------------------------------------------------------------------------------------------*/ + return member.GetValue(source); + + } + + /*========================================================================================================================== + | METHOD: GET PROPERTY VALUE + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Uses reflection to call a property, assuming that it is a) readable, and b) of type , + /// , or . + /// + /// The object instance on which the property is defined. + /// The name of the property to retrieve, derived from . + internal object? GetPropertyValue(object source, string propertyName) => GetValue(source, propertyName); + + /*========================================================================================================================== + | METHOD: GET METHOD VALUE + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Uses reflection to call a method, assuming that it has no parameters. + /// + /// The object instance on which the method is defined. + /// The name of the method to retrieve, derived from . + internal object? GetMethodValue(object source, string methodName) => GetValue(source, methodName); + + /*========================================================================================================================== + | HAS SETTER? + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Determines if a with a setter exists for a member with the given . + /// + /// The name of the member to assess, derived from . + /// True if a settable member exists; otherwise false. + internal bool HasSetter(string memberName) => GetMember(memberName)?.CanWrite ?? false; + + /*========================================================================================================================== + | METHOD: HAS SETTABLE PROPERTY + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Used reflection to identify if a local property is available and settable. + /// + /// + /// Will return false if the property is not available. + /// + /// The name of the property to assess, derived from . + /// Optional, the expected. + /// Optional, the expected on the property. + internal bool HasSettableProperty(string propertyName, Type? targetType = null, Type? attributeFlag = null) { + var property = GetMember(propertyName); + return ( + property is not null and { CanWrite: true, MemberType: MemberTypes.Property } && + property.IsSettable(targetType, true) && + (attributeFlag is null || Attribute.IsDefined(property.MemberInfo as PropertyInfo, attributeFlag)) + ); + } + + /// + /// The expected on the property. + internal bool HasSettableProperty(string propertyName, Type? targetType = null) where T : Attribute + => HasSettableProperty(propertyName, targetType, typeof(T)); + + /*========================================================================================================================== + | METHOD: HAS SETTABLE METHOD + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Used reflection to identify if a local method is available and settable. + /// + /// + /// Will return false if the method is not available. Methods are only considered settable if they have one parameter of + /// a settable type. Be aware that this will return false if the method has additional parameters, even if those + /// additional parameters are optional. + /// + /// The name of the method to assess, derived from . + /// Optional, the expected. + /// Optional, the expected on the property. + internal bool HasSettableMethod(string methodName, Type? targetType = null, Type? attributeFlag = null) { + var method = GetMember(methodName); + return ( + method is not null and { CanWrite: true, MemberType: MemberTypes.Method } && + method.IsSettable(targetType, true) && + (attributeFlag is null || Attribute.IsDefined(method.MemberInfo, attributeFlag)) + ); + } + + /// + /// The expected on the property. + internal bool HasSettableMethod(string methodName, Type? targetType = null) where T : Attribute + => HasSettableMethod(methodName, targetType, typeof(T)); + + /*========================================================================================================================== + | SET VALUE + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Sets the value of a member named on the supplied object. + /// + /// The instance on which the value should be set. + /// The name of the member to set, derived from . + /// The value to set the member to. + /// + /// Determines whether a fallback to is permitted. + /// + internal void SetValue(object target, string memberName, object? value, bool allowConversion = false) { + + /*------------------------------------------------------------------------------------------------------------------------ + | Validate dependencies + \-----------------------------------------------------------------------------------------------------------------------*/ + var member = GetMember(memberName); + + Contract.Assume(member, $"The {memberName} property could not be retrieved."); + + /*------------------------------------------------------------------------------------------------------------------------ + | Set value + \-----------------------------------------------------------------------------------------------------------------------*/ + member.SetValue(target, value, allowConversion); + + } + + /*========================================================================================================================== + | METHOD: SET PROPERTY VALUE + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Uses reflection to call a property, assuming that it is a) writable, and b) of type , , or , or is otherwise compatible with the type. + /// + /// The object on which the property is defined. + /// The name of the property to set, derived from . + /// The value to set on the property. + /// + /// Determines whether a fallback to is permitted. + /// + internal void SetPropertyValue(object target, string propertyName, object? value, bool allowConversion = false) + => SetValue(target, propertyName, value, allowConversion); + + /*========================================================================================================================== + | METHOD: SET METHOD VALUE + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Uses reflection to call a method, assuming that the parameter value is compatible with the + /// type. + /// + /// + /// Be aware that this will only succeed if the method has a single parameter of a settable type. If additional parameters + /// are present it will return false, even if those additional parameters are optional. + /// + /// The object instance on which the method is defined. + /// The name of the method to set, derived from . + /// The value to set the method to. + /// + /// Determines whether a fallback to is permitted. + /// + internal void SetMethodValue(object target, string methodName, object? value, bool allowConversion = false) + => SetValue(target, methodName, value, allowConversion); + + /*========================================================================================================================== + | METHOD: SET ITEM COMPATIBILITY + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Determines if each member corresponds to a compatible or convertible member of a corresponding . + /// + /// + /// The method applies basic assumptions to identify the corresponding and whether or not any matching parameters or members are compatible with members of that . See for more details. + /// + private void SetItemCompatibility() { + + /*------------------------------------------------------------------------------------------------------------------------ + | Only attempt to detect compatibility for model types, not for topics. + >------------------------------------------------------------------------------------------------------------------------- + | We expect mapping to topics to typically use e.g. Attributes or References, which will automatically marshal through + | properties if appropriate. + \-----------------------------------------------------------------------------------------------------------------------*/ + if (typeof(Topic).IsAssignableFrom(Type)) { + return; + } + + /*------------------------------------------------------------------------------------------------------------------------ + | Identify corresponding topic type + >------------------------------------------------------------------------------------------------------------------------- + | Assuming a model follows the convention {ContentType}TopicViewModel or {ContentType}ViewModel, find a corresponding + | Topic named {ContentType}. If this cannot be found, fall back to Topic. + \-----------------------------------------------------------------------------------------------------------------------*/ + var impliedContentType = Type.Name + .Replace("TopicViewModel", "", StringComparison.OrdinalIgnoreCase) + .Replace("ViewModel", "", StringComparison.OrdinalIgnoreCase); + var topicType = TopicFactory.TypeLookupService.Lookup(impliedContentType)?? typeof(Topic); + var topicAccessor = TypeAccessorCache.GetTypeAccessor(topicType); + + /*------------------------------------------------------------------------------------------------------------------------ + | Detect compatibility + >------------------------------------------------------------------------------------------------------------------------- + | If the Topic contains a property named {Name} or a method named Get{Name}, and that member's type is either compatible + | or convertible, then set MaybeCompatible to true. + \-----------------------------------------------------------------------------------------------------------------------*/ + foreach (var member in ConstructorParameters.Cast().Union(_members.Values)) { + var attributeKey = member.Configuration.AttributeKey; + var topicMember = topicAccessor.GetMember(attributeKey)?? topicAccessor.GetMember($"Get{attributeKey}"); + if (topicMember is null) { + continue; + } + else if (member.IsConvertible && topicMember.Type == typeof(string)) { + member.MaybeCompatible = true; + } + else if (member.Type.IsAssignableFrom(topicMember.Type)) { + member.MaybeCompatible = true; + } + + } + } + + } //Class +} //Namespace \ No newline at end of file diff --git a/OnTopic/Internal/Reflection/TypeAccessorCache.cs b/OnTopic/Internal/Reflection/TypeAccessorCache.cs new file mode 100644 index 00000000..67189a34 --- /dev/null +++ b/OnTopic/Internal/Reflection/TypeAccessorCache.cs @@ -0,0 +1,45 @@ +/*============================================================================================================================== +| Author Ignia, LLC +| Client Ignia, LLC +| Project Topics Library +\=============================================================================================================================*/ +using System.Collections.Concurrent; + +namespace OnTopic.Internal.Reflection { + + /*============================================================================================================================ + | CLASS: TYPE ACCESSOR CACHE + \---------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Provides access to a cached set of instances across the application. + /// + internal static class TypeAccessorCache { + + /*========================================================================================================================== + | PRIVATE VARIABLES + \-------------------------------------------------------------------------------------------------------------------------*/ + private static readonly ConcurrentDictionary _cache = new(); + + /*========================================================================================================================== + | GET TYPE ACCESSOR + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Retrieves a for a given . + /// + /// + /// As each is fixed at runtime, and we expect types that are mapped once to be mapped multiple times, + /// a static cache is maintained of instances. If the provided doesn't yet + /// exist in the cache, it will be created. + /// + /// The that needs to be dynamically accessed. + /// A for dynamically accessing the supplied . + internal static TypeAccessor GetTypeAccessor(Type type) { + return _cache.GetOrAdd(type, t => new(t)); + } + + /// + /// The that needs to be dynamically accessed. + internal static TypeAccessor GetTypeAccessor() => GetTypeAccessor(typeof(T)); + + } //Class +} //Namespace \ No newline at end of file diff --git a/OnTopic/Internal/Reflection/TypeMemberInfoCollection.cs b/OnTopic/Internal/Reflection/TypeMemberInfoCollection.cs deleted file mode 100644 index 7c8abf00..00000000 --- a/OnTopic/Internal/Reflection/TypeMemberInfoCollection.cs +++ /dev/null @@ -1,165 +0,0 @@ -/*============================================================================================================================== -| Author Ignia, LLC -| Client Ignia, LLC -| Project Topics Library -\=============================================================================================================================*/ -using System; -using System.Collections.ObjectModel; -using System.Diagnostics.CodeAnalysis; -using System.Linq; -using System.Reflection; -using OnTopic.Internal.Diagnostics; - -namespace OnTopic.Internal.Reflection { - - /*============================================================================================================================ - | CLASS: KEYED MEMBER INFO COLLECTION - \---------------------------------------------------------------------------------------------------------------------------*/ - /// - /// Provides keyed access to a collection of instances. - /// - internal class TypeMemberInfoCollection: KeyedCollection { - - /*========================================================================================================================== - | CONSTRUCTOR - \-------------------------------------------------------------------------------------------------------------------------*/ - /// - /// Initializes a new instance of the class. - /// - internal TypeMemberInfoCollection() : base() { - } - - /*========================================================================================================================== - | METHOD: GET MEMBERS - \-------------------------------------------------------------------------------------------------------------------------*/ - /// - /// Returns a collection of objects associated with a specific type. - /// - /// - /// If the collection cannot be found locally, it will be created. - /// - /// The type for which the members should be retrieved. - internal MemberInfoCollection GetMembers(Type type) { - if (!Contains(type)) { - lock (Items) { - if (!Contains(type)) { - Add(new(type)); - } - } - } - return this[type]; - } - - /*========================================================================================================================== - | METHOD: GET MEMBERS {T} - \-------------------------------------------------------------------------------------------------------------------------*/ - /// - /// Returns a collection of objects associated with a specific type. - /// - /// - /// If the collection cannot be found locally, it will be created. - /// - /// The type for which the members should be retrieved. - internal MemberInfoCollection GetMembers(Type type) where T : MemberInfo => - new(type, GetMembers(type).Where(m => typeof(T).IsAssignableFrom(m.GetType())).Cast()); - - /*========================================================================================================================== - | METHOD: GET MEMBER - \-------------------------------------------------------------------------------------------------------------------------*/ - /// - /// Used reflection to identify a local member by a given name, and returns the associated - /// instance. - /// - internal MemberInfo? GetMember(Type type, string name) { - var members = GetMembers(type); - if (members.Contains(name)) { - return members[name]; - } - return null; - } - - /*========================================================================================================================== - | METHOD: GET MEMBER {T} - \-------------------------------------------------------------------------------------------------------------------------*/ - /// - /// Used reflection to identify a local member by a given name, and returns the associated - /// instance. - /// - internal T? GetMember(Type type, string name) where T : MemberInfo { - var members = GetMembers(type); - if (members.Contains(name) && typeof(T).IsAssignableFrom(members[name].GetType())) { - return members[name] as T; - } - return null; - } - - /*========================================================================================================================== - | METHOD: HAS MEMBER - \-------------------------------------------------------------------------------------------------------------------------*/ - /// - /// Used reflection to identify if a local member is available. - /// - internal bool HasMember(Type type, string name) => GetMember(type, name) is not null; - - /*========================================================================================================================== - | METHOD: HAS MEMBER {T} - \-------------------------------------------------------------------------------------------------------------------------*/ - /// - /// Used reflection to identify if a local member of type is available. - /// - internal bool HasMember(Type type, string name) where T : MemberInfo => GetMember(type, name) is not null; - - /*========================================================================================================================== - | OVERRIDE: INSERT ITEM - \-------------------------------------------------------------------------------------------------------------------------*/ - /// - /// Fires any time an item is added to the collection. - /// - /// - /// Compared to the base implementation, will throw a specific error if a duplicate key - /// is inserted. This conveniently provides the , so it's clear what - /// is being duplicated. - /// - /// The zero-based index at which should be inserted. - /// The instance to insert. - /// - /// The TypeMemberInfoCollection already contains the MemberInfoCollection of the Type '{item.Type}'. - /// - protected override sealed void InsertItem(int index, MemberInfoCollection item) { - - /*------------------------------------------------------------------------------------------------------------------------ - | Validate parameters - \-----------------------------------------------------------------------------------------------------------------------*/ - Contract.Requires(item, nameof(item)); - - /*------------------------------------------------------------------------------------------------------------------------ - | Insert item, if not already present - \-----------------------------------------------------------------------------------------------------------------------*/ - if (!Contains(item.Type)) { - base.InsertItem(index, item); - } - else { - throw new ArgumentException( - $"The '{nameof(TypeMemberInfoCollection)}' already contains the {nameof(MemberInfoCollection)} of the Type " + - $"'{item.Type}'.", - nameof(item) - ); - } - } - - /*========================================================================================================================== - | OVERRIDE: GET KEY FOR ITEM - \-------------------------------------------------------------------------------------------------------------------------*/ - /// - /// Method must be overridden for the EntityCollection to extract the keys from the items. - /// - /// The object from which to extract the key. - /// The key for the specified collection item. - [ExcludeFromCodeCoverage] - protected override sealed Type GetKeyForItem(MemberInfoCollection item) { - Contract.Requires(item, "The item must be available in order to derive its key."); - return item.Type; - } - - } //Class -} //Namespace \ No newline at end of file diff --git a/OnTopic/InvalidKeyException.cs b/OnTopic/InvalidKeyException.cs index 1df95307..50db9edf 100644 --- a/OnTopic/InvalidKeyException.cs +++ b/OnTopic/InvalidKeyException.cs @@ -3,10 +3,7 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ -using System; -using System.Diagnostics.CodeAnalysis; using System.Runtime.Serialization; -using OnTopic.Internal.Diagnostics; namespace OnTopic { diff --git a/OnTopic/Lookup/CompositeTypeLookupService.cs b/OnTopic/Lookup/CompositeTypeLookupService.cs index 518d14be..f01ef37e 100644 --- a/OnTopic/Lookup/CompositeTypeLookupService.cs +++ b/OnTopic/Lookup/CompositeTypeLookupService.cs @@ -3,9 +3,6 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ -using System; -using System.Collections.Generic; -using System.Linq; namespace OnTopic.Lookup { diff --git a/OnTopic/Lookup/DefaultTopicLookupService.cs b/OnTopic/Lookup/DefaultTopicLookupService.cs index fdb9f061..c8d25917 100644 --- a/OnTopic/Lookup/DefaultTopicLookupService.cs +++ b/OnTopic/Lookup/DefaultTopicLookupService.cs @@ -3,8 +3,6 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ -using System; -using System.Collections.Generic; using System.Reflection; using OnTopic.Metadata; diff --git a/OnTopic/Lookup/DynamicTopicBindingModelLookupService.cs b/OnTopic/Lookup/DynamicTopicBindingModelLookupService.cs index a723b177..35065dcd 100644 --- a/OnTopic/Lookup/DynamicTopicBindingModelLookupService.cs +++ b/OnTopic/Lookup/DynamicTopicBindingModelLookupService.cs @@ -3,7 +3,6 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ -using System; using OnTopic.Models; namespace OnTopic.Lookup { diff --git a/OnTopic/Lookup/DynamicTopicLookupService.cs b/OnTopic/Lookup/DynamicTopicLookupService.cs index 4870926c..3e376929 100644 --- a/OnTopic/Lookup/DynamicTopicLookupService.cs +++ b/OnTopic/Lookup/DynamicTopicLookupService.cs @@ -3,7 +3,6 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ -using System; using OnTopic.Metadata; namespace OnTopic.Lookup { diff --git a/OnTopic/Lookup/DynamicTopicViewModelLookupService.cs b/OnTopic/Lookup/DynamicTopicViewModelLookupService.cs index dc64b9e1..052abe33 100644 --- a/OnTopic/Lookup/DynamicTopicViewModelLookupService.cs +++ b/OnTopic/Lookup/DynamicTopicViewModelLookupService.cs @@ -3,7 +3,6 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ -using System; namespace OnTopic.Lookup { diff --git a/OnTopic/Lookup/DynamicTypeLookupService.cs b/OnTopic/Lookup/DynamicTypeLookupService.cs index ebacd5d7..208f3e54 100644 --- a/OnTopic/Lookup/DynamicTypeLookupService.cs +++ b/OnTopic/Lookup/DynamicTypeLookupService.cs @@ -3,8 +3,6 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ -using System; -using System.Linq; namespace OnTopic.Lookup { diff --git a/OnTopic/Lookup/ITypeLookupService.cs b/OnTopic/Lookup/ITypeLookupService.cs index c9db5035..830a18a5 100644 --- a/OnTopic/Lookup/ITypeLookupService.cs +++ b/OnTopic/Lookup/ITypeLookupService.cs @@ -3,7 +3,6 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ -using System; using OnTopic.Mapping; namespace OnTopic.Lookup { diff --git a/OnTopic/Lookup/StaticTypeLookupService.cs b/OnTopic/Lookup/StaticTypeLookupService.cs index 7997153a..83b20f48 100644 --- a/OnTopic/Lookup/StaticTypeLookupService.cs +++ b/OnTopic/Lookup/StaticTypeLookupService.cs @@ -3,11 +3,7 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ -using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; using System.Reflection; -using OnTopic.Internal.Diagnostics; namespace OnTopic.Lookup { @@ -65,7 +61,10 @@ public StaticTypeLookupService( /// The list of instances to expose as part of this service. /// The default type to return if no match can be found. Defaults to object. [ExcludeFromCodeCoverage] - [Obsolete("The DefaultType property has been removed. Fallbacks types can now be added to Lookup() directly.", true)] + [Obsolete( + $"The {nameof(DefaultType)} property has been removed. Fallbacks types can now be added to {nameof(Lookup)} directly.", + true + )] public StaticTypeLookupService( IEnumerable? types, Type? defaultType @@ -80,7 +79,10 @@ public StaticTypeLookupService( /// The default type to return in case cannot find a match. /// [ExcludeFromCodeCoverage] - [Obsolete("The DefaultType property has been removed. Fallbacks types can now be added to Lookup() directly.", true)] + [Obsolete( + $"The {nameof(DefaultType)} property has been removed. Fallbacks types can now be added to {nameof(Lookup)} directly.", + true + )] public Type? DefaultType { get; } /*========================================================================================================================== diff --git a/OnTopic/Lookup/TypeCollection.cs b/OnTopic/Lookup/TypeCollection.cs index 1743a5b2..53ee0bfc 100644 --- a/OnTopic/Lookup/TypeCollection.cs +++ b/OnTopic/Lookup/TypeCollection.cs @@ -3,13 +3,9 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ -using System; using System.Collections; -using System.Collections.Generic; using System.Collections.ObjectModel; -using System.Diagnostics.CodeAnalysis; using System.Reflection; -using OnTopic.Internal.Diagnostics; namespace OnTopic.Lookup { diff --git a/OnTopic/Mapping/Annotations/AssociationTypes.cs b/OnTopic/Mapping/Annotations/AssociationTypes.cs index 54292b89..76090d96 100644 --- a/OnTopic/Mapping/Annotations/AssociationTypes.cs +++ b/OnTopic/Mapping/Annotations/AssociationTypes.cs @@ -3,7 +3,6 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ -using System; using OnTopic.Metadata; namespace OnTopic.Mapping.Annotations { diff --git a/OnTopic/Mapping/Annotations/AttributeKeyAttribute.cs b/OnTopic/Mapping/Annotations/AttributeKeyAttribute.cs index b8a6a526..0dcedd6b 100644 --- a/OnTopic/Mapping/Annotations/AttributeKeyAttribute.cs +++ b/OnTopic/Mapping/Annotations/AttributeKeyAttribute.cs @@ -3,8 +3,6 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ -using System; -using System.Diagnostics.CodeAnalysis; using OnTopic.Collections.Specialized; namespace OnTopic.Mapping.Annotations { @@ -52,7 +50,7 @@ public AttributeKeyAttribute(string key) { /// Gets the value of the attribute key. /// [ExcludeFromCodeCoverage] - [Obsolete("The Value property has been renamed to Key for consistency", true)] + [Obsolete($"The {nameof(Value)} property has been renamed to {nameof(Key)} for consistency", true)] public string? Value { get; } } //Class diff --git a/OnTopic/Mapping/Annotations/CollectionAttribute.cs b/OnTopic/Mapping/Annotations/CollectionAttribute.cs index 390f06c9..2ea490c5 100644 --- a/OnTopic/Mapping/Annotations/CollectionAttribute.cs +++ b/OnTopic/Mapping/Annotations/CollectionAttribute.cs @@ -3,7 +3,6 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ -using System; namespace OnTopic.Mapping.Annotations { diff --git a/OnTopic/Mapping/Annotations/DisableMappingAttribute.cs b/OnTopic/Mapping/Annotations/DisableMappingAttribute.cs index 70ed5241..451a47b3 100644 --- a/OnTopic/Mapping/Annotations/DisableMappingAttribute.cs +++ b/OnTopic/Mapping/Annotations/DisableMappingAttribute.cs @@ -3,7 +3,6 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ -using System; using OnTopic.Mapping.Reverse; namespace OnTopic.Mapping.Annotations { diff --git a/OnTopic/Mapping/Annotations/FilterByAttributeAttribute.cs b/OnTopic/Mapping/Annotations/FilterByAttributeAttribute.cs index 8ef7c3a6..bcf62974 100644 --- a/OnTopic/Mapping/Annotations/FilterByAttributeAttribute.cs +++ b/OnTopic/Mapping/Annotations/FilterByAttributeAttribute.cs @@ -3,8 +3,6 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ -using System; -using OnTopic.Internal.Diagnostics; namespace OnTopic.Mapping.Annotations { diff --git a/OnTopic/Mapping/Annotations/FilterByContentType.cs b/OnTopic/Mapping/Annotations/FilterByContentType.cs index a484a84b..95564583 100644 --- a/OnTopic/Mapping/Annotations/FilterByContentType.cs +++ b/OnTopic/Mapping/Annotations/FilterByContentType.cs @@ -3,7 +3,6 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ -using System; namespace OnTopic.Mapping.Annotations { diff --git a/OnTopic/Mapping/Annotations/FlattenAttribute.cs b/OnTopic/Mapping/Annotations/FlattenAttribute.cs index 7bd10da4..1ff72d49 100644 --- a/OnTopic/Mapping/Annotations/FlattenAttribute.cs +++ b/OnTopic/Mapping/Annotations/FlattenAttribute.cs @@ -3,7 +3,6 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ -using System; namespace OnTopic.Mapping.Annotations { diff --git a/OnTopic/Mapping/Annotations/IncludeAttribute.cs b/OnTopic/Mapping/Annotations/IncludeAttribute.cs index 7524ec67..7a0f6108 100644 --- a/OnTopic/Mapping/Annotations/IncludeAttribute.cs +++ b/OnTopic/Mapping/Annotations/IncludeAttribute.cs @@ -3,7 +3,6 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ -using System; namespace OnTopic.Mapping.Annotations { diff --git a/OnTopic/Mapping/Annotations/InheritAttribute.cs b/OnTopic/Mapping/Annotations/InheritAttribute.cs index 30fb33d9..b2f8f9a6 100644 --- a/OnTopic/Mapping/Annotations/InheritAttribute.cs +++ b/OnTopic/Mapping/Annotations/InheritAttribute.cs @@ -3,7 +3,6 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ -using System; using OnTopic.Collections.Specialized; namespace OnTopic.Mapping.Annotations { diff --git a/OnTopic/Mapping/Annotations/MapAsAttribute.cs b/OnTopic/Mapping/Annotations/MapAsAttribute.cs index 85232f91..7fc3adaa 100644 --- a/OnTopic/Mapping/Annotations/MapAsAttribute.cs +++ b/OnTopic/Mapping/Annotations/MapAsAttribute.cs @@ -3,7 +3,6 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ -using System; namespace OnTopic.Mapping.Annotations { diff --git a/OnTopic/Mapping/Annotations/MapToParentAttribute.cs b/OnTopic/Mapping/Annotations/MapToParentAttribute.cs index 9dbf7a08..6bbb95f9 100644 --- a/OnTopic/Mapping/Annotations/MapToParentAttribute.cs +++ b/OnTopic/Mapping/Annotations/MapToParentAttribute.cs @@ -3,7 +3,6 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ -using System; using OnTopic.Mapping.Reverse; namespace OnTopic.Mapping.Annotations { diff --git a/OnTopic/Mapping/Annotations/MetadataAttribute.cs b/OnTopic/Mapping/Annotations/MetadataAttribute.cs index 91048127..1cfbfe0e 100644 --- a/OnTopic/Mapping/Annotations/MetadataAttribute.cs +++ b/OnTopic/Mapping/Annotations/MetadataAttribute.cs @@ -3,7 +3,6 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ -using System; namespace OnTopic.Mapping.Annotations { diff --git a/OnTopic/Mapping/CachedTopicMappingService.cs b/OnTopic/Mapping/CachedTopicMappingService.cs index dde45851..14f8965a 100644 --- a/OnTopic/Mapping/CachedTopicMappingService.cs +++ b/OnTopic/Mapping/CachedTopicMappingService.cs @@ -3,10 +3,7 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ -using System; using System.Collections.Concurrent; -using System.Threading.Tasks; -using OnTopic.Internal.Diagnostics; using OnTopic.Mapping.Annotations; namespace OnTopic.Mapping { diff --git a/OnTopic/Mapping/Hierarchical/CachedHierarchicalTopicMappingService{T}.cs b/OnTopic/Mapping/Hierarchical/CachedHierarchicalTopicMappingService{T}.cs index 1984f24b..bf769275 100644 --- a/OnTopic/Mapping/Hierarchical/CachedHierarchicalTopicMappingService{T}.cs +++ b/OnTopic/Mapping/Hierarchical/CachedHierarchicalTopicMappingService{T}.cs @@ -3,9 +3,7 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ -using System; using System.Collections.Concurrent; -using System.Threading.Tasks; using OnTopic.Models; namespace OnTopic.Mapping.Hierarchical { diff --git a/OnTopic/Mapping/Hierarchical/HierarchicalTopicMappingService{T}.cs b/OnTopic/Mapping/Hierarchical/HierarchicalTopicMappingService{T}.cs index aa54b296..35ebcb36 100644 --- a/OnTopic/Mapping/Hierarchical/HierarchicalTopicMappingService{T}.cs +++ b/OnTopic/Mapping/Hierarchical/HierarchicalTopicMappingService{T}.cs @@ -3,11 +3,6 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using OnTopic.Internal.Diagnostics; using OnTopic.Mapping.Annotations; using OnTopic.Models; using OnTopic.Repositories; diff --git a/OnTopic/Mapping/Hierarchical/IHierarchicalTopicMappingService{T}.cs b/OnTopic/Mapping/Hierarchical/IHierarchicalTopicMappingService{T}.cs index f41c544c..cd907c1d 100644 --- a/OnTopic/Mapping/Hierarchical/IHierarchicalTopicMappingService{T}.cs +++ b/OnTopic/Mapping/Hierarchical/IHierarchicalTopicMappingService{T}.cs @@ -3,8 +3,6 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ -using System; -using System.Threading.Tasks; using OnTopic.Models; namespace OnTopic.Mapping.Hierarchical { diff --git a/OnTopic/Mapping/ITopicMappingService.cs b/OnTopic/Mapping/ITopicMappingService.cs index 74c22ff5..70e5a6c3 100644 --- a/OnTopic/Mapping/ITopicMappingService.cs +++ b/OnTopic/Mapping/ITopicMappingService.cs @@ -3,8 +3,6 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ -using System; -using System.Threading.Tasks; using OnTopic.Mapping.Annotations; namespace OnTopic.Mapping { diff --git a/OnTopic/Mapping/Internal/AssociationMap.cs b/OnTopic/Mapping/Internal/AssociationMap.cs index 97598847..a4a49569 100644 --- a/OnTopic/Mapping/Internal/AssociationMap.cs +++ b/OnTopic/Mapping/Internal/AssociationMap.cs @@ -3,7 +3,6 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ -using System.Collections.Generic; using OnTopic.Mapping.Annotations; namespace OnTopic.Mapping.Internal { diff --git a/OnTopic/Mapping/Internal/ItemConfiguration.cs b/OnTopic/Mapping/Internal/ItemConfiguration.cs index 65104fde..00cacb95 100644 --- a/OnTopic/Mapping/Internal/ItemConfiguration.cs +++ b/OnTopic/Mapping/Internal/ItemConfiguration.cs @@ -3,14 +3,11 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ -using System; -using System.Collections.Generic; using System.Collections.ObjectModel; using System.ComponentModel; -using System.Linq; using System.Reflection; using OnTopic.Collections.Specialized; -using OnTopic.Internal.Diagnostics; +using OnTopic.Internal.Reflection; using OnTopic.Mapping.Annotations; namespace OnTopic.Mapping.Internal { @@ -19,7 +16,7 @@ namespace OnTopic.Mapping.Internal { | CLASS: ITEM CONFIGURATION \---------------------------------------------------------------------------------------------------------------------------*/ /// - /// Evaluates the for a given instance for a or of a given instance for a or , and exposes known s through a set of property values. /// /// @@ -34,10 +31,6 @@ namespace OnTopic.Mapping.Internal { /// then the will instead use the value defined by that attribute, thus allowing a /// property on the DTO to be aliased to a different property or attribute name on the source . /// - /// - /// The works with both and - /// instances, whereas the works exclusively with instances. - /// /// internal class ItemConfiguration { @@ -45,32 +38,24 @@ internal class ItemConfiguration { | CONSTRUCTOR \-------------------------------------------------------------------------------------------------------------------------*/ /// - /// Given an instance, exposes a set of properties associated with known instances. + /// Given an instance, exposes a set of properties associated with known + /// instances. /// - /// - /// The instance to check for values. + /// + /// The instance associated with this . /// - /// The name of the or . - /// The prefix to apply to the attributes. - internal ItemConfiguration(ICustomAttributeProvider source, string name, string? attributePrefix = "") { - - /*------------------------------------------------------------------------------------------------------------------------ - | Validate parameters - \-----------------------------------------------------------------------------------------------------------------------*/ - Contract.Requires(source, nameof(source)); - Contract.Requires(name, nameof(name)); + internal ItemConfiguration(ItemMetadata itemMetadata) { /*------------------------------------------------------------------------------------------------------------------------ - | Set backing property + | Set backing properties \-----------------------------------------------------------------------------------------------------------------------*/ - Source = source; + CustomAttributes = itemMetadata.CustomAttributes; /*------------------------------------------------------------------------------------------------------------------------ | Set default values \-----------------------------------------------------------------------------------------------------------------------*/ - AttributeKey = attributePrefix + name; - AttributePrefix = attributePrefix; + AttributeKey = itemMetadata.Name; + AttributePrefix = ""; DefaultValue = null; InheritValue = false; CollectionKey = AttributeKey; @@ -84,23 +69,22 @@ internal ItemConfiguration(ICustomAttributeProvider source, string name, string? /*------------------------------------------------------------------------------------------------------------------------ | Attributes: Retrieve basic attributes \-----------------------------------------------------------------------------------------------------------------------*/ - GetAttributeValue(source, a => MapAs = a.Type); - GetAttributeValue(source, a => DefaultValue = a.Value); - GetAttributeValue(source, a => InheritValue = true); - GetAttributeValue(source, a => AttributeKey = attributePrefix + a.Key); - GetAttributeValue(source, a => MapToParent = true); - GetAttributeValue(source, a => AttributePrefix += (a.AttributePrefix?? name)); - GetAttributeValue(source, a => IncludeAssociations = a.Associations); - GetAttributeValue(source, a => FlattenChildren = true); - GetAttributeValue(source, a => MetadataKey = a.Key); - GetAttributeValue(source, a => DisableMapping = true); - GetAttributeValue(source, a => ContentTypeFilter = a.ContentType); + GetAttributeValue( a => MapAs = a.Type); + GetAttributeValue( a => DefaultValue = a.Value); + GetAttributeValue( a => InheritValue = true); + GetAttributeValue( a => AttributeKey = a.Key); + GetAttributeValue( a => MapToParent = true); + GetAttributeValue( a => AttributePrefix += (a.AttributePrefix?? AttributeKey)); + GetAttributeValue( a => IncludeAssociations = a.Associations); + GetAttributeValue( a => FlattenChildren = true); + GetAttributeValue( a => MetadataKey = a.Key); + GetAttributeValue( a => DisableMapping = true); + GetAttributeValue( a => ContentTypeFilter = a.ContentType); /*------------------------------------------------------------------------------------------------------------------------ | Attributes: Determine collection key and type \-----------------------------------------------------------------------------------------------------------------------*/ GetAttributeValue( - source, a => { CollectionKey = a.Key ?? CollectionKey; CollectionType = a.Type; @@ -114,7 +98,7 @@ internal ItemConfiguration(ICustomAttributeProvider source, string name, string? /*------------------------------------------------------------------------------------------------------------------------ | Attributes: Set attribute filters \-----------------------------------------------------------------------------------------------------------------------*/ - var filterByAttributes = (FilterByAttributeAttribute[])source.GetCustomAttributes(typeof(FilterByAttributeAttribute), true); + var filterByAttributes = CustomAttributes.OfType(); if (filterByAttributes is not null && filterByAttributes.Any()) { foreach (var filter in filterByAttributes) { AttributeFilters.Add(filter.Key, filter.Value); @@ -124,12 +108,12 @@ internal ItemConfiguration(ICustomAttributeProvider source, string name, string? } /*========================================================================================================================== - | PROPERTY: SOURCE + | PROPERTY: CUSTOM ATTRIBUTES \-------------------------------------------------------------------------------------------------------------------------*/ /// - /// The that the current is associated with. + /// The that the current is associated with. /// - internal ICustomAttributeProvider Source { get; } + protected IEnumerable CustomAttributes { get; } /*========================================================================================================================== | PROPERTY: ATTRIBUTE KEY @@ -434,6 +418,21 @@ internal ItemConfiguration(ICustomAttributeProvider source, string name, string? /// internal Dictionary AttributeFilters { get; } + /*========================================================================================================================== + | METHOD: GET COMPOSITE ATTRIBUTE KEY + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Retrieves the current , and, optionally, the supplied . + /// + /// + /// The current mapping operation's attribute prefix to be prepended to the . + /// + /// + /// The composite attribute key, based on and, optionally, the supplied . + /// + internal string GetCompositeAttributeKey(string? attributePrefix) => $"{attributePrefix}{AttributeKey}"; + /*========================================================================================================================== | METHOD: SATISFIES ATTRIBUTE FILTERS \-------------------------------------------------------------------------------------------------------------------------*/ @@ -448,18 +447,28 @@ internal bool SatisfiesAttributeFilters(Topic source) => source?.Attributes?.GetValue(f.Key, "")?.Equals(f.Value, StringComparison.OrdinalIgnoreCase)?? false ); + /*========================================================================================================================== + | PRIVATE: GET ATTRIBUTE + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Helper function identifies whether a exists in the and, if so, + /// returns the value. + /// + /// An type to evaluate. + private T? GetAttribute() where T : Attribute => + CustomAttributes.OfType().FirstOrDefault(); + /*========================================================================================================================== | PRIVATE: GET ATTRIBUTE VALUE \-------------------------------------------------------------------------------------------------------------------------*/ /// - /// Helper function evaluates an attribute and then, if it exists, executes an to process the + /// Helper function evaluates an attribute and then, if it exists, executes an to process the /// results. /// /// An type to evaluate. - /// The instance to pull the attribute from. /// The to execute on the attribute. - private static void GetAttributeValue(ICustomAttributeProvider source, Action action) where T : Attribute { - var attribute = (T)source.GetCustomAttributes(typeof(T), true).FirstOrDefault(); + private void GetAttributeValue(Action action) where T : Attribute { + var attribute = GetAttribute(); if (attribute is not null) { action(attribute); } diff --git a/OnTopic/Mapping/Internal/MappedTopicCache.cs b/OnTopic/Mapping/Internal/MappedTopicCache.cs index 2a1ec9e0..a08001e0 100644 --- a/OnTopic/Mapping/Internal/MappedTopicCache.cs +++ b/OnTopic/Mapping/Internal/MappedTopicCache.cs @@ -3,10 +3,7 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ -using System; using System.Collections.Concurrent; -using System.Diagnostics.CodeAnalysis; -using OnTopic.Internal.Diagnostics; using OnTopic.Mapping.Annotations; namespace OnTopic.Mapping.Internal { @@ -57,13 +54,6 @@ internal bool TryGetValue(int topicId, Type type, [NotNullWhen(true)] out Mapped /// The mapped view model associated with the . internal void Register(int topicId, AssociationTypes associations, object viewModel) { - /*------------------------------------------------------------------------------------------------------------------------ - | Validate input - \-----------------------------------------------------------------------------------------------------------------------*/ - Contract.Requires(topicId, nameof(topicId)); - Contract.Requires(associations, nameof(associations)); - Contract.Requires(viewModel, nameof(viewModel)); - /*------------------------------------------------------------------------------------------------------------------------ | Construct cache entry \-----------------------------------------------------------------------------------------------------------------------*/ @@ -99,12 +89,6 @@ internal void Register(int topicId, AssociationTypes associations, object viewMo /// The that the is being mapped to. internal MappedTopicCacheEntry Preregister(int topicId, Type type) { - /*------------------------------------------------------------------------------------------------------------------------ - | Validate input - \-----------------------------------------------------------------------------------------------------------------------*/ - Contract.Requires(topicId, nameof(topicId)); - Contract.Requires(type, nameof(type)); - /*------------------------------------------------------------------------------------------------------------------------ | Construct cache entry \-----------------------------------------------------------------------------------------------------------------------*/ diff --git a/OnTopic/Mapping/Internal/PropertyConfiguration.cs b/OnTopic/Mapping/Internal/PropertyConfiguration.cs deleted file mode 100644 index 7a242b70..00000000 --- a/OnTopic/Mapping/Internal/PropertyConfiguration.cs +++ /dev/null @@ -1,73 +0,0 @@ -/*============================================================================================================================== -| Author Ignia, LLC -| Client Ignia, LLC -| Project Topics Library -\=============================================================================================================================*/ -using System; -using System.ComponentModel.DataAnnotations; -using System.Reflection; -using OnTopic.Mapping.Annotations; - -namespace OnTopic.Mapping.Internal { - - /*============================================================================================================================ - | CLASS: PROPERTY ATTRIBUTES - \---------------------------------------------------------------------------------------------------------------------------*/ - /// - /// Evaluates a instance for known , and exposes them through a set of - /// property values. - /// - /// - /// - /// The class is utilized by implementations of to - /// facilitate the mapping of source instances to Data Transfer Objects (DTOs), such as View Models. - /// The attribute values provide hints to the mapping service that help manage how the mapping occurs. - /// - /// - /// For example, by default a property on a DTO class will be mapped to a property or attribute of the same name on the - /// source . If the is attached to a property on the DTO, however, - /// then the will instead use the value defined by that attribute, thus allowing a - /// property on the DTO to be aliased to a different property or attribute name on the source . - /// - /// - internal class PropertyConfiguration: ItemConfiguration { - - /*========================================================================================================================== - | CONSTRUCTOR - \-------------------------------------------------------------------------------------------------------------------------*/ - /// - /// Given a instance, exposes a set of properties associated with known - /// instances. - /// - /// The instance to check for values. - /// The prefix to apply to the attributes. - internal PropertyConfiguration(PropertyInfo property, string? attributePrefix = ""): - base(property, property.Name, attributePrefix) - { - Property = property; - } - - /*========================================================================================================================== - | PROPERTY: PROPERTY - \-------------------------------------------------------------------------------------------------------------------------*/ - /// - /// The that the current is associated with. - /// - internal PropertyInfo Property { get; } - - /*========================================================================================================================== - | METHOD: VALIDATE - \-------------------------------------------------------------------------------------------------------------------------*/ - /// - /// Given a target DTO, will automatically identify any attributes that derive from and - /// ensure that their conditions are satisfied. - /// - /// The target DTO to validate the current property on. - internal void Validate(object target) { - foreach (ValidationAttribute validator in Property.GetCustomAttributes(typeof(ValidationAttribute))) { - validator.Validate(Property.GetValue(target), Property.Name); - } - } - - } -} \ No newline at end of file diff --git a/OnTopic/Mapping/README.md b/OnTopic/Mapping/README.md index 4090ac46..584ab38b 100644 --- a/OnTopic/Mapping/README.md +++ b/OnTopic/Mapping/README.md @@ -13,6 +13,9 @@ The [`ITopicMappingService`](ITopicMappingService.cs) defines a service for mapp - [Attributes](#attributes) - [ReverseTopicMappingService](#reversetopicmappingservice) - [Example](#example-1) +- [`AttributeDictionary` Constuctor](#attributedictionary-constructor) + - [Example](#example-2) + - [Considerations](#considerations) - [Polymorphism](#polymorphism) - [Filtering](#filtering) - [Topics](#topics) @@ -152,6 +155,40 @@ In this example, the properties would map to: > *Note*: Often times, models won't require any attributes. These are only needed if the properties don't follow the built-in conventions and require additional hints. For instance, the `[Collection(…)]` attribute is useful if the collection key is ambiguous between outgoing relationships and incoming relationships. +## `AttributeDictionary` Constructor + +By default, all properties are mapped via reflection. As an optimization, view models may _optionally_ include a constructor that accepts an `AttributeDictionary`, using this to assign scalar values directly to properties. Any attributes in the `AttributeDictionary` won't be mapped via reflection, thus improving performance. + +### Example +The following is an example of a constructor that accepts an `AttributeDictionary`: +```csharp +public class PageTopicViewModel: TopicViewModel { + + public PageTopicViewModel(AttributeDictionary attributes): base(attributes) { + Contract.Requires(attributes, nameof(attributes)); + ShortTitle = attributes.GetValue(nameof(ShortTitle)); + Subtitle = attributes.GetValue(nameof(Subtitle)); + MetaTitle = attributes.GetValue(nameof(MetaTitle)); + MetaDescription = attributes.GetValue(nameof(MetaDescription)); + MetaKeywords = attributes.GetValue(nameof(MetaKeywords)); + NoIndex = attributes.GetBoolean(nameof(NoIndex))?? NoIndex; + Body = attributes.GetValue(nameof(Body)); + } + + pubic PageTopicViewModel(): base() {} + + // View model properties… + +} +``` + +### Considerations +- **Mapping Properties:** If attributes in the `AttributeDictionary` are not properly mapped via the constructor, the corresponding properties will _not_ be mapped. +- **Derived View Models:** If a view model is derived from another view model, it must either implement all inherited attributes, or it must pass the `AttributeDictionary` to the base class's constructor. +- **Empty Constructor:** It's more efficient to use reflection if there are a small number of attributes; therefore, a view model must still include an empty constructor, as well as any annotations necessary to allow mapping via reflection. +- **Strongly Typed Methods:** The `AttributeDictionary` includes strongly typed methods for retrieving supported scalar data types with conversion; these include `GetValue()` (for strings), `GetBoolean()`, `GetInteger()`, `GetDouble()`, `GetDateTime()`, `GetUri()`. +- **Default Values:** If properties include default values, they should be included via a null-coalescing operator to ensure that a null value in the `AttributeDictionary` doesn't override the local defaut. + ## Polymorphism If a reference type (e.g., `TopicViewModel Parent`) or a strongly-typed collection property (e.g., `List`) is defined, then any target instances must be assignable by the base type (in these cases, `TopicViewModel`). If they cannot be, then they will not be included; no error will occur. diff --git a/OnTopic/Mapping/Reverse/BindingModelValidator.cs b/OnTopic/Mapping/Reverse/BindingModelValidator.cs index 43142b4b..d915472c 100644 --- a/OnTopic/Mapping/Reverse/BindingModelValidator.cs +++ b/OnTopic/Mapping/Reverse/BindingModelValidator.cs @@ -3,17 +3,11 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ -using System; using System.Collections; using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.Linq; using System.Reflection; -using OnTopic.Internal.Diagnostics; using OnTopic.Internal.Reflection; using OnTopic.Mapping.Annotations; -using OnTopic.Mapping.Internal; using OnTopic.Metadata; using OnTopic.Models; using OnTopic.Repositories; @@ -62,7 +56,7 @@ static internal class BindingModelValidator { /// This helper function is intended to provide reporting to developers about errors in their model. As a result, it /// will exclusively throw exceptions, as opposed to populating validation object for rendering to the view. Because /// it's only evaluating the compiled model, which will not change during the application's life cycle, the and are stored as a in a static + /// name="typeAccessor"/> and are stored as a in a static /// once a particular combination has passed validation—that way, this check only needs /// to be executed once for any combination, at least for the current application life cycle. /// @@ -77,19 +71,15 @@ static internal class BindingModelValidator { /// cref="ContentTypeDescriptor"/> may not have undergone explicit testing. /// /// - /// - /// The binding model to validate. - /// - /// - /// A describing the 's properties. + /// + /// The of the binding model to validate. /// /// /// The object against which to validate the model. /// /// The prefix to apply to the attributes. static internal void ValidateModel( - [AllowNull]Type sourceType, - [AllowNull]MemberInfoCollection properties, + [AllowNull]TypeAccessor typeAccessor, [AllowNull]ContentTypeDescriptor contentTypeDescriptor, [AllowNull]string attributePrefix = "" ) { @@ -97,28 +87,27 @@ static internal void ValidateModel( /*------------------------------------------------------------------------------------------------------------------------ | Validate parameters \-----------------------------------------------------------------------------------------------------------------------*/ - Contract.Requires(sourceType, nameof(sourceType)); - Contract.Requires(properties, nameof(properties)); + Contract.Requires(typeAccessor, nameof(typeAccessor)); Contract.Requires(contentTypeDescriptor, nameof(contentTypeDescriptor)); /*------------------------------------------------------------------------------------------------------------------------ | Skip validation if this type has already been validated for this content type \-----------------------------------------------------------------------------------------------------------------------*/ - if (_modelsValidated.Contains((sourceType, contentTypeDescriptor.Key))) { + if (_modelsValidated.Contains((typeAccessor.Type, contentTypeDescriptor.Key))) { return; } /*------------------------------------------------------------------------------------------------------------------------ | Validate \-----------------------------------------------------------------------------------------------------------------------*/ - foreach (var property in properties) { - ValidateProperty(sourceType, property, contentTypeDescriptor, attributePrefix); + foreach (var property in typeAccessor.GetMembers(MemberTypes.Property)) { + ValidateProperty(typeAccessor.Type, property, contentTypeDescriptor, attributePrefix); } /*------------------------------------------------------------------------------------------------------------------------ | Add type, content type to model validation cache so it isn't checked again \-----------------------------------------------------------------------------------------------------------------------*/ - _modelsValidated.Add((sourceType, contentTypeDescriptor.Key)); + _modelsValidated.Add((typeAccessor.Type, contentTypeDescriptor.Key)); return; @@ -134,8 +123,8 @@ static internal void ValidateModel( /// /// The binding model to validate. /// - /// - /// A describing a specific property of the . + /// + /// A describing a specific property of the . /// /// /// The object against which to validate the model. @@ -143,7 +132,7 @@ static internal void ValidateModel( /// The prefix to apply to the attributes. static internal void ValidateProperty( [AllowNull]Type sourceType, - [AllowNull]PropertyInfo property, + [AllowNull]MemberAccessor propertyAccessor, [AllowNull]ContentTypeDescriptor contentTypeDescriptor, [AllowNull]string attributePrefix = "" ) { @@ -152,15 +141,14 @@ static internal void ValidateProperty( | Validate parameters \-----------------------------------------------------------------------------------------------------------------------*/ Contract.Requires(sourceType, nameof(sourceType)); - Contract.Requires(property, nameof(property)); + Contract.Requires(propertyAccessor, nameof(propertyAccessor)); Contract.Requires(contentTypeDescriptor, nameof(contentTypeDescriptor)); /*------------------------------------------------------------------------------------------------------------------------ | Define variables \-----------------------------------------------------------------------------------------------------------------------*/ - var propertyType = property.PropertyType; - var configuration = new PropertyConfiguration(property, attributePrefix); - var compositeAttributeKey = configuration.AttributeKey; + var configuration = propertyAccessor.Configuration; + var compositeAttributeKey = configuration.GetCompositeAttributeKey(attributePrefix); var attributeDescriptor = contentTypeDescriptor.AttributeDescriptors.GetValue(compositeAttributeKey); var childCollections = new[] { CollectionType.Children, CollectionType.NestedTopics }; var relationships = new[] { CollectionType.Relationship, CollectionType.IncomingRelationship }; @@ -176,7 +164,7 @@ static internal void ValidateProperty( /*------------------------------------------------------------------------------------------------------------------------ | Skip properties injected by the compiler for record types \-----------------------------------------------------------------------------------------------------------------------*/ - if (configuration.Property.Name is "EqualityContract") { + if (propertyAccessor.Name is "EqualityContract") { return; } @@ -184,10 +172,10 @@ static internal void ValidateProperty( | Handle mapping properties from referenced objects \-----------------------------------------------------------------------------------------------------------------------*/ if (configuration.MapToParent) { - var childProperties = new MemberInfoCollection(propertyType, propertyType.GetProperties()); + var typeAccessor = TypeAccessorCache.GetTypeAccessor(propertyAccessor.Type); + ValidateModel( - propertyType, - childProperties, + typeAccessor, contentTypeDescriptor, configuration.AttributePrefix ); @@ -197,7 +185,7 @@ static internal void ValidateProperty( /*------------------------------------------------------------------------------------------------------------------------ | Define list type (if it's a list) \-----------------------------------------------------------------------------------------------------------------------*/ - foreach (var type in configuration.Property.PropertyType.GetInterfaces()) { + foreach (var type in propertyAccessor.Type.GetInterfaces()) { if (type.IsGenericType && typeof(IList<>) == type.GetGenericTypeDefinition()) { //Uses last argument in case it's a KeyedCollection; in that case, we want the TItem type listType = type.GetGenericArguments().Last(); @@ -220,7 +208,7 @@ static internal void ValidateProperty( /*------------------------------------------------------------------------------------------------------------------------ | Handle parent \-----------------------------------------------------------------------------------------------------------------------*/ - if (configuration.AttributeKey is "Parent") { + if (configuration.GetCompositeAttributeKey(attributePrefix) is "Parent") { throw new MappingModelValidationException( $"The {nameof(ReverseTopicMappingService)} does not support mapping Parent topics. This property should be " + $"removed from the binding model, or otherwise decorated with the {nameof(DisableMappingAttribute)} to prevent " + @@ -235,7 +223,7 @@ static internal void ValidateProperty( throw new MappingModelValidationException( $"A '{nameof(sourceType)}' object was provided with a content type set to '{contentTypeDescriptor.Key}'. This " + $"content type does not contain an attribute named '{compositeAttributeKey}', as requested by the " + - $"'{configuration.Property.Name}' property. If this property is not intended to be mapped by the " + + $"'{propertyAccessor.Name}' property. If this property is not intended to be mapped by the " + $"{nameof(ReverseTopicMappingService)}, then it should be decorated with {nameof(DisableMappingAttribute)}." ); } @@ -244,7 +232,7 @@ static internal void ValidateProperty( | Detect non-mapped relationships \-----------------------------------------------------------------------------------------------------------------------*/ if (attributeDescriptor.ModelType is ModelType.Relationship) { - ValidateRelationship(sourceType, configuration, attributeDescriptor, listType); + ValidateRelationship(sourceType, propertyAccessor, attributeDescriptor, listType); } /*------------------------------------------------------------------------------------------------------------------------ @@ -255,7 +243,7 @@ attributeDescriptor.ModelType is ModelType.NestedTopic && !typeof(ITopicBindingModel).IsAssignableFrom(listType) ) { throw new MappingModelValidationException( - $"The '{property.Name}' property on the '{sourceType.Name}' class has been determined to be a " + + $"The '{propertyAccessor.Name}' property on the '{sourceType.Name}' class has been determined to be a " + $"{configuration.CollectionType}, but the generic type '{listType.Name}' does not implement the " + $"{nameof(ITopicBindingModel)} interface. This is required for binding models. If this collection is not intended " + $"to be mapped as a {ModelType.NestedTopic} then update the definition in the associated " + @@ -269,11 +257,11 @@ attributeDescriptor.ModelType is ModelType.NestedTopic && \-----------------------------------------------------------------------------------------------------------------------*/ if ( attributeDescriptor.ModelType is ModelType.Reference && - !typeof(IAssociatedTopicBindingModel).IsAssignableFrom(propertyType) + !typeof(IAssociatedTopicBindingModel).IsAssignableFrom(propertyAccessor.Type) ) { throw new MappingModelValidationException( - $"The '{property.Name}' property on the '{sourceType.Name}' class has been determined to be a " + - $"{ModelType.Reference}, but the generic type '{propertyType.Name}' does not implement the " + + $"The '{propertyAccessor.Name}' property on the '{sourceType.Name}' class has been determined to be a " + + $"{ModelType.Reference}, but the generic type '{propertyAccessor.Type.Name}' does not implement the " + $"{nameof(IAssociatedTopicBindingModel)} interface. This is required for references. If this property is not " + $"intended to be mapped as a {ModelType.Reference} then update the definition in the associated " + $"{nameof(ContentTypeDescriptor)}. If this property is not intended to be mapped at all, include the " + @@ -293,8 +281,8 @@ attributeDescriptor.ModelType is ModelType.Reference && /// /// The binding model to validate. /// - /// - /// A describing a specific property of the . + /// + /// A describing a specific property of the . /// /// /// The object against which to validate the model. @@ -302,7 +290,7 @@ attributeDescriptor.ModelType is ModelType.Reference && /// The generic used for the corresponding . static internal void ValidateRelationship( [AllowNull]Type sourceType, - [AllowNull]PropertyConfiguration configuration, + [AllowNull]MemberAccessor propertyAccessor, [AllowNull]AttributeDescriptor attributeDescriptor, [DisallowNull]Type listType ) { @@ -311,21 +299,20 @@ [DisallowNull]Type listType | Validate parameters \-----------------------------------------------------------------------------------------------------------------------*/ Contract.Requires(sourceType, nameof(sourceType)); - Contract.Requires(configuration, nameof(configuration)); + Contract.Requires(propertyAccessor, nameof(propertyAccessor)); Contract.Requires(attributeDescriptor, nameof(attributeDescriptor)); - //Contract.Requires(listType, nameof(listType)); /*------------------------------------------------------------------------------------------------------------------------ | Define variables \-----------------------------------------------------------------------------------------------------------------------*/ - var property = configuration.Property; + var configuration = propertyAccessor.Configuration; /*------------------------------------------------------------------------------------------------------------------------ | Validate list \-----------------------------------------------------------------------------------------------------------------------*/ - if (!typeof(IList).IsAssignableFrom(property.PropertyType)) { + if (!typeof(IList).IsAssignableFrom(propertyAccessor.Type)) { throw new MappingModelValidationException( - $"The '{property.Name}' property on the '{sourceType.Name}' class maps to a relationship attribute " + + $"The '{propertyAccessor.Name}' property on the '{sourceType.Name}' class maps to a relationship attribute " + $"'{attributeDescriptor.Key}', but does not implement {nameof(IList)}. Relationships must implement " + $"{nameof(IList)} or derive from a collection that does." ); @@ -336,7 +323,7 @@ [DisallowNull]Type listType \-----------------------------------------------------------------------------------------------------------------------*/ if (!new[] { CollectionType.Any, CollectionType.Relationship }.Contains(configuration.CollectionType)) { throw new MappingModelValidationException( - $"The '{property.Name}' property on the '{sourceType.Name}' class maps to a relationship attribute " + + $"The '{propertyAccessor.Name}' property on the '{sourceType.Name}' class maps to a relationship attribute " + $"'{attributeDescriptor.Key}', but is configured as a {configuration.CollectionType}. The property should be " + $"flagged as either {nameof(CollectionType.Any)} or {nameof(CollectionType.Relationship)}." ); @@ -347,7 +334,7 @@ [DisallowNull]Type listType \-----------------------------------------------------------------------------------------------------------------------*/ if (!typeof(IAssociatedTopicBindingModel).IsAssignableFrom(listType)) { throw new MappingModelValidationException( - $"The '{property.Name}' property on the '{sourceType.Name}' class has been determined to be a " + + $"The '{propertyAccessor.Name}' property on the '{sourceType.Name}' class has been determined to be a " + $"{configuration.CollectionType}, but the generic type '{listType.Name}' does not implement the " + $"{nameof(IAssociatedTopicBindingModel)} interface. This is required for binding models. If this collection is not " + $"intended to be mapped as a {configuration.CollectionType} then update the definition in the associated " + diff --git a/OnTopic/Mapping/Reverse/IReverseTopicMappingService.cs b/OnTopic/Mapping/Reverse/IReverseTopicMappingService.cs index b1649a57..276795e1 100644 --- a/OnTopic/Mapping/Reverse/IReverseTopicMappingService.cs +++ b/OnTopic/Mapping/Reverse/IReverseTopicMappingService.cs @@ -3,7 +3,6 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ -using System.Threading.Tasks; using OnTopic.Mapping.Annotations; using OnTopic.Metadata; using OnTopic.Models; diff --git a/OnTopic/Mapping/Reverse/ReverseTopicMappingService.cs b/OnTopic/Mapping/Reverse/ReverseTopicMappingService.cs index 4a77de34..859a6e57 100644 --- a/OnTopic/Mapping/Reverse/ReverseTopicMappingService.cs +++ b/OnTopic/Mapping/Reverse/ReverseTopicMappingService.cs @@ -3,19 +3,12 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ -using System; using System.Collections; -using System.Collections.Generic; using System.ComponentModel; using System.Globalization; -using System.Linq; using System.Reflection; -using System.Threading.Tasks; -using OnTopic.Attributes; using OnTopic.Collections; -using OnTopic.Internal.Diagnostics; using OnTopic.Internal.Reflection; -using OnTopic.Mapping.Internal; using OnTopic.Metadata; using OnTopic.Models; using OnTopic.Repositories; @@ -28,11 +21,6 @@ namespace OnTopic.Mapping.Reverse { /// public class ReverseTopicMappingService : IReverseTopicMappingService { - /*========================================================================================================================== - | STATIC VARIABLES - \-------------------------------------------------------------------------------------------------------------------------*/ - static readonly MemberDispatcher _typeCache = new(); - /*========================================================================================================================== | PRIVATE VARIABLES \-------------------------------------------------------------------------------------------------------------------------*/ @@ -202,25 +190,19 @@ public ReverseTopicMappingService(ITopicRepository topicRepository) { \-----------------------------------------------------------------------------------------------------------------------*/ if (source is null) return target; - /*------------------------------------------------------------------------------------------------------------------------ - | Validate input - \-----------------------------------------------------------------------------------------------------------------------*/ - Contract.Requires(target, nameof(target)); - Contract.Assume(target.ContentType, nameof(target.ContentType)); - /*------------------------------------------------------------------------------------------------------------------------ | Validate model \-----------------------------------------------------------------------------------------------------------------------*/ - var properties = _typeCache.GetMembers(source.GetType()); + var typeAccessor = TypeAccessorCache.GetTypeAccessor(source.GetType()); var contentTypeDescriptor = _contentTypeDescriptors.GetValue(target.ContentType); - BindingModelValidator.ValidateModel(source.GetType(), properties, contentTypeDescriptor, attributePrefix); + BindingModelValidator.ValidateModel(typeAccessor, contentTypeDescriptor, attributePrefix); /*------------------------------------------------------------------------------------------------------------------------ | Loop through properties, mapping each one \-----------------------------------------------------------------------------------------------------------------------*/ var taskQueue = new List(); - foreach (var property in properties) { + foreach (var property in typeAccessor.GetMembers(MemberTypes.Property)) { taskQueue.Add(SetPropertyAsync(source, target, property, attributePrefix)); } await Task.WhenAll(taskQueue.ToArray()).ConfigureAwait(false); @@ -244,28 +226,21 @@ public ReverseTopicMappingService(ITopicRepository topicRepository) { /// The binding model from which to derive the data. Must inherit from . /// /// The entity to map the data to. - /// Information related to the current property. + /// Information related to the current property. /// The prefix to apply to the attributes. private async Task SetPropertyAsync( object source, Topic target, - PropertyInfo property, + MemberAccessor memberAccessor, string? attributePrefix = null ) { - /*------------------------------------------------------------------------------------------------------------------------ - | Validate parameters - \-----------------------------------------------------------------------------------------------------------------------*/ - Contract.Requires(source, nameof(source)); - Contract.Requires(target, nameof(target)); - Contract.Requires(property, nameof(property)); - /*------------------------------------------------------------------------------------------------------------------------ | Establish per-property variables \-----------------------------------------------------------------------------------------------------------------------*/ - var configuration = new PropertyConfiguration(property, attributePrefix); + var configuration = memberAccessor.Configuration; var contentTypeDescriptor = _contentTypeDescriptors.GetValue(target.ContentType); - var compositeAttributeKey = configuration.AttributeKey; + var compositeAttributeKey = configuration.GetCompositeAttributeKey(attributePrefix); Contract.Assume(contentTypeDescriptor, nameof(contentTypeDescriptor)); @@ -279,7 +254,7 @@ private async Task SetPropertyAsync( /*------------------------------------------------------------------------------------------------------------------------ | Skip properties injected by the compiler for record types \-----------------------------------------------------------------------------------------------------------------------*/ - if (configuration.Property.Name is "EqualityContract") { + if (memberAccessor.Name is "EqualityContract") { return; } @@ -288,7 +263,7 @@ private async Task SetPropertyAsync( \-----------------------------------------------------------------------------------------------------------------------*/ if (configuration.MapToParent) { await MapAsync( - property.GetValue(source), + memberAccessor.GetValue(source), target, configuration.AttributePrefix ).ConfigureAwait(false); @@ -302,30 +277,30 @@ await MapAsync( if (attributeType is null) { throw new MappingModelValidationException( - $"The attribute '{configuration.AttributeKey}' mapped by the {source.GetType()} could not be found on the " + + $"The attribute '{configuration.GetCompositeAttributeKey(attributePrefix)}' mapped by the {source.GetType()} could not be found on the " + $"'{contentTypeDescriptor.Key}' content type."); } /*------------------------------------------------------------------------------------------------------------------------ | Validate fields \-----------------------------------------------------------------------------------------------------------------------*/ - configuration.Validate(source); + memberAccessor.Validate(source); /*------------------------------------------------------------------------------------------------------------------------ | Handle property by type \-----------------------------------------------------------------------------------------------------------------------*/ switch (attributeType.ModelType) { case ModelType.ScalarValue: - SetScalarValue(source, target, configuration); + SetScalarValue(source, target, memberAccessor, attributePrefix); return; case ModelType.Relationship: - SetRelationships(source, target, configuration); + SetRelationships(source, target, memberAccessor, attributePrefix); return; case ModelType.NestedTopic: - await SetNestedTopicsAsync(source, target, configuration).ConfigureAwait(false); + await SetNestedTopicsAsync(source, target, memberAccessor, attributePrefix).ConfigureAwait(false); return; case ModelType.Reference: - SetReference(source, target, configuration); + SetReference(source, target, memberAccessor, attributePrefix); return; } @@ -338,40 +313,32 @@ await MapAsync( /// Sets an attribute on the target with a scalar value from the source binding model. /// /// - /// Assuming the 's is of the type , , , or , the method will attempt to set the property on the - /// . If the value is not set on the then the will be evaluated as a fallback. If the property is not of a settable type then the - /// property is not set. If the value is empty, then it will be treated as null in the 's - /// . + /// Assuming the 's property is of the type , , , or , the method will attempt to set the property on the . + /// If the value is not set on the then the will be + /// evaluated as a fallback. If the property is not of a settable type then the property is not set. If the value is + /// empty, then it will be treated as null in the 's . /// /// /// The binding model from which to derive the data. Must inherit from . /// /// The entity to map the data to. - /// - /// The with details about the property's attributes. - /// + /// The with details about the property's attributes. + /// The prefix to apply to the attributes. /// private static void SetScalarValue( object source, Topic target, - PropertyConfiguration configuration - ) - { - - /*------------------------------------------------------------------------------------------------------------------------ - | Validate parameters - \-----------------------------------------------------------------------------------------------------------------------*/ - Contract.Requires(source, nameof(source)); - Contract.Requires(target, nameof(target)); - Contract.Requires(configuration, nameof(configuration)); + MemberAccessor memberAccessor, + string? attributePrefix + ) { /*------------------------------------------------------------------------------------------------------------------------ | Attempt to retrieve value from the binding model property \-----------------------------------------------------------------------------------------------------------------------*/ - var attributeValue = _typeCache.GetPropertyValue(source, configuration.Property.Name)?.ToString(); + var configuration = memberAccessor.Configuration; + var attributeValue = memberAccessor.GetValue(source)?.ToString(); /*------------------------------------------------------------------------------------------------------------------------ | Fall back to default, if configured @@ -384,7 +351,7 @@ PropertyConfiguration configuration | Handle type conversion \-----------------------------------------------------------------------------------------------------------------------*/ if (attributeValue is not null) { - switch (configuration.Property.PropertyType.Name) { + switch (memberAccessor.Type.Name) { case nameof(Boolean): attributeValue = attributeValue is "True" ? "1" : "0"; break; @@ -394,7 +361,7 @@ PropertyConfiguration configuration /*------------------------------------------------------------------------------------------------------------------------ | Set the value (to null, if appropriate) \-----------------------------------------------------------------------------------------------------------------------*/ - target.Attributes.SetValue(configuration.AttributeKey, attributeValue); + target.Attributes.SetValue(configuration.GetCompositeAttributeKey(attributePrefix), attributeValue); } @@ -409,27 +376,24 @@ PropertyConfiguration configuration /// The binding model from which to derive the data. Must inherit from . /// /// The entity to map the data to. - /// - /// The with details about the property's attributes. - /// + /// The with details about the property's attributes. + /// The prefix to apply to the attributes. private void SetRelationships( object source, Topic target, - PropertyConfiguration configuration - ) - { + MemberAccessor memberAccessor, + string? attributePrefix + ) { /*------------------------------------------------------------------------------------------------------------------------ - | Validate parameters + | Establish configuration \-----------------------------------------------------------------------------------------------------------------------*/ - Contract.Requires(source, nameof(source)); - Contract.Requires(target, nameof(target)); - Contract.Requires(configuration, nameof(configuration)); + var configuration = memberAccessor.Configuration; /*------------------------------------------------------------------------------------------------------------------------ | Retrieve source list \-----------------------------------------------------------------------------------------------------------------------*/ - var sourceList = (IList?)configuration.Property.GetValue(source, null); + var sourceList = (IList?)memberAccessor.GetValue(source); if (sourceList is null) { sourceList = new List(); @@ -438,7 +402,7 @@ PropertyConfiguration configuration /*------------------------------------------------------------------------------------------------------------------------ | Clear existing relationships \-----------------------------------------------------------------------------------------------------------------------*/ - target.Relationships.Clear(configuration.AttributeKey); + target.Relationships.Clear(configuration.GetCompositeAttributeKey(attributePrefix)); /*------------------------------------------------------------------------------------------------------------------------ | Set relationships for each @@ -447,11 +411,11 @@ PropertyConfiguration configuration var targetTopic = _topicRepository.Load(relationship.UniqueKey, target); if (targetTopic is null) { throw new MappingModelValidationException( - $"The relationship '{relationship.UniqueKey}' mapped in the '{configuration.Property.Name}' property could not " + - $"be located in the repository." + $"The relationship '{relationship.UniqueKey}' mapped in the '{memberAccessor.Name}' property could not be " + + $"located in the repository." ); } - target.Relationships.SetValue(configuration.AttributeKey, targetTopic); + target.Relationships.SetValue(configuration.GetCompositeAttributeKey(attributePrefix), targetTopic); } } @@ -467,33 +431,31 @@ PropertyConfiguration configuration /// The binding model from which to derive the data. Must inherit from . /// /// The entity to map the data to. - /// - /// The with details about the property's attributes. - /// + /// The with details about the property's attributes. + /// The prefix to apply to the attributes. private async Task SetNestedTopicsAsync( object source, Topic target, - PropertyConfiguration configuration + MemberAccessor memberAccessor, + string? attributePrefix ) { /*------------------------------------------------------------------------------------------------------------------------ - | Validate parameters + | Establish configuration \-----------------------------------------------------------------------------------------------------------------------*/ - Contract.Requires(source, nameof(source)); - Contract.Requires(target, nameof(target)); - Contract.Requires(configuration, nameof(configuration)); + var configuration = memberAccessor.Configuration; /*------------------------------------------------------------------------------------------------------------------------ | Retrieve source list \-----------------------------------------------------------------------------------------------------------------------*/ - var sourceList = (IList?)configuration.Property.GetValue(source, null) ?? new List(); + var sourceList = (IList?)memberAccessor.GetValue(source) ?? new List(); /*------------------------------------------------------------------------------------------------------------------------ | Establish target collection to store mapped topics \-----------------------------------------------------------------------------------------------------------------------*/ - var container = target.Children.GetValue(configuration.AttributeKey); + var container = target.Children.GetValue(configuration.GetCompositeAttributeKey(attributePrefix)); if (container is null) { - container = TopicFactory.Create(configuration.AttributeKey, "List", target); + container = TopicFactory.Create(configuration.GetCompositeAttributeKey(attributePrefix), "List", target); container.IsHidden = true; } @@ -515,33 +477,31 @@ PropertyConfiguration configuration /// The binding model from which to derive the data. Must inherit from . /// /// The entity to map the data to. - /// - /// The with details about the property's attributes. - /// + /// The with details about the property's attributes. + /// The prefix to apply to the attributes. private void SetReference( object source, Topic target, - PropertyConfiguration configuration + MemberAccessor memberAccessor, + string? attributePrefix ) { /*------------------------------------------------------------------------------------------------------------------------ - | Validate parameters + | Establish configuration \-----------------------------------------------------------------------------------------------------------------------*/ - Contract.Requires(source, nameof(source)); - Contract.Requires(target, nameof(target)); - Contract.Requires(configuration, nameof(configuration)); + var configuration = memberAccessor.Configuration; /*------------------------------------------------------------------------------------------------------------------------ | Retrieve source value \-----------------------------------------------------------------------------------------------------------------------*/ - var modelReference = (IAssociatedTopicBindingModel?)configuration.Property.GetValue(source); + var modelReference = (IAssociatedTopicBindingModel?)memberAccessor.GetValue(source); /*------------------------------------------------------------------------------------------------------------------------ | Provide error handling \-----------------------------------------------------------------------------------------------------------------------*/ if (modelReference is null || modelReference.UniqueKey is null) { throw new MappingModelValidationException( - $"The {configuration.Property.Name} property must reference an object with its `UniqueKey` property set The " + + $"The {memberAccessor.Name} property must reference an object with its `UniqueKey` property set The " + $"value may be empty, but it should not be null." ); } @@ -557,18 +517,18 @@ PropertyConfiguration configuration if (modelReference.UniqueKey.Length > 0 && topicReference is null) { throw new MappingModelValidationException( $"The topic '{modelReference.UniqueKey}' referenced by the '{source.GetType()}' model's " + - $"'{configuration.Property.Name}' property could not be found." + $"'{memberAccessor.Name}' property could not be found." ); } /*------------------------------------------------------------------------------------------------------------------------ | Set target attribute \-----------------------------------------------------------------------------------------------------------------------*/ - if (configuration.AttributeKey.EndsWith("Id", StringComparison.Ordinal)) { - target.Attributes.SetValue(configuration.AttributeKey, topicReference?.Id.ToString(CultureInfo.InvariantCulture)); + if (configuration.GetCompositeAttributeKey(attributePrefix).EndsWith("Id", StringComparison.Ordinal)) { + target.Attributes.SetValue(configuration.GetCompositeAttributeKey(attributePrefix), topicReference?.Id.ToString(CultureInfo.InvariantCulture)); } else { - target.References.SetValue(configuration.AttributeKey, topicReference); + target.References.SetValue(configuration.GetCompositeAttributeKey(attributePrefix), topicReference); } } @@ -586,12 +546,6 @@ private async Task PopulateTargetCollectionAsync( KeyedTopicCollection targetList ) { - /*------------------------------------------------------------------------------------------------------------------------ - | Validate parameters - \-----------------------------------------------------------------------------------------------------------------------*/ - Contract.Requires(sourceList, nameof(sourceList)); - Contract.Requires(targetList, nameof(targetList)); - /*------------------------------------------------------------------------------------------------------------------------ | Queue up mapping tasks \-----------------------------------------------------------------------------------------------------------------------*/ diff --git a/OnTopic/Mapping/TopicMappingService.cs b/OnTopic/Mapping/TopicMappingService.cs index 1fc55040..eda0b87d 100644 --- a/OnTopic/Mapping/TopicMappingService.cs +++ b/OnTopic/Mapping/TopicMappingService.cs @@ -3,16 +3,9 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ -using System; using System.Collections; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.Linq; using System.Reflection; -using System.Threading.Tasks; -using OnTopic.Attributes; using OnTopic.Collections.Specialized; -using OnTopic.Internal.Diagnostics; using OnTopic.Internal.Reflection; using OnTopic.Lookup; using OnTopic.Mapping.Annotations; @@ -31,16 +24,12 @@ namespace OnTopic.Mapping { /// public class TopicMappingService : ITopicMappingService { - /*========================================================================================================================== - | STATIC VARIABLES - \-------------------------------------------------------------------------------------------------------------------------*/ - static readonly MemberDispatcher _typeCache = new(); - /*========================================================================================================================== | PRIVATE VARIABLES \-------------------------------------------------------------------------------------------------------------------------*/ readonly ITopicRepository _topicRepository; readonly ITypeLookupService _typeLookupService; + static readonly TypeAccessor _topicTypeAccessor = TypeAccessorCache.GetTypeAccessor(); /*========================================================================================================================== | CONSTRUCTOR @@ -179,9 +168,12 @@ public TopicMappingService(ITopicRepository topicRepository, ITypeLookupService /*------------------------------------------------------------------------------------------------------------------------ | Identify parameters \-----------------------------------------------------------------------------------------------------------------------*/ - var constructorInfo = _typeCache.GetMembers(type).Where(c => c.IsPublic).FirstOrDefault(); - var parameters = constructorInfo?.GetParameters()?? Array.Empty(); - var arguments = new object?[parameters.Length]; + var typeAccessor = TypeAccessorCache.GetTypeAccessor(type); + var properties = typeAccessor.GetMembers(MemberTypes.Property); + var parameters = typeAccessor.ConstructorParameters; + var arguments = new object?[parameters.Count]; + var attributeArguments = (IDictionary)new Dictionary(); + var parameterQueue = new Dictionary>(); /*------------------------------------------------------------------------------------------------------------------------ | Pre-cache entry @@ -194,18 +186,53 @@ public TopicMappingService(ITopicRepository topicRepository, ITypeLookupService cache.Preregister(topic.Id, type); /*------------------------------------------------------------------------------------------------------------------------ - | Set parameters - \-----------------------------------------------------------------------------------------------------------------------*/ - var parameterQueue = new Dictionary>(); + | Handle AttributeDictionary constructor + >------------------------------------------------------------------------------------------------------------------------- + | A model may optionally expose a constructor with a single parameter accepting an AttributeDictionary. In this scenario, + | the TopicMappingService may optionally pass a lightweight AttributeDictionary, allowing the model's constructor to + | populate scalar values, instead of relying on reflection. + \-----------------------------------------------------------------------------------------------------------------------*/ + if (parameters.Count is 1 && parameters[0].Type == typeof(AttributeDictionary)) { + + // This strategy is only performant if there are quite a several scalar properties and they are well-covered by the + // attributes. As a fast heuristic to evaluate this, we expect five or more attributes and three or more compatible + // properties. In practice, this should be benefitial with any more than mapped attributes, but we also expect that most + // topics will have 2-3 excluded or unmapped attributes (e.g., Title, LastModified). With models, we can be a bit more + // intelligent, by excluding any members that are likely compatible with Topic properties, thus exluding e.g., Id, Key, + // WebPath, etc. This doesn't guarantee that the attributes map to the properties, but a more accurate evaluation would + // undermine the performance benefits of this optimization. + if (topic.Attributes.Count >= 5 && properties.Count(p => !p.MaybeCompatible) >= 3) { + var attributes = topic.Attributes.AsAttributeDictionary(true); + arguments[0] = attributes; + attributeArguments = attributes; + } + else { + parameters = new(); + arguments = Array.Empty(); + } - foreach (var parameter in parameters) { - parameterQueue.Add(parameter.Position, GetParameterAsync(topic, associations, parameter, cache, attributePrefix)); } - await Task.WhenAll(parameterQueue.Values).ConfigureAwait(false); + /*------------------------------------------------------------------------------------------------------------------------ + | Handle other constructors + >------------------------------------------------------------------------------------------------------------------------- + | A model may optionally expose a constructor with multiple parameters, which can be defined via reflection in the same + | way as properties would be. This is especially useful for records using the positional syntax (i.e., where properties + | are defined using the constructor). This also, optionally, provides the model with more control, where needed, over how + | it's constructed. + \-----------------------------------------------------------------------------------------------------------------------*/ + else { + + foreach (var parameter in parameters) { + parameterQueue.Add(parameter.ParameterInfo.Position, GetParameterAsync(topic, associations, parameter, cache, attributePrefix)); + } + + await Task.WhenAll(parameterQueue.Values).ConfigureAwait(false); + + foreach (var parameter in parameterQueue) { + arguments[parameter.Key] = await parameter.Value.ConfigureAwait(false); + } - foreach (var parameter in parameterQueue) { - arguments[parameter.Key] = parameter.Value.Result; } /*------------------------------------------------------------------------------------------------------------------------ @@ -227,9 +254,9 @@ public TopicMappingService(ITopicRepository topicRepository, ITypeLookupService | Loop through properties, mapping each one \-----------------------------------------------------------------------------------------------------------------------*/ var propertyQueue = new List(); - var mappedParameters = parameters.Select(p => p.Name); + var mappedParameters = parameters.Select(p => p.Name).Union(attributeArguments.Select(a => a.Key)); - foreach (var property in _typeCache.GetMembers(target.GetType())) { + foreach (var property in typeAccessor.GetMembers(MemberTypes.Property)) { if (!mappedParameters.Contains(property.Name, StringComparer.OrdinalIgnoreCase)) { propertyQueue.Add(SetPropertyAsync(topic, target, associations, property, cache, attributePrefix, false)); } @@ -298,7 +325,7 @@ private async Task MapAsync( /*------------------------------------------------------------------------------------------------------------------------ | Handle topics \-----------------------------------------------------------------------------------------------------------------------*/ - if (typeof(Topic).IsAssignableFrom(target.GetType())) { + if (target is Topic) { return topic; } @@ -311,7 +338,7 @@ private async Task MapAsync( if (cache.TryGetValue(topic.Id, target.GetType(), out var cacheEntry)) { associations = cacheEntry.GetMissingAssociations(associations); target = cacheEntry.MappedTopic; - if (associations == AssociationTypes.None) { + if (associations is AssociationTypes.None) { return cacheEntry.MappedTopic; } cacheEntry.AddMissingAssociations(associations); @@ -328,8 +355,9 @@ private async Task MapAsync( | Loop through properties, mapping each one \-----------------------------------------------------------------------------------------------------------------------*/ var taskQueue = new List(); + var typeAccessor = TypeAccessorCache.GetTypeAccessor(target.GetType()); - foreach (var property in _typeCache.GetMembers(target.GetType())) { + foreach (var property in typeAccessor.GetMembers(MemberTypes.Property)) { taskQueue.Add(SetPropertyAsync(topic, target, associations, property, cache, attributePrefix, cacheEntry is not null)); } await Task.WhenAll(taskQueue.ToArray()).ConfigureAwait(false); @@ -356,47 +384,60 @@ private async Task MapAsync( private async Task GetParameterAsync( Topic source, AssociationTypes associations, - ParameterInfo parameter, + ParameterMetadata parameter, MappedTopicCache cache, string? attributePrefix = null ) { - var configuration = new ItemConfiguration(parameter, parameter.Name, attributePrefix); + /*------------------------------------------------------------------------------------------------------------------------ + | Establish per-property variables + \-----------------------------------------------------------------------------------------------------------------------*/ + var configuration = parameter.Configuration; + /*------------------------------------------------------------------------------------------------------------------------ + | Bypass if mapping is disabled + \-----------------------------------------------------------------------------------------------------------------------*/ if (configuration.DisableMapping) { - return parameter.DefaultValue; + return parameter.ParameterInfo.DefaultValue; } - var value = await GetValue(source, parameter.ParameterType, associations, configuration, cache, false).ConfigureAwait(false); - - if (value is null && IsList(parameter.ParameterType)) { - return await getList(parameter.ParameterType, configuration).ConfigureAwait(false); - } - else if (configuration.MapToParent) { + /*------------------------------------------------------------------------------------------------------------------------ + | Handle [MapToParent] attribute + \-----------------------------------------------------------------------------------------------------------------------*/ + if (configuration.MapToParent) { return await MapAsync( source, - parameter.ParameterType, + parameter.Type, associations, cache, - configuration.AttributePrefix + configuration.AttributePrefix + attributePrefix ).ConfigureAwait(false); } + /*------------------------------------------------------------------------------------------------------------------------ + | Determine value + \-----------------------------------------------------------------------------------------------------------------------*/ + var value = await GetValue(source, parameter.Type, associations, parameter, cache, attributePrefix, false).ConfigureAwait(false); + + if (value is null && parameter.IsList) { + return await getList(parameter.Type).ConfigureAwait(false); + } + return value; /*------------------------------------------------------------------------------------------------------------------------ | Get List Function \-----------------------------------------------------------------------------------------------------------------------*/ - async Task getList(Type targetType, ItemConfiguration configuration) { + async Task getList(Type targetType) { - var sourceList = GetSourceCollection(source, associations, configuration); + var sourceList = GetSourceCollection(source, associations, parameter, attributePrefix); var targetList = InitializeCollection(targetType); if (sourceList is null || targetList is null) { return (IList?)null; } - await PopulateTargetCollectionAsync(sourceList, targetList, configuration, cache).ConfigureAwait(false); + await PopulateTargetCollectionAsync(sourceList, targetList, parameter, cache).ConfigureAwait(false); return targetList; @@ -414,7 +455,7 @@ private async Task MapAsync( /// The entity to derive the data from. /// The target object to map the data to. /// Determines what associations the mapping should include, if any. - /// Information related to the current property. + /// Information related to the current property. /// A cache to keep track of already-mapped object instances. /// The prefix to apply to the attributes. /// Determines if properties not associated with associations should be mapped. @@ -422,61 +463,57 @@ private async Task SetPropertyAsync( Topic source, object target, AssociationTypes associations, - PropertyInfo property, + MemberAccessor propertyAccessor, MappedTopicCache cache, string? attributePrefix = null, bool mapAssociationsOnly = false ) { - /*------------------------------------------------------------------------------------------------------------------------ - | Validate parameters - \-----------------------------------------------------------------------------------------------------------------------*/ - Contract.Requires(source, nameof(source)); - Contract.Requires(target, nameof(target)); - Contract.Requires(associations, nameof(associations)); - Contract.Requires(property, nameof(property)); - Contract.Requires(cache, nameof(cache)); - /*------------------------------------------------------------------------------------------------------------------------ | Establish per-property variables \-----------------------------------------------------------------------------------------------------------------------*/ - var configuration = new PropertyConfiguration(property, attributePrefix); + var configuration = propertyAccessor.Configuration; /*------------------------------------------------------------------------------------------------------------------------ - | Handle by type, attribute + | Bypass if mapping is disabled \-----------------------------------------------------------------------------------------------------------------------*/ if (configuration.DisableMapping) { return; } - var value = await GetValue(source, property.PropertyType, associations, configuration, cache, mapAssociationsOnly).ConfigureAwait(false); - - if (value is null && IsList(property.PropertyType)) { - await SetCollectionValueAsync(source, target, associations, configuration, cache).ConfigureAwait(false); - } - else if (configuration.MapToParent) { - var targetProperty = property.GetValue(target); + /*------------------------------------------------------------------------------------------------------------------------ + | Handle [MapToParent] attribute + \-----------------------------------------------------------------------------------------------------------------------*/ + if (configuration.MapToParent) { + var targetProperty = propertyAccessor.GetValue(target); if (targetProperty is not null) { await MapAsync( source, targetProperty, associations, cache, - configuration.AttributePrefix + configuration.AttributePrefix + attributePrefix ).ConfigureAwait(false); } } - else if (value != null && _typeCache.HasSettableProperty(target.GetType(), property.Name)) { - _typeCache.SetPropertyValue(target, configuration.Property.Name, value); - } - else if (value != null && _typeCache.HasSettableProperty(target.GetType(), property.Name, property.PropertyType)) { - _typeCache.SetPropertyValue(target, configuration.Property.Name, value); + + /*------------------------------------------------------------------------------------------------------------------------ + | Determine value + \-----------------------------------------------------------------------------------------------------------------------*/ + else { + var value = await GetValue(source, propertyAccessor.Type, associations, propertyAccessor, cache, attributePrefix, mapAssociationsOnly).ConfigureAwait(false); + if (value is null && propertyAccessor.IsList) { + await SetCollectionValueAsync(source, target, associations, propertyAccessor, cache, attributePrefix).ConfigureAwait(false); + } + else if (value != null && propertyAccessor.CanWrite) { + propertyAccessor.SetValue(target, value, true); + } } /*------------------------------------------------------------------------------------------------------------------------ | Validate fields \-----------------------------------------------------------------------------------------------------------------------*/ - configuration.Validate(target); + propertyAccessor.Validate(target); } @@ -489,35 +526,24 @@ await MapAsync( /// The entity to derive the data from. /// The of the target parameter or property. /// Determines what associations the mapping should include, if any. - /// Information related to the current parameter or property. + /// Information related to the current parameter or property. /// A cache to keep track of already-mapped object instances. + /// The prefix to apply to the attributes. /// Determines if properties not associated with associations should be mapped. private async Task GetValue( Topic source, Type targetType, AssociationTypes associations, - ItemConfiguration configuration, + ItemMetadata itemMetadata, MappedTopicCache cache, - bool mapAssociationsOnly = false + string? attributePrefix = "", + bool mapAssociationsOnly = false ) { /*------------------------------------------------------------------------------------------------------------------------ - | Validate parameters + | Establish configuration \-----------------------------------------------------------------------------------------------------------------------*/ - Contract.Requires(source, nameof(source)); - Contract.Requires(associations, nameof(associations)); - Contract.Requires(configuration, nameof(configuration)); - Contract.Requires(cache, nameof(cache)); - - /*------------------------------------------------------------------------------------------------------------------------ - | Establish per-property variables - \-----------------------------------------------------------------------------------------------------------------------*/ - var topicReferenceId = source.Attributes.GetInteger($"{configuration.AttributeKey}Id", 0); - var topicReference = source.References.GetValue(configuration.AttributeKey); - - if (topicReferenceId == 0 && configuration.AttributeKey.EndsWith("Id", StringComparison.OrdinalIgnoreCase)) { - topicReferenceId = source.Attributes.GetInteger(configuration.AttributeKey, 0); - } + var configuration = itemMetadata.Configuration; /*------------------------------------------------------------------------------------------------------------------------ | Assign default value @@ -530,41 +556,63 @@ await MapAsync( /*------------------------------------------------------------------------------------------------------------------------ | Handle by type, attribute \-----------------------------------------------------------------------------------------------------------------------*/ - if (TryGetCompatibleProperty(source, targetType, configuration, out var compatibleValue)) { + if (TryGetCompatibleProperty(source, targetType, itemMetadata, attributePrefix, out var compatibleValue)) { value = compatibleValue; } - else if (!mapAssociationsOnly && AttributeValueConverter.IsConvertible(targetType)) { - value = GetScalarValue(source, configuration); + else if (itemMetadata.IsConvertible) { + if (!mapAssociationsOnly) { + value = GetScalarValue(source, itemMetadata, attributePrefix); + } } - else if (IsList(targetType)) { + else if (itemMetadata.IsList) { return null; } - else if (configuration.AttributeKey is "Parent" && associations.HasFlag(AssociationTypes.Parents)) { - if (source.Parent is not null) { - value = await GetTopicReferenceAsync(source.Parent, targetType, configuration, cache).ConfigureAwait(false); + else if (configuration.GetCompositeAttributeKey(attributePrefix) is "Parent") { + if (associations.HasFlag(AssociationTypes.Parents) && source.Parent is not null) { + value = await GetTopicReferenceAsync(source.Parent, targetType, itemMetadata, cache).ConfigureAwait(false); } } - else if ( - topicReference is not null && - associations.HasFlag(AssociationTypes.References) - ) { - value = await GetTopicReferenceAsync(topicReference, targetType, configuration, cache).ConfigureAwait(false); + else if (configuration.MapToParent) { + return null; } - else if (topicReferenceId > 0 && associations.HasFlag(AssociationTypes.References)) { - topicReference = _topicRepository.Load(topicReferenceId, source); + else if (itemMetadata.Type.IsClass && associations.HasFlag(AssociationTypes.References)) { + var topicReference = getTopicReference(); if (topicReference is not null) { - value = await GetTopicReferenceAsync(topicReference, targetType, configuration, cache).ConfigureAwait(false); + value = await GetTopicReferenceAsync(topicReference, targetType, itemMetadata, cache).ConfigureAwait(false); } } - else if (configuration.MapToParent) { - return null; - } /*------------------------------------------------------------------------------------------------------------------------ | Return value \-----------------------------------------------------------------------------------------------------------------------*/ return value; + /*------------------------------------------------------------------------------------------------------------------------ + | Get Topic Reference + \-----------------------------------------------------------------------------------------------------------------------*/ + Topic? getTopicReference() { + + // Check for standard topic reference + var topicReference = source.References.GetValue(configuration.GetCompositeAttributeKey(attributePrefix)); + if (topicReference is not null) { + return topicReference; + } + + int topicReferenceId; + if (configuration.GetCompositeAttributeKey(attributePrefix).EndsWith("Id", StringComparison.OrdinalIgnoreCase)) { + topicReferenceId = source.Attributes.GetInteger(configuration.GetCompositeAttributeKey(attributePrefix), 0); + } + else { + topicReferenceId = source.Attributes.GetInteger($"{configuration.GetCompositeAttributeKey(attributePrefix)}Id", 0); + } + if (topicReferenceId > 0) { + topicReference = _topicRepository.Load(topicReferenceId, source); + } + + return topicReference; + + } + } /*========================================================================================================================== @@ -574,40 +622,50 @@ topicReference is not null && /// Gets a scalar property from a . /// /// - /// The method will attempt to retrieve the value from the - /// based on, in order, the 's Get{Property}() method, - /// {Property} property, and, finally, its collection (using method will attempt to retrieve the value from the + /// based on, in order, the 's Get{Property}() method, + /// {Property} property, and, finally, its collection (using ). /// /// The source from which to pull the value. - /// The with details about the property's attributes. + /// The with details about the property's attributes. + /// The prefix to apply to the attributes. /// - private static object? GetScalarValue(Topic source, ItemConfiguration configuration) { + private static object? GetScalarValue(Topic source, ItemMetadata itemMetadata, string? attributePrefix) { /*------------------------------------------------------------------------------------------------------------------------ - | Validate parameters + | Establish configuration \-----------------------------------------------------------------------------------------------------------------------*/ - Contract.Requires(source, nameof(source)); - Contract.Requires(configuration, nameof(configuration)); + var configuration = itemMetadata.Configuration; /*------------------------------------------------------------------------------------------------------------------------ | Attempt to retrieve value from topic.Get{Property}() \-----------------------------------------------------------------------------------------------------------------------*/ - var attributeValue = _typeCache.GetMethodValue(source, $"Get{configuration.AttributeKey}")?.ToString(); + var typeAccessor = GetTopicAccessor(source.GetType()); + + var attributeValue = (string?)null; + var maybeCompatible = source.GetType() != typeof(Topic) || itemMetadata.MaybeCompatible; + + /*------------------------------------------------------------------------------------------------------------------------ + | Attempt to retrieve value from topic.{Property} + \-----------------------------------------------------------------------------------------------------------------------*/ + if (maybeCompatible) { + attributeValue = typeAccessor.GetMethodValue(source, $"Get{configuration.GetCompositeAttributeKey(attributePrefix)}")?.ToString(); + } /*------------------------------------------------------------------------------------------------------------------------ | Attempt to retrieve value from topic.{Property} \-----------------------------------------------------------------------------------------------------------------------*/ - if (String.IsNullOrEmpty(attributeValue)) { - attributeValue = _typeCache.GetPropertyValue(source, configuration.AttributeKey)?.ToString(); + if (maybeCompatible && attributeValue is null) { + attributeValue = typeAccessor.GetPropertyValue(source, configuration.GetCompositeAttributeKey(attributePrefix))?.ToString(); } /*------------------------------------------------------------------------------------------------------------------------ | Otherwise, attempt to retrieve value from topic.Attributes.GetValue({Property}) \-----------------------------------------------------------------------------------------------------------------------*/ - if (String.IsNullOrEmpty(attributeValue)) { + if (attributeValue is null) { attributeValue = source.Attributes.GetValue( - configuration.AttributeKey, + configuration.GetCompositeAttributeKey(attributePrefix), configuration.DefaultValue?.ToString(), configuration.InheritValue ); @@ -620,38 +678,6 @@ topicReference is not null && } - /*========================================================================================================================== - | PRIVATE: IS LIST? - \-------------------------------------------------------------------------------------------------------------------------*/ - /// - /// Given a type, determines whether it's a list that is recognized by the . - /// - /// - /// - /// To qualify, the must either implement , or it must be of type , , or —any of which, if null, will be - /// instantiated as a new . - /// - /// - /// It is technically possible for the to implement one of the interfaces, such as , while the assigned reference type is not compatible with the interface - /// required by e.g. . Detecting this requires looping through the interface implementations which is comparatively more costly given - /// the number of times gets called. In practice, collections that implement e.g. are expected to also support . If they don't, however, the mapping will throw an - /// exception since the assigned value will not be castable to an . - /// - /// - /// The of collection to initialize. - private static bool IsList(Type targetType) => - typeof(IList).IsAssignableFrom(targetType) || - targetType.IsGenericType && - ( - targetType.GetGenericTypeDefinition() == typeof(IEnumerable<>) || - targetType.GetGenericTypeDefinition() == typeof(ICollection<>) || - targetType.GetGenericTypeDefinition() == typeof(IList<>) - ); - /*========================================================================================================================== | PRIVATE: INITIALIZE COLLECTION \-------------------------------------------------------------------------------------------------------------------------*/ @@ -661,18 +687,6 @@ private static bool IsList(Type targetType) => /// The of collection to initialize. private static IList? InitializeCollection(Type targetType) { - /*------------------------------------------------------------------------------------------------------------------------ - | Validate parameters - \-----------------------------------------------------------------------------------------------------------------------*/ - Contract.Requires(targetType, nameof(targetType)); - - /*------------------------------------------------------------------------------------------------------------------------ - | Escape clause if preconditions are not met - \-----------------------------------------------------------------------------------------------------------------------*/ - if (!IsList(targetType)) { - return (IList?)null; - } - /*------------------------------------------------------------------------------------------------------------------------ | Attempt to create specific type \-----------------------------------------------------------------------------------------------------------------------*/ @@ -711,60 +725,47 @@ private static bool IsList(Type targetType) => /// target collection. /// /// - /// Given a collection on a DTO, attempts to identify a source + /// Given a collection on a DTO, attempts to identify a source /// collection on the . Collections can be mapped to , , or to a nested topic (which will be part of /// ). By default, will attempt to map based on the - /// property name, though this behavior can be modified using the , based on annotations + /// property name, though this behavior can be modified using the , based on annotations /// on the DTO. /// /// The source from which to pull the value. /// The target DTO on which to set the property value. /// Determines what associations the mapping should include, if any. - /// - /// The with details about the property's attributes. - /// + /// The with details about the property's attributes. /// A cache to keep track of already-mapped object instances. + /// The prefix to apply to the attributes. private async Task SetCollectionValueAsync( Topic source, object target, AssociationTypes associations, - PropertyConfiguration configuration, - MappedTopicCache cache + MemberAccessor memberAccessor, + MappedTopicCache cache, + string? attributePrefix ) { - /*------------------------------------------------------------------------------------------------------------------------ - | Validate parameters - \-----------------------------------------------------------------------------------------------------------------------*/ - Contract.Requires(source, nameof(source)); - Contract.Requires(associations, nameof(associations)); - Contract.Requires(configuration, nameof(configuration)); - Contract.Requires(cache, nameof(cache)); - - /*------------------------------------------------------------------------------------------------------------------------ - | Escape clause if preconditions are not met - \-----------------------------------------------------------------------------------------------------------------------*/ - if (!IsList(configuration.Property.PropertyType)) return; - /*------------------------------------------------------------------------------------------------------------------------ | Ensure target list is created \-----------------------------------------------------------------------------------------------------------------------*/ - var targetList = (IList?)configuration.Property.GetValue(target, null); + var targetList = (IList?)memberAccessor.GetValue(target); if (targetList is null) { - targetList = InitializeCollection(configuration.Property.PropertyType); - configuration.Property.SetValue(target, targetList); + targetList = InitializeCollection(memberAccessor.Type); + memberAccessor.SetValue(target, targetList); } Contract.Assume( targetList, - $"The target list type, '{configuration.Property.PropertyType}', could not be properly constructed, as required to " + - $"map the '{configuration.Property.Name}' property on the '{target?.GetType().Name}' object." + $"The target list type, '{memberAccessor.Type}', could not be properly constructed, as required to " + + $"map the '{memberAccessor.Name}' property on the '{target?.GetType().Name}' object." ); /*------------------------------------------------------------------------------------------------------------------------ | Establish source collection to store topics to be mapped \-----------------------------------------------------------------------------------------------------------------------*/ - var sourceList = GetSourceCollection(source, associations, configuration); + var sourceList = GetSourceCollection(source, associations, memberAccessor, attributePrefix); /*------------------------------------------------------------------------------------------------------------------------ | Validate that source collection was identified @@ -774,7 +775,7 @@ MappedTopicCache cache /*------------------------------------------------------------------------------------------------------------------------ | Map the topics from the source collection, and add them to the target collection \-----------------------------------------------------------------------------------------------------------------------*/ - await PopulateTargetCollectionAsync(sourceList, targetList, configuration, cache).ConfigureAwait(false); + await PopulateTargetCollectionAsync(sourceList, targetList, memberAccessor, cache).ConfigureAwait(false); } @@ -785,30 +786,28 @@ MappedTopicCache cache /// Given a source topic and a property configuration, attempts to identify a source collection that maps to the property. /// /// - /// Given a collection on a target DTO, attempts to identify a source collection on the + /// Given a collection on a target DTO, attempts to identify a source collection on the /// . Collections can be mapped to , , or to a nested topic (which will be part of /// ). By default, will attempt to map based on the - /// property name, though this behavior can be modified using the , based on annotations + /// property name, though this behavior can be modified using the , based on annotations /// on the target DTO. /// /// The source from which to pull the value. /// Determines what associations the mapping should include, if any. - /// - /// The with details about the property's attributes. - /// - private IList GetSourceCollection(Topic source, AssociationTypes associations, ItemConfiguration configuration) { - - /*------------------------------------------------------------------------------------------------------------------------ - | Validate parameters - \-----------------------------------------------------------------------------------------------------------------------*/ - Contract.Requires(source, nameof(source)); - Contract.Requires(associations, nameof(associations)); - Contract.Requires(configuration, nameof(configuration)); + /// The with details about the property's attributes. + /// The prefix to apply to the attributes. + private IList GetSourceCollection( + Topic source, + AssociationTypes associations, + ItemMetadata itemMetadata, + string? attributePrefix + ) { /*------------------------------------------------------------------------------------------------------------------------ | Establish source collection to store topics to be mapped \-----------------------------------------------------------------------------------------------------------------------*/ + var configuration = itemMetadata.Configuration; var listSource = (IList)Array.Empty(); var collectionKey = configuration.CollectionKey; var collectionType = configuration.CollectionType; @@ -857,11 +856,11 @@ private IList GetSourceCollection(Topic source, AssociationTypes associat //For example, the ContentTypeDescriptor's AttributeDescriptors collection, which provides a rollup of //AttributeDescriptors from the current ContentTypeDescriptor, as well as all of its ascendents. if (listSource.Count == 0) { - var sourceProperty = _typeCache.GetMember(source.GetType(), configuration.AttributeKey); + var sourceProperty = TypeAccessorCache.GetTypeAccessor(source.GetType()).GetMember(configuration.GetCompositeAttributeKey(attributePrefix)); if ( sourceProperty?.GetValue(source) is IList sourcePropertyValue && sourcePropertyValue.Count > 0 && - typeof(Topic).IsAssignableFrom(sourcePropertyValue[0]?.GetType()) + sourcePropertyValue[0] is Topic ) { listSource = getCollection( CollectionType.MappedCollection, @@ -897,7 +896,7 @@ private IList GetSourceCollection(Topic source, AssociationTypes associat | Provide local function for evaluating current collection \-----------------------------------------------------------------------------------------------------------------------*/ IList getCollection(CollectionType collection, Func contains, Func> getTopics) { - var targetAssociations = AssociationMap.Mappings[collection]; + var targetAssociations = AssociationMap.Mappings[collection]; var preconditionsMet = listSource.Count == 0 && (collectionType is CollectionType.Any || collectionType.Equals(collection)) && @@ -917,24 +916,19 @@ IList getCollection(CollectionType collection, Func contain /// /// The to pull the source objects from. /// The target to add the mapped objects to. - /// - /// The with details about the property's attributes. - /// + /// The with details about the property's attributes. /// A cache to keep track of already-mapped object instances. private async Task PopulateTargetCollectionAsync( IList sourceList, IList targetList, - ItemConfiguration configuration, + ItemMetadata itemMetadata, MappedTopicCache cache ) { /*------------------------------------------------------------------------------------------------------------------------ - | Validate parameters + | Establish configuration \-----------------------------------------------------------------------------------------------------------------------*/ - Contract.Requires(sourceList, nameof(sourceList)); - Contract.Requires(targetList, nameof(targetList)); - Contract.Requires(configuration, nameof(configuration)); - Contract.Requires(cache, nameof(cache)); + var configuration = itemMetadata.Configuration; /*------------------------------------------------------------------------------------------------------------------------ | Determine the type of item in the list @@ -954,11 +948,18 @@ MappedTopicCache cache foreach (var childTopic in sourceList) { - //Ensure the source topic matches any [FilterByAttribute()] settings - if (!configuration.SatisfiesAttributeFilters(childTopic)) { + //Ensure the source topic isn't disabled; disabled topics should never be returned to the presentation layer unless + //explicitly requested by a top-level request. + if (childTopic.IsDisabled) { continue; } + //Skip nested topics; those should be explicitly mapped to their own collection or topic reference + if (childTopic.ContentType.Equals("List", StringComparison.OrdinalIgnoreCase)) { + continue; + } + + //Ensure the source topic matches any [FilterByContentType()] settings if ( configuration.ContentTypeFilter is not null && !childTopic.ContentType.Equals(configuration.ContentTypeFilter, StringComparison.OrdinalIgnoreCase) @@ -966,14 +967,8 @@ configuration.ContentTypeFilter is not null && continue; } - //Skip nested topics; those should be explicitly mapped to their own collection or topic reference - if (childTopic.ContentType.Equals("List", StringComparison.OrdinalIgnoreCase)) { - continue; - } - - //Ensure the source topic isn't disabled; disabled topics should never be returned to the presentation layer unless - //explicitly requested by a top-level request. - if (childTopic.IsDisabled) { + //Ensure the source topic matches any [FilterByAttribute()] settings + if (!configuration.SatisfiesAttributeFilters(childTopic)) { continue; } @@ -986,7 +981,7 @@ configuration.ContentTypeFilter is not null && } } else { - AddToList(childDto); + addToList(childDto); } } @@ -999,22 +994,20 @@ configuration.ContentTypeFilter is not null && var dto = await dtoTask.ConfigureAwait(false); taskQueue.Remove(dtoTask); if (dto is not null) { - AddToList(dto); + addToList(dto); } } /*------------------------------------------------------------------------------------------------------------------------ | Function: Add to List \-----------------------------------------------------------------------------------------------------------------------*/ - void AddToList(object dto) { - if (dto is not null) { - try { - targetList.Add(dto); - } - catch (ArgumentException) { - //Ignore exceptions caused by duplicate keys, in case the IList represents a keyed collection - //We would defensively check for this, except IList doesn't provide a suitable method to do so - } + void addToList(object dto) { + try { + targetList.Add(dto); + } + catch (ArgumentException) { + //Ignore exceptions caused by duplicate keys, in case the IList represents a keyed collection + //We would defensively check for this, except IList doesn't provide a suitable method to do so } } @@ -1056,22 +1049,19 @@ void AddToList(object dto) { /// /// The source from which to pull the value. /// The expected for the mapped . - /// The with details about the item's attributes. + /// The with details about the item's attributes. /// A cache to keep track of already-mapped object instances. private async Task GetTopicReferenceAsync( Topic source, Type targetType, - ItemConfiguration configuration, + ItemMetadata itemMetadata, MappedTopicCache cache ) { /*------------------------------------------------------------------------------------------------------------------------ - | Validate parameters + | Establish configuration \-----------------------------------------------------------------------------------------------------------------------*/ - Contract.Requires(source, nameof(source)); - Contract.Requires(targetType, nameof(targetType)); - Contract.Requires(configuration, nameof(configuration)); - Contract.Requires(cache, nameof(cache)); + var configuration = itemMetadata.Configuration; /*------------------------------------------------------------------------------------------------------------------------ | Bypass disabled topics @@ -1092,13 +1082,6 @@ MappedTopicCache cache topicDto = await MapAsync(source, mappingType, configuration.IncludeAssociations, cache).ConfigureAwait(false); } - /*------------------------------------------------------------------------------------------------------------------------ - | Validate results - \-----------------------------------------------------------------------------------------------------------------------*/ - if (topicDto is null) { - return null; - } - /*------------------------------------------------------------------------------------------------------------------------ | Return type \-----------------------------------------------------------------------------------------------------------------------*/ @@ -1116,12 +1099,6 @@ MappedTopicCache cache /// The list of instances to add each child to. private IList FlattenTopicGraph(Topic source, IList targetList) { - /*------------------------------------------------------------------------------------------------------------------------ - | Validate parameters - \-----------------------------------------------------------------------------------------------------------------------*/ - Contract.Requires(source, nameof(source)); - Contract.Requires(targetList, nameof(targetList)); - /*------------------------------------------------------------------------------------------------------------------------ | Validate source properties \-----------------------------------------------------------------------------------------------------------------------*/ @@ -1144,32 +1121,39 @@ private IList FlattenTopicGraph(Topic source, IList targetList) { /// Gets a property on the that is compatible to the . /// /// - /// Even if the property values can't be set by the , properties should be settable - /// assuming the source and target types are compatible. In this case, needn't know - /// anything about the property type as it doesn't need to do a conversion; it can just do a one-to-one mapping. + /// Even if the property values can't be set by the , properties should be settable assuming the + /// source and target types are compatible. In this case, needn't know anything about + /// the property type as it doesn't need to do a conversion; it can just do a one-to-one mapping. /// /// The source from which to pull the value. /// The target . - /// The with details about the item's attributes. + /// The with details about the item's attributes. + /// The prefix to apply to the attributes. /// The compatible property, if it is available. - private static bool TryGetCompatibleProperty(Topic source, Type targetType, ItemConfiguration configuration, out object? value) { + private static bool TryGetCompatibleProperty(Topic source, Type targetType, ItemMetadata itemMetadata, string? attributePrefix, out object? value) { /*------------------------------------------------------------------------------------------------------------------------ - | Validate parameters + | Establish configuration \-----------------------------------------------------------------------------------------------------------------------*/ - Contract.Requires(source, nameof(source)); - Contract.Requires(targetType, nameof(targetType)); - Contract.Requires(configuration, nameof(configuration)); + var configuration = itemMetadata.Configuration; + + /*------------------------------------------------------------------------------------------------------------------------ + | Rely on MaybeCompatible to bypass known incompatible types + \-----------------------------------------------------------------------------------------------------------------------*/ + if (source.GetType() == typeof(Topic) && !itemMetadata.MaybeCompatible) { + value = null; + return false; + }; /*------------------------------------------------------------------------------------------------------------------------ | Attempt to retrieve value from topic.{Property} \-----------------------------------------------------------------------------------------------------------------------*/ - var sourceProperty = _typeCache.GetMember(source.GetType(), configuration.AttributeKey); + var sourcePropertyAccessor = GetTopicAccessor(source.GetType()).GetMember(configuration.GetCompositeAttributeKey(attributePrefix)); /*------------------------------------------------------------------------------------------------------------------------ | Escape clause if preconditions are not met \-----------------------------------------------------------------------------------------------------------------------*/ - if (sourceProperty is null || !targetType.IsAssignableFrom(sourceProperty.PropertyType)) { + if (sourcePropertyAccessor is null || !targetType.IsAssignableFrom(sourcePropertyAccessor.Type)) { value = null; return false; } @@ -1177,11 +1161,22 @@ private static bool TryGetCompatibleProperty(Topic source, Type targetType, Item /*------------------------------------------------------------------------------------------------------------------------ | Return value \-----------------------------------------------------------------------------------------------------------------------*/ - value = sourceProperty.GetValue(source); + value = sourcePropertyAccessor.GetValue(source); return true; } + /*========================================================================================================================== + | PRIVATE: GET TOPIC ACCESSOR + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Given a specific for a , returns the appropriate from + /// the . + /// + /// The of the source . + private static TypeAccessor GetTopicAccessor(Type topicType) => + topicType == typeof(Topic) ? _topicTypeAccessor : TypeAccessorCache.GetTypeAccessor(topicType); + } //Class } //Namespace \ No newline at end of file diff --git a/OnTopic/Mapping/_exceptions/InvalidTypeException.cs b/OnTopic/Mapping/_exceptions/InvalidTypeException.cs index e5d9775c..650abc76 100644 --- a/OnTopic/Mapping/_exceptions/InvalidTypeException.cs +++ b/OnTopic/Mapping/_exceptions/InvalidTypeException.cs @@ -3,10 +3,7 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ -using System; -using System.Diagnostics.CodeAnalysis; using System.Runtime.Serialization; -using OnTopic.Internal.Diagnostics; using OnTopic.Lookup; namespace OnTopic.Mapping { diff --git a/OnTopic/Mapping/_exceptions/MappingModelValidationException.cs b/OnTopic/Mapping/_exceptions/MappingModelValidationException.cs index 717d00d0..09abd0fe 100644 --- a/OnTopic/Mapping/_exceptions/MappingModelValidationException.cs +++ b/OnTopic/Mapping/_exceptions/MappingModelValidationException.cs @@ -3,10 +3,7 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ -using System; -using System.Diagnostics.CodeAnalysis; using System.Runtime.Serialization; -using OnTopic.Internal.Diagnostics; using OnTopic.Mapping.Reverse; using OnTopic.Metadata; diff --git a/OnTopic/Mapping/_exceptions/TopicMappingException.cs b/OnTopic/Mapping/_exceptions/TopicMappingException.cs index 0252a575..9e872bac 100644 --- a/OnTopic/Mapping/_exceptions/TopicMappingException.cs +++ b/OnTopic/Mapping/_exceptions/TopicMappingException.cs @@ -3,10 +3,7 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ -using System; -using System.Diagnostics.CodeAnalysis; using System.Runtime.Serialization; -using OnTopic.Internal.Diagnostics; namespace OnTopic.Mapping { diff --git a/OnTopic/Metadata/AttributeDescriptor.cs b/OnTopic/Metadata/AttributeDescriptor.cs index f5294ec4..2d099174 100644 --- a/OnTopic/Metadata/AttributeDescriptor.cs +++ b/OnTopic/Metadata/AttributeDescriptor.cs @@ -3,11 +3,7 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ -using System; -using System.Diagnostics.CodeAnalysis; -using OnTopic.Attributes; using OnTopic.Collections.Specialized; -using OnTopic.Internal.Diagnostics; namespace OnTopic.Metadata { diff --git a/OnTopic/Metadata/ContentTypeDescriptor.cs b/OnTopic/Metadata/ContentTypeDescriptor.cs index 248e8ce4..3c740a2a 100644 --- a/OnTopic/Metadata/ContentTypeDescriptor.cs +++ b/OnTopic/Metadata/ContentTypeDescriptor.cs @@ -3,14 +3,9 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ -using System; -using System.Linq; -using OnTopic.Attributes; using OnTopic.Collections; using OnTopic.Collections.Specialized; -using OnTopic.Internal.Diagnostics; using OnTopic.Associations; -using System.Diagnostics.CodeAnalysis; namespace OnTopic.Metadata { diff --git a/OnTopic/Metadata/ContentTypeDescriptorCollection.cs b/OnTopic/Metadata/ContentTypeDescriptorCollection.cs index fe1062f5..56144a09 100644 --- a/OnTopic/Metadata/ContentTypeDescriptorCollection.cs +++ b/OnTopic/Metadata/ContentTypeDescriptorCollection.cs @@ -3,10 +3,7 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ -using System; -using System.Linq; using OnTopic.Collections; -using OnTopic.Internal.Diagnostics; using OnTopic.Querying; using OnTopic.Repositories; @@ -79,7 +76,7 @@ public void Refresh(ContentTypeDescriptor? rootContentType) { | Add all ContentTypeDescriptors to collection \-----------------------------------------------------------------------------------------------------------------------*/ var contentTypeDescriptors = rootContentType - .FindAll(t => typeof(ContentTypeDescriptor).IsAssignableFrom(t.GetType())) + .FindAll(t => t is ContentTypeDescriptor) .Cast(); foreach (var contentType in contentTypeDescriptors) { Add((ContentTypeDescriptor)contentType); diff --git a/OnTopic/Models/IAssociatedTopicBindingModel.cs b/OnTopic/Models/IAssociatedTopicBindingModel.cs index 32c5a8a6..e0df1ba7 100644 --- a/OnTopic/Models/IAssociatedTopicBindingModel.cs +++ b/OnTopic/Models/IAssociatedTopicBindingModel.cs @@ -4,7 +4,6 @@ | Project Topics Library \=============================================================================================================================*/ using System.ComponentModel.DataAnnotations; -using System.Diagnostics.CodeAnalysis; using OnTopic.Mapping.Reverse; namespace OnTopic.Models { diff --git a/OnTopic/Models/ICoreTopicViewModel.cs b/OnTopic/Models/ICoreTopicViewModel.cs index 3b3b5a22..5096d783 100644 --- a/OnTopic/Models/ICoreTopicViewModel.cs +++ b/OnTopic/Models/ICoreTopicViewModel.cs @@ -5,7 +5,6 @@ \=============================================================================================================================*/ using System.Collections.ObjectModel; using System.ComponentModel.DataAnnotations; -using System.Diagnostics.CodeAnalysis; using OnTopic.Mapping; using OnTopic.Metadata; diff --git a/OnTopic/Models/INavigableTopicViewModel.cs b/OnTopic/Models/INavigableTopicViewModel.cs index b574fb19..df13b929 100644 --- a/OnTopic/Models/INavigableTopicViewModel.cs +++ b/OnTopic/Models/INavigableTopicViewModel.cs @@ -4,7 +4,6 @@ | Project Website \=============================================================================================================================*/ using System.ComponentModel.DataAnnotations; -using System.Diagnostics.CodeAnalysis; namespace OnTopic.Models { diff --git a/OnTopic/Models/IPageTopicViewModel.cs b/OnTopic/Models/IPageTopicViewModel.cs index b19beba9..f6f6974a 100644 --- a/OnTopic/Models/IPageTopicViewModel.cs +++ b/OnTopic/Models/IPageTopicViewModel.cs @@ -3,7 +3,6 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ -using System; using OnTopic.Mapping; namespace OnTopic.Models { @@ -22,7 +21,7 @@ namespace OnTopic.Models { /// provided via the public interface then it will instead need to be defined in some other way. /// /// - [Obsolete("The IPageTopicViewModel is no longer utilized.", true)] + [Obsolete($"The {nameof(IPageTopicViewModel)} is no longer utilized.", true)] public interface IPageTopicViewModel : ITopicViewModel, INavigableTopicViewModel { /*========================================================================================================================== diff --git a/OnTopic/Models/IRelatedTopicBindingModel.cs b/OnTopic/Models/IRelatedTopicBindingModel.cs index 184eb052..4d960171 100644 --- a/OnTopic/Models/IRelatedTopicBindingModel.cs +++ b/OnTopic/Models/IRelatedTopicBindingModel.cs @@ -3,7 +3,6 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ -using System; using OnTopic.Mapping.Reverse; namespace OnTopic.Models { diff --git a/OnTopic/Models/ITopicViewModel.cs b/OnTopic/Models/ITopicViewModel.cs index 75e1f2d4..ffd1b357 100644 --- a/OnTopic/Models/ITopicViewModel.cs +++ b/OnTopic/Models/ITopicViewModel.cs @@ -3,9 +3,7 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ -using System; using System.ComponentModel.DataAnnotations; -using System.Diagnostics.CodeAnalysis; using OnTopic.Mapping; namespace OnTopic.Models { @@ -72,7 +70,7 @@ public interface ITopicViewModel: ICoreTopicViewModel, IAssociatedTopicBindingMo /// Gets or sets whether the current topic is hidden. /// [ExcludeFromCodeCoverage] - [Obsolete("The IsHidden property is no longer supported by ITopicViewModel.", true)] + [Obsolete($"The {nameof(IsHidden)} property is no longer supported by {nameof(ITopicViewModel)}.", true)] bool IsHidden { get; init; } /*========================================================================================================================== diff --git a/OnTopic/Obsolete/Attributes/AttributeValue.cs b/OnTopic/Obsolete/Attributes/AttributeValue.cs index 02d93a7b..e7cd82a4 100644 --- a/OnTopic/Obsolete/Attributes/AttributeValue.cs +++ b/OnTopic/Obsolete/Attributes/AttributeValue.cs @@ -3,8 +3,6 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ -using System; -using System.Diagnostics.CodeAnalysis; using OnTopic.Collections; using OnTopic.Metadata; using OnTopic.Repositories; @@ -42,7 +40,7 @@ namespace OnTopic.Attributes { /// /// [ExcludeFromCodeCoverage] - [Obsolete("The AttributeValue type has been renamed to AttributeRecord.", true)] + [Obsolete($"The {nameof(AttributeValue)} type has been renamed to {nameof(AttributeRecord)}.", true)] public class AttributeValue { /*========================================================================================================================== diff --git a/OnTopic/Obsolete/Collections/AttributeValueCollection.cs b/OnTopic/Obsolete/Collections/AttributeValueCollection.cs index b1748931..d0dcec16 100644 --- a/OnTopic/Obsolete/Collections/AttributeValueCollection.cs +++ b/OnTopic/Obsolete/Collections/AttributeValueCollection.cs @@ -3,11 +3,7 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ -using System; using System.Collections.ObjectModel; -using System.Diagnostics.CodeAnalysis; -using OnTopic.Attributes; -using OnTopic.Internal.Diagnostics; using OnTopic.Repositories; #pragma warning disable IDE0060 // Remove unused parameter @@ -26,7 +22,7 @@ namespace OnTopic.Collections { /// the class. /// [ExcludeFromCodeCoverage] - [Obsolete("The AttributeValueCollection class has been renamed to AttributeCollection.", true)] + [Obsolete($"The {nameof(AttributeValueCollection)} class has been renamed to {nameof(AttributeCollection)}.", true)] public class AttributeValueCollection : KeyedCollection { /*========================================================================================================================== diff --git a/OnTopic/Obsolete/Collections/NamedTopicCollection.cs b/OnTopic/Obsolete/Collections/NamedTopicCollection.cs index d8a3167a..faa93624 100644 --- a/OnTopic/Obsolete/Collections/NamedTopicCollection.cs +++ b/OnTopic/Obsolete/Collections/NamedTopicCollection.cs @@ -3,11 +3,9 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ -using System; -using System.Collections.Generic; using System.Collections.ObjectModel; -using System.Diagnostics.CodeAnalysis; -using System.Linq; +using OnTopic.Associations; +using OnTopic.Collections.Specialized; namespace OnTopic.Collections { @@ -19,7 +17,10 @@ namespace OnTopic.Collections { /// , or other derivatives of . /// [ExcludeFromCodeCoverage] - [Obsolete("Migrated to KeyValuesPair as part of new TopicRelationshipsMultiMap type.", true)] + [Obsolete( + $"Migrated to {nameof(KeyValuesPair>)} as part of new {nameof(TopicRelationshipMultiMap)} type.", + true + )] public class NamedTopicCollection: KeyedTopicCollection { /*========================================================================================================================== diff --git a/OnTopic/Obsolete/Collections/ReadOnlyTopicCollection{T}.cs b/OnTopic/Obsolete/Collections/ReadOnlyTopicCollection{T}.cs index b53cadef..0b87600a 100644 --- a/OnTopic/Obsolete/Collections/ReadOnlyTopicCollection{T}.cs +++ b/OnTopic/Obsolete/Collections/ReadOnlyTopicCollection{T}.cs @@ -3,10 +3,7 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ -using System; -using System.Collections.Generic; using System.Collections.ObjectModel; -using System.Diagnostics.CodeAnalysis; #pragma warning disable IDE0060 // Remove unused parameter @@ -19,7 +16,10 @@ namespace OnTopic.Collections { /// Provides a read-only collection of topics. /// [ExcludeFromCodeCoverage] - [Obsolete("The ReadOnlyTopicCollection has been renamed to ReadOnlyKeyedTopicCollection", true)] + [Obsolete( + $"The {nameof(ReadOnlyTopicCollection )} has been renamed to {nameof(ReadOnlyKeyedTopicCollection)}", + true + )] public class ReadOnlyTopicCollection : ReadOnlyCollection where T : Topic { /*========================================================================================================================== diff --git a/OnTopic/Obsolete/Collections/RelatedTopicCollection.cs b/OnTopic/Obsolete/Collections/RelatedTopicCollection.cs index b1d65aa5..daf3a591 100644 --- a/OnTopic/Obsolete/Collections/RelatedTopicCollection.cs +++ b/OnTopic/Obsolete/Collections/RelatedTopicCollection.cs @@ -3,13 +3,9 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ -using System; using System.Collections.ObjectModel; -using System.Diagnostics.CodeAnalysis; -using System.Linq; -using OnTopic.Internal.Diagnostics; +using OnTopic.Associations; -#pragma warning disable CA1801 // Review unused parameters #pragma warning disable IDE0060 // Remove unused parameter namespace OnTopic.Collections { @@ -21,7 +17,7 @@ namespace OnTopic.Collections { /// Provides a simple interface for accessing collections of topic collections. /// [ExcludeFromCodeCoverage] - [Obsolete("RelatedTopicCollection has been migrated to the new TopicRelationshipMultiMap", true)] + [Obsolete($"{nameof(RelatedTopicCollection)} has been migrated to the new {nameof(TopicRelationshipMultiMap)}", true)] public class RelatedTopicCollection : KeyedCollection { /*========================================================================================================================== @@ -199,5 +195,4 @@ protected override string GetKeyForItem(NamedTopicCollection item) { } //Class } //Namespace -#pragma warning restore CA1801 // Review unused parameters #pragma warning restore IDE0060 // Remove unused parameter \ No newline at end of file diff --git a/OnTopic/Obsolete/Collections/TopicCollection{T}.cs b/OnTopic/Obsolete/Collections/TopicCollection{T}.cs index 789a297b..ad817d52 100644 --- a/OnTopic/Obsolete/Collections/TopicCollection{T}.cs +++ b/OnTopic/Obsolete/Collections/TopicCollection{T}.cs @@ -3,11 +3,7 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ -using System; -using System.Collections.Generic; using System.Collections.ObjectModel; -using System.Diagnostics.CodeAnalysis; -using OnTopic.Internal.Diagnostics; #pragma warning disable IDE0060 // Remove unused parameter @@ -20,7 +16,7 @@ namespace OnTopic.Collections { /// Provides a strongly-typed collection of instances, or a derived type. /// [ExcludeFromCodeCoverage] - [Obsolete("The TopicCollection class has been renamed to KeyedTopicCollection.", true)] + [Obsolete($"The {nameof(TopicCollection)} class has been renamed to {nameof(KeyedTopicCollection)}.", true)] public class TopicCollection: KeyedCollection, IEnumerable where T : Topic { /*========================================================================================================================== diff --git a/OnTopic/Obsolete/Mapping/Annotations/FollowAttribute.cs b/OnTopic/Obsolete/Mapping/Annotations/FollowAttribute.cs index 39ca3a0e..fc1229e4 100644 --- a/OnTopic/Obsolete/Mapping/Annotations/FollowAttribute.cs +++ b/OnTopic/Obsolete/Mapping/Annotations/FollowAttribute.cs @@ -3,8 +3,6 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ -using System; -using System.Diagnostics.CodeAnalysis; namespace OnTopic.Mapping.Annotations { @@ -14,7 +12,7 @@ namespace OnTopic.Mapping.Annotations { /// [ExcludeFromCodeCoverage] [AttributeUsage(AttributeTargets.Property)] - [Obsolete("The [Follow] attribute has been renamed to [Include].", true)] + [Obsolete($"The {nameof(FollowAttribute)} has been renamed to {nameof(IncludeAttribute)}.", true)] public sealed class FollowAttribute : Attribute { /*========================================================================================================================== diff --git a/OnTopic/Obsolete/Mapping/Annotations/RelationshipAttribute.cs b/OnTopic/Obsolete/Mapping/Annotations/RelationshipAttribute.cs index 73ae9017..dfa3b083 100644 --- a/OnTopic/Obsolete/Mapping/Annotations/RelationshipAttribute.cs +++ b/OnTopic/Obsolete/Mapping/Annotations/RelationshipAttribute.cs @@ -3,8 +3,6 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ -using System; -using System.Diagnostics.CodeAnalysis; namespace OnTopic.Mapping.Annotations { @@ -14,7 +12,7 @@ namespace OnTopic.Mapping.Annotations { /// [ExcludeFromCodeCoverage] [AttributeUsage(AttributeTargets.Property)] - [Obsolete("The [Relationship] attribute has been renamed to [Collection].", true)] + [Obsolete($"The {nameof(RelationshipAttribute)} has been renamed to {nameof(CollectionAttribute)}.", true)] public sealed class RelationshipAttribute : Attribute { /*========================================================================================================================== diff --git a/OnTopic/Obsolete/Mapping/Annotations/RelationshipType.cs b/OnTopic/Obsolete/Mapping/Annotations/RelationshipType.cs index 6a545c26..b3bd88cd 100644 --- a/OnTopic/Obsolete/Mapping/Annotations/RelationshipType.cs +++ b/OnTopic/Obsolete/Mapping/Annotations/RelationshipType.cs @@ -3,7 +3,6 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ -using System; namespace OnTopic.Mapping.Annotations { @@ -11,7 +10,7 @@ namespace OnTopic.Mapping.Annotations { | ENUM: RELATIONSHIP TYPE \---------------------------------------------------------------------------------------------------------------------------*/ /// - [Obsolete("RelationshipType has been renamed to CollectionType", true)] + [Obsolete($"{nameof(RelationshipType)} has been renamed to {nameof(CollectionType)}", true)] public enum RelationshipType { #pragma warning disable CS1591 // Missing XML comment for publicly visible type or member diff --git a/OnTopic/Obsolete/Mapping/Annotations/Relationships.cs b/OnTopic/Obsolete/Mapping/Annotations/Relationships.cs index 2145c561..464ca53b 100644 --- a/OnTopic/Obsolete/Mapping/Annotations/Relationships.cs +++ b/OnTopic/Obsolete/Mapping/Annotations/Relationships.cs @@ -3,7 +3,6 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ -using System; namespace OnTopic.Mapping.Annotations { @@ -12,7 +11,7 @@ namespace OnTopic.Mapping.Annotations { \---------------------------------------------------------------------------------------------------------------------------*/ /// [Flags] - [Obsolete("The Relationships enum has been renamed to AssociationTypes.", true)] + [Obsolete($"The {nameof(Relationships)} enum has been renamed to {nameof(AssociationTypes)}.", true)] public enum Relationships { /*========================================================================================================================== diff --git a/OnTopic/Obsolete/Metadata/Attributes/AttributeTypeDescriptor.cs b/OnTopic/Obsolete/Metadata/Attributes/AttributeTypeDescriptor.cs index 622fbb26..8ee9e98d 100644 --- a/OnTopic/Obsolete/Metadata/Attributes/AttributeTypeDescriptor.cs +++ b/OnTopic/Obsolete/Metadata/Attributes/AttributeTypeDescriptor.cs @@ -3,8 +3,6 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ -using System; -using System.Diagnostics.CodeAnalysis; namespace OnTopic.Metadata.AttributeTypes { @@ -21,7 +19,10 @@ namespace OnTopic.Metadata.AttributeTypes { /// typed representation of those properties. This class provides a base for those representations. /// [ExcludeFromCodeCoverage] - [Obsolete("The AttributeTypeDescriptor class is obsolete. Derive from AttributeDescriptor instead.", true)] + [Obsolete( + $"The {nameof(AttributeTypeDescriptor)} class is obsolete. Derive from {nameof(AttributeDescriptor)} instead.", + true + )] public abstract class AttributeTypeDescriptor : AttributeDescriptor { /*========================================================================================================================== diff --git a/OnTopic/Obsolete/Repositories/DeleteEventArgs.cs b/OnTopic/Obsolete/Repositories/DeleteEventArgs.cs index 409aa520..87d64a8e 100644 --- a/OnTopic/Obsolete/Repositories/DeleteEventArgs.cs +++ b/OnTopic/Obsolete/Repositories/DeleteEventArgs.cs @@ -3,8 +3,6 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ -using System; -using System.Diagnostics.CodeAnalysis; namespace OnTopic.Repositories { @@ -15,7 +13,7 @@ namespace OnTopic.Repositories { /// The DeleteEventArgs class defines an event argument type specific to deletion events /// [ExcludeFromCodeCoverage] - [Obsolete("The DeleteEventArgs has been renamed to TopicEventArgs", true)] + [Obsolete($"The {nameof(DeleteEventArgs)} has been renamed to {nameof(TopicEventArgs)}", true)] public class DeleteEventArgs : EventArgs { /*========================================================================================================================== diff --git a/OnTopic/Obsolete/Repositories/MoveEventArgs.cs b/OnTopic/Obsolete/Repositories/MoveEventArgs.cs index 192dbb2e..c4665f40 100644 --- a/OnTopic/Obsolete/Repositories/MoveEventArgs.cs +++ b/OnTopic/Obsolete/Repositories/MoveEventArgs.cs @@ -3,9 +3,6 @@ | Client Ignia | Project Topics Library \=============================================================================================================================*/ -using System; -using System.Diagnostics.CodeAnalysis; -using OnTopic.Internal.Diagnostics; namespace OnTopic.Repositories { @@ -19,7 +16,7 @@ namespace OnTopic.Repositories { /// Allows tracking of the source and destination topics. /// [ExcludeFromCodeCoverage] - [Obsolete("The MoveEventArgs have been renamed to TopicMoveEventArgs.", true)] + [Obsolete($"The {nameof(MoveEventArgs)} have been renamed to {nameof(TopicMoveEventArgs)}.", true)] public class MoveEventArgs : EventArgs { /*========================================================================================================================== diff --git a/OnTopic/Obsolete/Repositories/RenameEventArgs.cs b/OnTopic/Obsolete/Repositories/RenameEventArgs.cs index fac93adb..50e54213 100644 --- a/OnTopic/Obsolete/Repositories/RenameEventArgs.cs +++ b/OnTopic/Obsolete/Repositories/RenameEventArgs.cs @@ -3,8 +3,6 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ -using System; -using System.Diagnostics.CodeAnalysis; namespace OnTopic.Repositories { @@ -15,7 +13,7 @@ namespace OnTopic.Repositories { /// The RenameEventArgs object defines an event argument type specific to rename events. /// [ExcludeFromCodeCoverage] - [Obsolete("The RenameEventArgs have been renamed to TopicEventArgs.", true)] + [Obsolete($"The {nameof(RenameEventArgs)} have been renamed to {nameof(TopicEventArgs)}.", true)] public class RenameEventArgs : EventArgs { /*========================================================================================================================== diff --git a/OnTopic/OnTopic.csproj b/OnTopic/OnTopic.csproj index 7e414d6b..99296a8e 100644 --- a/OnTopic/OnTopic.csproj +++ b/OnTopic/OnTopic.csproj @@ -14,20 +14,20 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + all diff --git a/OnTopic/Properties/AssemblyInfo.cs b/OnTopic/Properties/AssemblyInfo.cs index 2363fe73..68bde120 100644 --- a/OnTopic/Properties/AssemblyInfo.cs +++ b/OnTopic/Properties/AssemblyInfo.cs @@ -3,7 +3,17 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ -using System; + +/*============================================================================================================================== +| USING DIRECTIVES (GLOBAL) +\-----------------------------------------------------------------------------------------------------------------------------*/ +global using System.Diagnostics.CodeAnalysis; +global using OnTopic.Attributes; +global using OnTopic.Internal.Diagnostics; + +/*============================================================================================================================== +| USING DIRECTIVES (LOCAL) +\-----------------------------------------------------------------------------------------------------------------------------*/ using System.Runtime.CompilerServices; using System.Runtime.InteropServices; @@ -13,6 +23,6 @@ | Declare and define attributes used in the compiling of the finished assembly. \-----------------------------------------------------------------------------------------------------------------------------*/ [assembly: ComVisible(false)] -[assembly: InternalsVisibleTo("OnTopic.Tests")] [assembly: CLSCompliant(true)] -[assembly: GuidAttribute("3CA9F6CB-B45A-4E74-AAA4-0C87CAA2704F")] \ No newline at end of file +[assembly: InternalsVisibleTo("OnTopic.Tests")] +[assembly: Guid("3CA9F6CB-B45A-4E74-AAA4-0C87CAA2704F")] \ No newline at end of file diff --git a/OnTopic/Querying/TopicCollectionExtensions.cs b/OnTopic/Querying/TopicCollectionExtensions.cs index 75d1f99e..d2d924b6 100644 --- a/OnTopic/Querying/TopicCollectionExtensions.cs +++ b/OnTopic/Querying/TopicCollectionExtensions.cs @@ -3,8 +3,6 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ -using System.Collections.Generic; -using System.Linq; using OnTopic.Collections; namespace OnTopic.Querying { diff --git a/OnTopic/Querying/TopicExtensions.cs b/OnTopic/Querying/TopicExtensions.cs index 8c5a1c39..6764ca59 100644 --- a/OnTopic/Querying/TopicExtensions.cs +++ b/OnTopic/Querying/TopicExtensions.cs @@ -3,11 +3,8 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ -using System; -using OnTopic.Attributes; using OnTopic.Collections; using OnTopic.Collections.Specialized; -using OnTopic.Internal.Diagnostics; using OnTopic.Metadata; namespace OnTopic.Querying { @@ -288,7 +285,7 @@ public static ReadOnlyTopicCollection FindAllByAttribute(this Topic topic, strin \-----------------------------------------------------------------------------------------------------------------------*/ var contentTypeDescriptor = rootContentType?.FindFirst(t => t.Key.Equals(topic.ContentType, StringComparison.OrdinalIgnoreCase) && - t.GetType().IsAssignableFrom(typeof(ContentTypeDescriptor)) + t is ContentTypeDescriptor ) as ContentTypeDescriptor; /*------------------------------------------------------------------------------------------------------------------------ diff --git a/OnTopic/Repositories/ITopicRepository.cs b/OnTopic/Repositories/ITopicRepository.cs index 12e2c2fa..3729c3da 100644 --- a/OnTopic/Repositories/ITopicRepository.cs +++ b/OnTopic/Repositories/ITopicRepository.cs @@ -3,8 +3,6 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ -using System; -using System.Diagnostics.CodeAnalysis; using OnTopic.Metadata; namespace OnTopic.Repositories { @@ -59,17 +57,17 @@ public interface ITopicRepository { /// [ExcludeFromCodeCoverage] - [Obsolete("The DeleteEvent has been renamed to TopicDeleted")] + [Obsolete($"The {nameof(DeleteEvent)} has been renamed to {nameof(TopicDeleted)}")] event EventHandler DeleteEvent; /// [ExcludeFromCodeCoverage] - [Obsolete("The MoveEvent has been renamed to TopicMoved")] + [Obsolete($"The {nameof(MoveEvent)} has been renamed to {nameof(TopicMoved)}")] event EventHandler MoveEvent; /// [ExcludeFromCodeCoverage] - [Obsolete("The RenameEvent has been renamed to TopicRenamed")] + [Obsolete($"The {nameof(RenameEvent)} has been renamed to {nameof(TopicRenamed)}")] event EventHandler RenameEvent; /*========================================================================================================================== diff --git a/OnTopic/Repositories/ObservableTopicRepository.cs b/OnTopic/Repositories/ObservableTopicRepository.cs index ae5ced2c..8a257c65 100644 --- a/OnTopic/Repositories/ObservableTopicRepository.cs +++ b/OnTopic/Repositories/ObservableTopicRepository.cs @@ -3,8 +3,6 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ -using System; -using System.Diagnostics.CodeAnalysis; using OnTopic.Metadata; namespace OnTopic.Repositories { @@ -68,17 +66,17 @@ public event EventHandler? TopicRenamed { /// [ExcludeFromCodeCoverage] - [Obsolete("The DeleteEvent has been renamed to TopicDeleted", true)] + [Obsolete($"The {nameof(DeleteEvent)} has been renamed to {nameof(TopicDeleted)}", true)] public event EventHandler? DeleteEvent; /// [ExcludeFromCodeCoverage] - [Obsolete("The MoveEvent has been renamed to TopicMoved", true)] + [Obsolete($"The {nameof(MoveEvent)} has been renamed to {nameof(TopicMoved)}", true)] public event EventHandler? MoveEvent; /// [ExcludeFromCodeCoverage] - [Obsolete("The RenameEvent has been renamed to TopicRenamed", true)] + [Obsolete($"The {nameof(RenameEvent)} has been renamed to {nameof(TopicRenamed)}", true)] public event EventHandler? RenameEvent; /*========================================================================================================================== @@ -246,7 +244,7 @@ public event EventHandler? TopicRenamed { /// [ExcludeFromCodeCoverage] - [Obsolete("The 'isDraft' argument of the Save() method has been removed.")] + [Obsolete($"The 'isDraft' argument of the {nameof(Save)} method has been removed.")] public int Save(Topic topic, bool isRecursive, bool isDraft) => throw new NotImplementedException(); /*========================================================================================================================== diff --git a/OnTopic/Repositories/TopicRepository.cs b/OnTopic/Repositories/TopicRepository.cs index 837396fe..ba899043 100644 --- a/OnTopic/Repositories/TopicRepository.cs +++ b/OnTopic/Repositories/TopicRepository.cs @@ -3,15 +3,9 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ -using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.Linq; using Microsoft; -using OnTopic.Attributes; using OnTopic.Collections; using OnTopic.Collections.Specialized; -using OnTopic.Internal.Diagnostics; using OnTopic.Metadata; using OnTopic.Querying; @@ -104,7 +98,10 @@ public override ContentTypeDescriptorCollection GetContentTypeDescriptors() { /// /// [ExcludeFromCodeCoverage] - [Obsolete("Deprecated. Instead, use the new SetContentTypeDescriptors() method, which provides the same function.", true)] + [Obsolete( + $"Deprecated. Instead, use the new {nameof(SetContentTypeDescriptors)} method, which provides the same function.", + true + )] protected virtual ContentTypeDescriptorCollection GetContentTypeDescriptors(ContentTypeDescriptor? contentTypeDescriptors) => SetContentTypeDescriptors(contentTypeDescriptors); @@ -768,7 +765,7 @@ protected IEnumerable GetAttributes( //Skip if the value is null or empty; these values are not persisted to storage and should be treated as equivalent to //non-existent values. - if (attributeValue.Value is null || attributeValue.Value.Length == 0) { + if (String.IsNullOrEmpty(attributeValue.Value)) { continue; } diff --git a/OnTopic/Repositories/TopicRepositoryDecorator.cs b/OnTopic/Repositories/TopicRepositoryDecorator.cs index 07ed1767..9a6a2416 100644 --- a/OnTopic/Repositories/TopicRepositoryDecorator.cs +++ b/OnTopic/Repositories/TopicRepositoryDecorator.cs @@ -3,9 +3,6 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ -using System; -using System.Diagnostics.CodeAnalysis; -using OnTopic.Internal.Diagnostics; using OnTopic.Metadata; namespace OnTopic.Repositories { diff --git a/OnTopic/Repositories/_eventArgs/TopicEventArgs.cs b/OnTopic/Repositories/_eventArgs/TopicEventArgs.cs index 5ab65afa..697ba7cc 100644 --- a/OnTopic/Repositories/_eventArgs/TopicEventArgs.cs +++ b/OnTopic/Repositories/_eventArgs/TopicEventArgs.cs @@ -3,8 +3,6 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ -using System; -using OnTopic.Internal.Diagnostics; namespace OnTopic.Repositories { diff --git a/OnTopic/Repositories/_eventArgs/TopicLoadEventArgs.cs b/OnTopic/Repositories/_eventArgs/TopicLoadEventArgs.cs index b7725d86..6433369e 100644 --- a/OnTopic/Repositories/_eventArgs/TopicLoadEventArgs.cs +++ b/OnTopic/Repositories/_eventArgs/TopicLoadEventArgs.cs @@ -3,8 +3,6 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ -using System; -using OnTopic.Internal.Diagnostics; namespace OnTopic.Repositories { diff --git a/OnTopic/Repositories/_eventArgs/TopicMoveEventArgs.cs b/OnTopic/Repositories/_eventArgs/TopicMoveEventArgs.cs index e2ac52a6..274f38d6 100644 --- a/OnTopic/Repositories/_eventArgs/TopicMoveEventArgs.cs +++ b/OnTopic/Repositories/_eventArgs/TopicMoveEventArgs.cs @@ -3,8 +3,6 @@ | Client Ignia | Project Topics Library \=============================================================================================================================*/ -using System; -using OnTopic.Internal.Diagnostics; namespace OnTopic.Repositories { diff --git a/OnTopic/Repositories/_eventArgs/TopicRenameEventArgs.cs b/OnTopic/Repositories/_eventArgs/TopicRenameEventArgs.cs index e83959f2..b1fc5a81 100644 --- a/OnTopic/Repositories/_eventArgs/TopicRenameEventArgs.cs +++ b/OnTopic/Repositories/_eventArgs/TopicRenameEventArgs.cs @@ -3,8 +3,6 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ -using System; -using OnTopic.Internal.Diagnostics; namespace OnTopic.Repositories { diff --git a/OnTopic/Repositories/_eventArgs/TopicSaveEventArgs.cs b/OnTopic/Repositories/_eventArgs/TopicSaveEventArgs.cs index daf1e5aa..2f3f6827 100644 --- a/OnTopic/Repositories/_eventArgs/TopicSaveEventArgs.cs +++ b/OnTopic/Repositories/_eventArgs/TopicSaveEventArgs.cs @@ -3,8 +3,6 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ -using System; -using OnTopic.Internal.Diagnostics; namespace OnTopic.Repositories { diff --git a/OnTopic/Repositories/_exceptions/ReferentialIntegrityException.cs b/OnTopic/Repositories/_exceptions/ReferentialIntegrityException.cs index 81d04ff4..631cc9da 100644 --- a/OnTopic/Repositories/_exceptions/ReferentialIntegrityException.cs +++ b/OnTopic/Repositories/_exceptions/ReferentialIntegrityException.cs @@ -3,10 +3,7 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ -using System; -using System.Diagnostics.CodeAnalysis; using System.Runtime.Serialization; -using OnTopic.Internal.Diagnostics; namespace OnTopic.Repositories { diff --git a/OnTopic/Repositories/_exceptions/TopicNotFoundException.cs b/OnTopic/Repositories/_exceptions/TopicNotFoundException.cs index caa72e87..741eeba6 100644 --- a/OnTopic/Repositories/_exceptions/TopicNotFoundException.cs +++ b/OnTopic/Repositories/_exceptions/TopicNotFoundException.cs @@ -3,11 +3,8 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ -using System; using System.Data.Common; -using System.Diagnostics.CodeAnalysis; using System.Runtime.Serialization; -using OnTopic.Internal.Diagnostics; namespace OnTopic.Repositories { diff --git a/OnTopic/Repositories/_exceptions/TopicRepositoryException.cs b/OnTopic/Repositories/_exceptions/TopicRepositoryException.cs index 86f8b84c..d14f8a84 100644 --- a/OnTopic/Repositories/_exceptions/TopicRepositoryException.cs +++ b/OnTopic/Repositories/_exceptions/TopicRepositoryException.cs @@ -3,11 +3,8 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ -using System; using System.Data.Common; -using System.Diagnostics.CodeAnalysis; using System.Runtime.Serialization; -using OnTopic.Internal.Diagnostics; namespace OnTopic.Repositories { diff --git a/OnTopic/Topic.cs b/OnTopic/Topic.cs index bd20f79f..a63c943d 100644 --- a/OnTopic/Topic.cs +++ b/OnTopic/Topic.cs @@ -3,15 +3,10 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ -using System; using System.Collections.ObjectModel; -using System.Diagnostics.CodeAnalysis; using System.Globalization; -using System.Linq; -using OnTopic.Attributes; using OnTopic.Collections; using OnTopic.Collections.Specialized; -using OnTopic.Internal.Diagnostics; using OnTopic.Metadata; using OnTopic.Associations; @@ -402,7 +397,11 @@ public string Title { /// !string.IsNullOrWhiteSpace(value) /// [ExcludeFromCodeCoverage] - [Obsolete("The Description convenience property will be removed in OnTopic Library 5.0. Use Attributes.SetValue() instead.", true)] + [Obsolete( + $"The Description convenience property will be removed in OnTopic Library 5.0. Use " + + $"{nameof(AttributeValueCollection.SetValue)} instead.", + true + )] public string? Description { get => Attributes.GetValue("Description"); set => SetAttributeValue("Description", value); @@ -719,7 +718,10 @@ public Topic? BaseTopic { /// [ExcludeFromCodeCoverage] - [Obsolete("The DerivedTopic property has been renamed to BaseTopic. Please update references.", true)] + [Obsolete( + $"The {nameof(DerivedTopic)} property has been renamed to {nameof(BaseTopic)}. Please update references.", + true + )] public Topic? DerivedTopic { get => BaseTopic; set => BaseTopic = value; diff --git a/OnTopic/TopicFactory.cs b/OnTopic/TopicFactory.cs index a732221d..2f9a8698 100644 --- a/OnTopic/TopicFactory.cs +++ b/OnTopic/TopicFactory.cs @@ -3,11 +3,8 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ -using System; -using System.Diagnostics.CodeAnalysis; using System.Text.RegularExpressions; using OnTopic.Collections.Specialized; -using OnTopic.Internal.Diagnostics; using OnTopic.Lookup; using OnTopic.Metadata;