Skip to content

Commit

Permalink
[Back-port to 1.13] Expose configuration to customizations (#378)
Browse files Browse the repository at this point in the history
* Expose IConfiguration in ViewModelCompositionOptions (#374)

* Expose IConfiguration in ViewModelCompositionOptions

* Throw if configuration is not set

* Approved API

* Tests

* # Customizing ViewModel Composition options from dependent assemblies

* Link to Customizing ViewModel Composition options from dependent assemblies

(cherry picked from commit 8dd8e73)

* Add directives required to compile with .NET Standard
  • Loading branch information
mauroservienti authored Apr 8, 2022
1 parent 77d9aaa commit 1e2cf78
Show file tree
Hide file tree
Showing 8 changed files with 225 additions and 14 deletions.
4 changes: 4 additions & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,7 @@ By virtue of leveraging ASP.NET Core 3.x Endpoints ServiceComposer automatically
By default ServiceComposer serializes responses using the Newtonsoft JSON serializer. The built-in serialization support can be configured to seriazlie responses using a camel case or pascal case approach on a per request basis by adding to the request an `Accept-Casing` custom HTTP header. For more information refer to the [response serialization casing](response-serialization-casing.md) section. Or it's possible to take full control over the [response serialization settings on a case-by-case](custom-json-response-serialization-settings.md) by suppliying at configuration time a customization function.

Starting with version 1.9.0, regular MVC Output Formatters can be used to serialize the response model, and honor the `Accept` HTTP header set by clients. When using output formatters the serialization casing is controlled by the formatter configuration and not by ServiceComposer. For more information on using output formatters refers to the [output formatters serialization section](output-formatters-serialization.md).

## Customizing ViewModel Composition options from dependent assemblies

It's possible to access and [customize ViewModel Composition options](options-customizations.md) at application start-up by defining types implmenting the `IViewModelCompositionOptionsCustomization` interface.
7 changes: 7 additions & 0 deletions docs/options-customizations.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Customizing ViewModel Composition options from dependent assemblies

Assemblies containing types participating in the composition process can customize the current application `ViewModelCompositionOptions` by defining a type that implements the `IViewModelCompositionOptionsCustomization` interface. At runtime, when the application starts, types implementing the `IViewModelCompositionOptionsCustomization` will be instantiated and the `Customize` method will be invoked.

> Note: Types implementing `IViewModelCompositionOptionsCustomization` are not managed by IoC container. Dependency injection is not available.
`ViewModelCompositionOptions` offers the ability to access the application `IConfiguration` insance. By default the `ViewModelCompositionOptions.Configuration` property is null. If accessed it throws an `ArgumentException`. To enable configuration support, pass the `IConfiguration` instance when configuring ServiceComposer via the `AddViewModelComposition` extension method.
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
using System;
using System.Linq;
using System.Reflection;

namespace ServiceComposer.AspNetCore.Endpoints.Tests;

public static class IsNestedTypeOfExtension
{
public static bool IsNestedTypeOf<T>(this Type type) where T : class
{
return type.IsNested
&& typeof(T).GetNestedTypes(BindingFlags.NonPublic | BindingFlags.Public)
.Contains(type);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,13 @@
using System.Linq;
using System.Reflection;
using System.Threading.Tasks;
using Castle.Core.Configuration;
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
using ServiceComposer.AspNetCore.Testing;
using TestClassLibraryWithHandlers;
using Xunit;
using IConfiguration = Microsoft.Extensions.Configuration.IConfiguration;

namespace ServiceComposer.AspNetCore.Endpoints.Tests
{
Expand All @@ -31,8 +33,12 @@ public async Task Matching_handlers_are_found()
return true;
}

if (type.IsNested && typeof(When_using_assembly_scanner).GetNestedTypes(BindingFlags.NonPublic | BindingFlags.Public)
.Contains(type))
if (type == typeof(CustomizationsThatAccessTheConfiguration))
{
return false;
}

if (type.IsNestedTypeOf<When_using_assembly_scanner>())
{
return true;
}
Expand All @@ -56,12 +62,22 @@ public async Task Matching_handlers_are_found()
Assert.True(response.IsSuccessStatusCode);
}

private static bool _invoked = false;
class Customizations : IViewModelCompositionOptionsCustomization
static bool _invokedCustomizations = false;
class EmptyCustomizations : IViewModelCompositionOptionsCustomization
{
public void Customize(ViewModelCompositionOptions options)
{
_invokedCustomizations = true;
}
}


static IConfiguration _customizationsThatAccessTheConfigurationConfig;
class CustomizationsThatAccessTheConfiguration : IViewModelCompositionOptionsCustomization
{
public void Customize(ViewModelCompositionOptions options)
{
_invoked = true;
_customizationsThatAccessTheConfigurationConfig = options.Configuration;
}
}

Expand All @@ -82,8 +98,12 @@ public async Task Options_customization_are_invoked()
return true;
}

if (type.IsNested && typeof(When_using_assembly_scanner).GetNestedTypes(BindingFlags.NonPublic | BindingFlags.Public)
.Contains(type))
if (type == typeof(CustomizationsThatAccessTheConfiguration))
{
return false;
}

if (type.IsNestedTypeOf<When_using_assembly_scanner>())
{
return true;
}
Expand All @@ -105,7 +125,96 @@ public async Task Options_customization_are_invoked()

// Assert
Assert.True(response.IsSuccessStatusCode);
Assert.True(_invoked);
Assert.True(_invokedCustomizations);
}

[Fact]
public void Options_customization_throws_if_configuration_is_not_available()
{
Assert.Throws<ArgumentException>(() =>
{
var client = new SelfContainedWebApplicationFactoryWithWebHost<When_using_assembly_scanner>
(
configureServices: services =>
{
services.AddViewModelComposition(options =>
{
options.TypesFilter = type =>
{
if (type.Assembly.FullName.Contains("TestClassLibraryWithHandlers"))
{
return true;
}

if (type == typeof(EmptyCustomizations))
{
return false;
}

if (type.IsNestedTypeOf<When_using_assembly_scanner>())
{
return true;
}

return false;
};
});
services.AddRouting();
},
configure: app =>
{
app.UseRouting();
app.UseEndpoints(builder => builder.MapCompositionHandlers());
}
).CreateClient();
});
}

[Fact]
public void Options_customization_can_access_configuration_if_set()
{
// Arrange
var config = new FakeConfig();
var factory = new SelfContainedWebApplicationFactoryWithWebHost<When_using_assembly_scanner>
(
configureServices: services =>
{
services.AddViewModelComposition(options =>
{
options.TypesFilter = type =>
{
if (type.Assembly.FullName.Contains("TestClassLibraryWithHandlers"))
{
return true;
}

if (type == typeof(EmptyCustomizations))
{
return false;
}

if (type.IsNestedTypeOf<When_using_assembly_scanner>())
{
return true;
}

return false;
};
}, config);
services.AddRouting();
},
configure: app =>
{
app.UseRouting();
app.UseEndpoints(builder => builder.MapCompositionHandlers());
}
);

// Act
var client = factory.CreateClient();

// Assert
Assert.Same(config, _customizationsThatAccessTheConfigurationConfig);
}

[Fact]
Expand All @@ -127,8 +236,12 @@ public void ViewModel_preview_handlers_are_registered_automatically()
return true;
}

if (type.IsNested && typeof(When_using_assembly_scanner).GetNestedTypes(BindingFlags.NonPublic | BindingFlags.Public)
.Contains(type))
if (type == typeof(CustomizationsThatAccessTheConfiguration))
{
return false;
}

if (type.IsNestedTypeOf<When_using_assembly_scanner>())
{
return true;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -162,12 +162,13 @@ namespace ServiceComposer.AspNetCore
}
public static class ServiceCollectionExtensions
{
public static void AddViewModelComposition(this Microsoft.Extensions.DependencyInjection.IServiceCollection services) { }
public static void AddViewModelComposition(this Microsoft.Extensions.DependencyInjection.IServiceCollection services, System.Action<ServiceComposer.AspNetCore.ViewModelCompositionOptions> config) { }
public static void AddViewModelComposition(this Microsoft.Extensions.DependencyInjection.IServiceCollection services, Microsoft.Extensions.Configuration.IConfiguration configuration = null) { }
public static void AddViewModelComposition(this Microsoft.Extensions.DependencyInjection.IServiceCollection services, System.Action<ServiceComposer.AspNetCore.ViewModelCompositionOptions> config, Microsoft.Extensions.Configuration.IConfiguration configuration = null) { }
}
public class ViewModelCompositionOptions
{
public ServiceComposer.AspNetCore.AssemblyScanner AssemblyScanner { get; }
public Microsoft.Extensions.Configuration.IConfiguration Configuration { get; }
public ServiceComposer.AspNetCore.ResponseSerializationOptions ResponseSerialization { get; }
public Microsoft.Extensions.DependencyInjection.IServiceCollection Services { get; }
public void AddServicesConfigurationHandler(System.Type serviceType, System.Action<System.Type, Microsoft.Extensions.DependencyInjection.IServiceCollection> configurationHandler) { }
Expand Down
20 changes: 19 additions & 1 deletion src/ServiceComposer.AspNetCore/ServiceCollectionExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,21 +1,39 @@
using Microsoft.Extensions.DependencyInjection;
using System;
#if NETCOREAPP3_1 || NET5_0_OR_GREATER
using Microsoft.Extensions.Configuration;
#endif

namespace ServiceComposer.AspNetCore
{
public static class ServiceCollectionExtensions
{
#if NETCOREAPP3_1 || NET5_0_OR_GREATER
public static void AddViewModelComposition(this IServiceCollection services, IConfiguration configuration = null)
{
AddViewModelComposition(services, null, configuration);
}

public static void AddViewModelComposition(this IServiceCollection services, Action<ViewModelCompositionOptions> config, IConfiguration configuration = null)
{
var options = new ViewModelCompositionOptions(services, configuration);
config?.Invoke(options);

options.InitializeServiceCollection();
}
#else
public static void AddViewModelComposition(this IServiceCollection services)
{
AddViewModelComposition(services, null);
}

public static void AddViewModelComposition(this IServiceCollection services, Action<ViewModelCompositionOptions> config)
{
var options = new ViewModelCompositionOptions(services);
config?.Invoke(options);

options.InitializeServiceCollection();
}
#endif
}
}
26 changes: 25 additions & 1 deletion src/ServiceComposer.AspNetCore/ViewModelCompositionOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,11 @@
using System.Linq;
using System.Reflection;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
#if NETCOREAPP3_1 || NET5_0_OR_GREATER
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.Extensions.Options;
using Microsoft.Extensions.Configuration;
#endif

namespace ServiceComposer.AspNetCore
Expand All @@ -15,11 +16,19 @@ public class ViewModelCompositionOptions
{
readonly CompositionMetadataRegistry _compositionMetadataRegistry = new CompositionMetadataRegistry();
#if NETCOREAPP3_1 || NET5_0_OR_GREATER
readonly IConfiguration _configuration;
readonly CompositionOverControllersRoutes _compositionOverControllersRoutes = new CompositionOverControllersRoutes();
#endif

#if NETCOREAPP3_1 || NET5_0_OR_GREATER
internal ViewModelCompositionOptions(IServiceCollection services, IConfiguration configuration = null)
#else
internal ViewModelCompositionOptions(IServiceCollection services)
#endif
{
#if NETCOREAPP3_1 || NET5_0_OR_GREATER
_configuration = configuration;
#endif
Services = services;
AssemblyScanner = new AssemblyScanner();

Expand Down Expand Up @@ -218,6 +227,21 @@ internal void InitializeServiceCollection()
public IServiceCollection Services { get; private set; }

#if NETCOREAPP3_1 || NET5_0_OR_GREATER
public IConfiguration Configuration
{
get
{
if (_configuration is null)
{
throw new ArgumentException("No configuration instance has been set. " +
"To access the application configuration call the " +
"AddViewModelComposition overload te accepts an " +
"IConfiguration instance.");
}
return _configuration;
}
}

public ResponseSerializationOptions ResponseSerialization { get; }
#endif

Expand Down
29 changes: 29 additions & 0 deletions src/TestClassLibraryWithHandlers/FakeConfig.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
using System.Collections.Generic;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Primitives;

namespace TestClassLibraryWithHandlers;

public class FakeConfig : IConfiguration
{
public IEnumerable<IConfigurationSection> GetChildren()
{
throw new System.NotImplementedException();
}

public IChangeToken GetReloadToken()
{
throw new System.NotImplementedException();
}

public IConfigurationSection GetSection(string key)
{
throw new System.NotImplementedException();
}

public string this[string key]
{
get => throw new System.NotImplementedException();
set => throw new System.NotImplementedException();
}
}

0 comments on commit 1e2cf78

Please sign in to comment.