diff --git a/Application/EdFi.Ods.Api/Container/Modules/ApplicationModule.cs b/Application/EdFi.Ods.Api/Container/Modules/ApplicationModule.cs index 398fee6a93..7006066f27 100644 --- a/Application/EdFi.Ods.Api/Container/Modules/ApplicationModule.cs +++ b/Application/EdFi.Ods.Api/Container/Modules/ApplicationModule.cs @@ -26,6 +26,7 @@ using EdFi.Ods.Api.Middleware; using EdFi.Ods.Api.Providers; using EdFi.Ods.Api.Security.Authentication; +using EdFi.Ods.Api.Serialization; using EdFi.Ods.Api.Validation; using EdFi.Ods.Common; using EdFi.Ods.Common.Caching; @@ -55,6 +56,7 @@ using Microsoft.AspNetCore.Mvc.ApplicationModels; using Microsoft.AspNetCore.Mvc.Filters; using Microsoft.Extensions.Options; +using Newtonsoft.Json.Serialization; using Module = Autofac.Module; namespace EdFi.Ods.Api.Container.Modules @@ -91,6 +93,15 @@ protected override void Load(ContainerBuilder builder) .As>() .SingleInstance(); + builder.RegisterType() + .WithProperty("NamingStrategy", + new CamelCaseNamingStrategy + { + ProcessDictionaryKeys = true, + OverrideSpecifiedNames = true + }) + .SingleInstance(); + builder.RegisterType() .As() .SingleInstance(); diff --git a/Application/EdFi.Ods.Api/Filters/EnforceAssignedProfileUsageFilter.cs b/Application/EdFi.Ods.Api/Filters/EnforceAssignedProfileUsageFilter.cs index 10fdba90c1..3b41367aa8 100644 --- a/Application/EdFi.Ods.Api/Filters/EnforceAssignedProfileUsageFilter.cs +++ b/Application/EdFi.Ods.Api/Filters/EnforceAssignedProfileUsageFilter.cs @@ -22,7 +22,7 @@ namespace EdFi.Ods.Api.Filters; -public class EnforceAssignedProfileUsageFilter : IAsyncActionFilter +public class EnforceAssignedProfileUsageFilter : IAsyncResourceFilter { private readonly IApiClientContextProvider _apiClientContextProvider; private readonly IContextProvider _dataManagementResourceContextProvider; @@ -49,7 +49,7 @@ public EnforceAssignedProfileUsageFilter( _isEnabled = apiSettings.IsFeatureEnabled("Profiles"); } - public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next) + public async Task OnResourceExecutionAsync(ResourceExecutingContext context, ResourceExecutionDelegate next) { // If Profiles feature is not enabled, don't do any processing. if (!_isEnabled) diff --git a/Application/EdFi.Ods.Api/Serialization/ProfilesAwareContractResolver.cs b/Application/EdFi.Ods.Api/Serialization/ProfilesAwareContractResolver.cs index 585b1096b3..640449bdb8 100644 --- a/Application/EdFi.Ods.Api/Serialization/ProfilesAwareContractResolver.cs +++ b/Application/EdFi.Ods.Api/Serialization/ProfilesAwareContractResolver.cs @@ -15,6 +15,7 @@ using EdFi.Ods.Common.Profiles; using EdFi.Ods.Common.Security.Claims; using EdFi.Ods.Common.Utils.Profiles; +using log4net; using Microsoft.Extensions.Primitives; using Newtonsoft.Json.Serialization; @@ -40,6 +41,7 @@ public class ProfilesAwareContractResolver : DefaultContractResolver private readonly string _resourcesNamespacePrefix = $"{Namespaces.Resources.BaseNamespace}."; private readonly ISchemaNameMapProvider _schemaNameMapProvider; private static readonly char[] _decimalAsCharArray = { '.' }; + private readonly ILog _logger = LogManager.GetLogger(typeof(ProfilesAwareContractResolver)); public ProfilesAwareContractResolver( IContextProvider profileContentTypeContextProvider, @@ -212,4 +214,14 @@ protected override List GetSerializableMembers(Type objectType) return profileConstrainedMembers; } + + public void Clear() + { + if (_logger.IsDebugEnabled) + { + _logger.Debug("Clears profile contracts due to profile metadata cache expiration..."); + } + + _contractByKey.Clear(); + } } diff --git a/Application/EdFi.Ods.Api/Startup/NewtonsoftJsonOptionConfigurator.cs b/Application/EdFi.Ods.Api/Startup/NewtonsoftJsonOptionConfigurator.cs index fd77aa9f1b..6ed3b7017f 100644 --- a/Application/EdFi.Ods.Api/Startup/NewtonsoftJsonOptionConfigurator.cs +++ b/Application/EdFi.Ods.Api/Startup/NewtonsoftJsonOptionConfigurator.cs @@ -3,13 +3,16 @@ // The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. // See the LICENSE and NOTICES files in the project root for more information. +using System; using EdFi.Ods.Api.Serialization; using EdFi.Ods.Common; using EdFi.Ods.Common.Context; using EdFi.Ods.Common.Models; using EdFi.Ods.Common.Profiles; using EdFi.Ods.Common.Security.Claims; +using EdFi.Ods.Common.Serialization; using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; using Newtonsoft.Json; using Newtonsoft.Json.Serialization; @@ -22,21 +25,11 @@ namespace EdFi.Ods.Api.Startup; /// public class NewtonsoftJsonOptionConfigurator : IConfigureOptions { - private readonly IContextProvider _profileContentTypeContextProvider; - private readonly IContextProvider _dataManagementResourceContextProvider; - private readonly IProfileResourceModelProvider _profileResourceModelProvider; - private readonly ISchemaNameMapProvider _schemaNameMapProvider; + private readonly IServiceProvider _serviceProvider; - public NewtonsoftJsonOptionConfigurator( - IContextProvider profileContentTypeContextProvider, - IContextProvider dataManagementResourceContextProvider, - IProfileResourceModelProvider profileResourceModelProvider, - ISchemaNameMapProvider schemaNameMapProvider) + public NewtonsoftJsonOptionConfigurator(IServiceProvider serviceProvider) { - _profileContentTypeContextProvider = profileContentTypeContextProvider; - _dataManagementResourceContextProvider = dataManagementResourceContextProvider; - _profileResourceModelProvider = profileResourceModelProvider; - _schemaNameMapProvider = schemaNameMapProvider; + _serviceProvider = serviceProvider; } public void Configure(MvcNewtonsoftJsonOptions options) @@ -45,18 +38,9 @@ public void Configure(MvcNewtonsoftJsonOptions options) options.SerializerSettings.DateTimeZoneHandling = DateTimeZoneHandling.Utc; options.SerializerSettings.DateParseHandling = DateParseHandling.None; options.SerializerSettings.Formatting = Formatting.Indented; - options.SerializerSettings.ContractResolver - = new ProfilesAwareContractResolver( - _profileContentTypeContextProvider, - _dataManagementResourceContextProvider, - _profileResourceModelProvider, - _schemaNameMapProvider) - { - NamingStrategy = new CamelCaseNamingStrategy - { - ProcessDictionaryKeys = true, - OverrideSpecifiedNames = true - } - }; + + var contactResolver = _serviceProvider.GetService(); + + options.SerializerSettings.ContractResolver = contactResolver; } -} +} \ No newline at end of file diff --git a/Application/EdFi.Ods.Api/Startup/OdsStartupBase.cs b/Application/EdFi.Ods.Api/Startup/OdsStartupBase.cs index a743a3dbdf..acf68813aa 100644 --- a/Application/EdFi.Ods.Api/Startup/OdsStartupBase.cs +++ b/Application/EdFi.Ods.Api/Startup/OdsStartupBase.cs @@ -220,6 +220,14 @@ public void ConfigureServices(IServiceCollection services) services.AddHealthCheck(Configuration, _apiSettings); services.AddScheduledJobs(); + // Identify all EdFi.Ods.* assemblies + var mediatorAssemblies = AppDomain.CurrentDomain.GetAssemblies() + .Where(a => a.GetName().Name.StartsWith("EdFi.Ods.")) + .ToArray(); + + // Add all the MediatR services from the Ed-Fi assemblies + services.AddMediatR(configuration => configuration.RegisterServicesFromAssemblies(mediatorAssemblies)); + ConfigurePluginsServices(); void ConfigurePluginsServices() diff --git a/Application/EdFi.Ods.Common/Caching/CachingInterceptor.cs b/Application/EdFi.Ods.Common/Caching/CachingInterceptor.cs index 9445804362..072502b162 100644 --- a/Application/EdFi.Ods.Common/Caching/CachingInterceptor.cs +++ b/Application/EdFi.Ods.Common/Caching/CachingInterceptor.cs @@ -9,7 +9,7 @@ namespace EdFi.Ods.Common.Caching; -public class CachingInterceptor : IInterceptor +public class CachingInterceptor : IInterceptor, IClearable { private readonly ICacheProvider _cacheProvider; @@ -75,4 +75,16 @@ protected virtual ulong GenerateCacheKey(MethodInfo method, object[] arguments) "Support for generating cache keys for more than 3 arguments has not been implemented."); } } + + public void Clear() + { + if (_cacheProvider is IClearable clearable) + { + clearable.Clear(); + + return; + } + + throw new NotSupportedException($"Unable to clear the underlying data associated with the {nameof(CachingInterceptor)}."); + } } diff --git a/Application/EdFi.Ods.Common/Caching/InterceptorCacheKeys.cs b/Application/EdFi.Ods.Common/Caching/InterceptorCacheKeys.cs new file mode 100644 index 0000000000..e535fb69ff --- /dev/null +++ b/Application/EdFi.Ods.Common/Caching/InterceptorCacheKeys.cs @@ -0,0 +1,11 @@ +namespace EdFi.Ods.Common.Caching; + +public static class InterceptorCacheKeys +{ + public const string Security = "cache-security"; + public const string Descriptors = "cache-descriptors"; + public const string OdsInstances = "cache-ods-instances"; + public const string ProfileMetadata = "cache-profile-metadata"; + public const string ApiClientDetails = "cache-api-client-details"; + public const string ModelStateKey = "cache-model-state-key"; +} diff --git a/Application/EdFi.Ods.Common/Models/IMappingContractProvider.cs b/Application/EdFi.Ods.Common/Models/IMappingContractProvider.cs index aaecd58031..defd6b7ba7 100644 --- a/Application/EdFi.Ods.Common/Models/IMappingContractProvider.cs +++ b/Application/EdFi.Ods.Common/Models/IMappingContractProvider.cs @@ -3,6 +3,7 @@ // The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. // See the LICENSE and NOTICES files in the project root for more information. +using EdFi.Ods.Common.Caching; using EdFi.Ods.Common.Models.Domain; namespace EdFi.Ods.Common.Models; @@ -11,7 +12,7 @@ namespace EdFi.Ods.Common.Models; /// Defines a method for obtaining a mapping contract appropriate for a resource/entity in the current context that indicates /// what should be mapped/synchronized between entities/resources. /// -public interface IMappingContractProvider +public interface IMappingContractProvider : IClearable { /// /// Gets a mapping contract appropriate for a resource/entity in the current context. diff --git a/Application/EdFi.Ods.Common/Models/MappingContractProvider.cs b/Application/EdFi.Ods.Common/Models/MappingContractProvider.cs index c1e93682de..d287bd8d3e 100644 --- a/Application/EdFi.Ods.Common/Models/MappingContractProvider.cs +++ b/Application/EdFi.Ods.Common/Models/MappingContractProvider.cs @@ -16,6 +16,7 @@ using EdFi.Ods.Common.Profiles; using EdFi.Ods.Common.Security.Claims; using EdFi.Ods.Common.Utils.Profiles; +using log4net; namespace EdFi.Ods.Common.Models; @@ -25,6 +26,7 @@ public class MappingContractProvider : IMappingContractProvider private readonly IContextProvider _profileContentTypeContextProvider; private readonly IProfileResourceModelProvider _profileResourceModelProvider; private readonly ISchemaNameMapProvider _schemaNameMapProvider; + private readonly ILog _logger = LogManager.GetLogger(typeof(MappingContractProvider)); private readonly ConcurrentDictionary _mappingContractByKey = new(); @@ -65,7 +67,7 @@ public IMappingContract GetMappingContract(FullName resourceClassFullName) throw new BadRequestException( "The resource in the profile-based content type does not match the resource targeted by the request."); } - + var mappingContractKey = new MappingContractKey( resourceClassFullName, profileContentTypeContext.ProfileName, @@ -177,7 +179,8 @@ private IMappingContract GetOrCreateMappingContract(MappingContractKey mappingCo return profileResourceClass.AllPropertyByName.ContainsKey(memberName) || profileResourceClass.EmbeddedObjectByName.ContainsKey(memberName) || - profileResourceClass.CollectionByName.ContainsKey(memberName); + profileResourceClass.CollectionByName.ContainsKey(memberName) || + profileResourceClass.ReferenceByName.ContainsKey(memberName); } if (parameterInfo.Name.EndsWith("Included")) @@ -211,6 +214,16 @@ private IMappingContract GetOrCreateMappingContract(MappingContractKey mappingCo return mappingContract; } + + public void Clear() + { + if (_logger.IsDebugEnabled) + { + _logger.Debug("Clears mapping Contracts due to profile metadata cache expiration..."); + } + + _mappingContractByKey.Clear(); + } } public class MappingContractKey : IEquatable @@ -314,4 +327,4 @@ public override int GetHashCode() return hashCode; } } -} +} \ No newline at end of file diff --git a/Application/EdFi.Ods.Features/Container/Modules/OpenApiMetadataModule.cs b/Application/EdFi.Ods.Features/Container/Modules/OpenApiMetadataModule.cs index 6a9f181878..13a68513e2 100644 --- a/Application/EdFi.Ods.Features/Container/Modules/OpenApiMetadataModule.cs +++ b/Application/EdFi.Ods.Features/Container/Modules/OpenApiMetadataModule.cs @@ -29,7 +29,7 @@ public OpenApiMetadataModule(ApiSettings apiSettings) public override void ApplyConfigurationSpecificRegistrations(ContainerBuilder builder) { builder.RegisterType() - .AsImplementedInterfaces() + .As() .SingleInstance(); builder.RegisterType() diff --git a/Application/EdFi.Ods.Features/Container/Modules/ProfilesModule.cs b/Application/EdFi.Ods.Features/Container/Modules/ProfilesModule.cs index 27caf3a9e4..698704dca1 100644 --- a/Application/EdFi.Ods.Features/Container/Modules/ProfilesModule.cs +++ b/Application/EdFi.Ods.Features/Container/Modules/ProfilesModule.cs @@ -73,7 +73,7 @@ public override void ApplyConfigurationSpecificRegistrations(ContainerBuilder bu builder.RegisterType() .AsImplementedInterfaces() - .SingleInstance(); + .SingleInstance(); builder.RegisterType() .As() diff --git a/Application/EdFi.Ods.Features/OpenApiMetadata/Providers/OpenApiMetadataCacheProvider.cs b/Application/EdFi.Ods.Features/OpenApiMetadata/Providers/OpenApiMetadataCacheProvider.cs index bf3cbccdea..9c898b7250 100644 --- a/Application/EdFi.Ods.Features/OpenApiMetadata/Providers/OpenApiMetadataCacheProvider.cs +++ b/Application/EdFi.Ods.Features/OpenApiMetadata/Providers/OpenApiMetadataCacheProvider.cs @@ -8,26 +8,22 @@ using System.Collections.Generic; using System.Diagnostics; using System.Linq; -using System.Threading; -using System.Threading.Tasks; using EdFi.Common.Extensions; using EdFi.Ods.Api.Constants; using EdFi.Ods.Api.Models; using EdFi.Ods.Api.Providers; using EdFi.Ods.Api.Routing; using EdFi.Ods.Common.Models; -using EdFi.Ods.Common.Profiles; using EdFi.Ods.Features.OpenApiMetadata.Dtos; using EdFi.Ods.Features.OpenApiMetadata.Factories; using EdFi.Ods.Features.OpenApiMetadata.Strategies.ResourceStrategies; using log4net; -using MediatR; using Microsoft.OpenApi; using OpenApiMetadataSections = EdFi.Ods.Api.Constants.OpenApiMetadataSections; namespace EdFi.Ods.Features.OpenApiMetadata.Providers { - public class OpenApiMetadataCacheProvider : IOpenApiMetadataCacheProvider, INotificationHandler + public class OpenApiMetadataCacheProvider : IOpenApiMetadataCacheProvider { private const string Descriptors = "descriptors"; private const string Resources = "resources"; @@ -110,6 +106,11 @@ public OpenApiContent GetOpenApiContentByFeedName(string feedName) public void ResetCacheInitialization() { + if (_logger.IsDebugEnabled) + { + _logger.Debug("Resetting OpenApiMetadata initialization due to profile metadata cache expiration..."); + } + // Reset the underlying cache _openApiMetadataMetadataCache = new Lazy>(LazyInitializeCache); } @@ -224,17 +225,5 @@ void AddToCache(IEnumerable openApiContents) } } } - - public Task Handle(ProfileMetadataCacheExpired notification, CancellationToken cancellationToken) - { - if (_logger.IsDebugEnabled) - { - _logger.Debug("Resetting OpenApiMetadata initialization due to profile metadata cache expiration..."); - } - - ResetCacheInitialization(); - - return Task.CompletedTask; - } } } diff --git a/Application/EdFi.Ods.Features/Profiles/ProfileMetadataCacheExpiredNotificationHandler.cs b/Application/EdFi.Ods.Features/Profiles/ProfileMetadataCacheExpiredNotificationHandler.cs index 485157fa6a..744b87a972 100644 --- a/Application/EdFi.Ods.Features/Profiles/ProfileMetadataCacheExpiredNotificationHandler.cs +++ b/Application/EdFi.Ods.Features/Profiles/ProfileMetadataCacheExpiredNotificationHandler.cs @@ -5,7 +5,14 @@ using System.Threading; using System.Threading.Tasks; +using Autofac.Features.Indexed; +using Castle.DynamicProxy; +using EdFi.Common.Security; using EdFi.Ods.Api.Jobs; +using EdFi.Ods.Api.Providers; +using EdFi.Ods.Api.Serialization; +using EdFi.Ods.Common.Caching; +using EdFi.Ods.Common.Models; using EdFi.Ods.Common.Profiles; using log4net; using MediatR; @@ -13,17 +20,47 @@ namespace EdFi.Ods.Features.Profiles; /// -/// Handles the notification of profile metadata cache expiration to schedule the -/// job for execution. +/// Handles the notification of profile metadata cache expiration. +/// +/// +/// Clears the cache. +/// +/// +/// Schedule the job for execution. +/// +/// +/// Clears the cache. +/// +/// +/// Clears the cache. +/// +/// +/// Clears the cache. +/// +/// /// public class ProfileMetadataCacheExpiredNotificationHandler : INotificationHandler { + private readonly IInterceptor _apiClientDetailsInterceptor; private readonly IApiJobScheduler _apiJobScheduler; + private readonly ProfilesAwareContractResolver _profilesAwareContractResolver; + private readonly IOpenApiMetadataCacheProvider _openApiMetadataCacheProvider; + private readonly IMappingContractProvider _mappingContractProvider; + private readonly ILog _logger = LogManager.GetLogger(typeof(ProfileMetadataCacheExpiredNotificationHandler)); - public ProfileMetadataCacheExpiredNotificationHandler(IApiJobScheduler apiJobScheduler) + public ProfileMetadataCacheExpiredNotificationHandler( + IIndex interceptorIndex, + IApiJobScheduler apiJobScheduler, + ProfilesAwareContractResolver profilesAwareContractResolver, + IOpenApiMetadataCacheProvider openApiMetadataCacheProvider, + IMappingContractProvider mappingContractProvider) { + _apiClientDetailsInterceptor = interceptorIndex[InterceptorCacheKeys.ApiClientDetails]; _apiJobScheduler = apiJobScheduler; + _profilesAwareContractResolver = profilesAwareContractResolver; + _openApiMetadataCacheProvider = openApiMetadataCacheProvider; + _mappingContractProvider = mappingContractProvider; } /// @@ -37,5 +74,13 @@ public async Task Handle(ProfileMetadataCacheExpired notification, CancellationT await _apiJobScheduler.AddSingleExecutionJob( nameof(ProfileMetadataCacheExpiredNotificationHandler)); + + _profilesAwareContractResolver.Clear(); + _openApiMetadataCacheProvider.ResetCacheInitialization(); + _mappingContractProvider.Clear(); + + // Clears te ApiClientDetails cache, to handle scenarios where a Profile is removed + // from the database + (_apiClientDetailsInterceptor as IClearable).Clear(); } }