Skip to content

Commit

Permalink
NCBC-2963: Add named collection dependency injection support
Browse files Browse the repository at this point in the history
Motivation
----------
As an SDK consumer keeping my concerns separate via dependency
injection, it makes sense to be able to inject a specific collection via
DI without the receiver knowing the name of the scope or collection.
This is an extension of the current approach which allows injection of
buckets without knowing the name.

Modifications
-------------
Separate some of the logic in NamedBucketProxyGenerator into a shared
ProxyModuleBuilder class.

Switch the proxy generation of INamedBucketProvider classes from lazy
to when the DI container is being configured. This also required moving
some of our proxy generation internals out of DI into singletons.

Add INamedCollectionProvider and the related proxy generation logic.

Add IBucketBuilder and IScopeBuilder to allow extending a named bucket
provider during registration with a set of one or more named
collections. Also use extension methods to support registering the
default scope/collection.

Add a .NET 5 target to gain access to the DynamicDependencyAttribute,
and annotate the members we're accessing via reflection.

Add properties to IScope and ICouchbaseCollection to determine if they
are "_default". This will help consumers who receive a collection
blindly via INamedCollectionProvider recognize default collections.

Enable nullable reference types for the entire project.

Results
-------
SDK consumers may use syntax like this to register collections:

```cs
public interface IMyBucket : INamedBucketProvider
{
}

public interface IMyDefaultCollectionProvider : INamedCollectionProvider
{
}

public interface IMyCollectionProvider : INamedCollectionProvider
{
}

services.AddCouchbaseBucket<IMyBucket>("my-bucket", builder => {
    builder.AddDefaultCollection<IMyDefaultCollectionProvider>();
    builder.AddScope("my-scope")
        .AddCollection<IMyCollection>("my-collection");
});

```

Syntax like this may be used to consume the collections:

```cs
public class MyController : Controller
{
    private readonly IMyCollectionProvider _collectionProvider;

    public MyController(IMyCollectionProvider collectionProvider)
    {
        _collectionProvider = collectionProvider;
    }

    public async Task<IActionResult> Get()
    {
        var collection = await _collectionProvider.GetCollectionAsync();

        // Use the collection and return a result
    }
}
```

SDK consumers on .NET 5 and later should be able to safely enable
trimming because we've annotated the members we're accessing via
reflection.

Change-Id: Iaaea00aa5f1fcef53db64769e024585946d8b120
Reviewed-on: http://review.couchbase.org/c/couchbase-net-client/+/161332
Tested-by: Build Bot <build@couchbase.com>
Reviewed-by: Jeffry Morris <jeffrymorris@gmail.com>
  • Loading branch information
brantburnett authored and jeffrymorris committed Sep 20, 2021
1 parent 994b0a6 commit 520a6be
Show file tree
Hide file tree
Showing 25 changed files with 857 additions and 85 deletions.
@@ -0,0 +1,40 @@
using System;

namespace Couchbase.Extensions.DependencyInjection
{
/// <summary>
/// Extensions for <see cref="IBucketBuilder"/> and <see cref="IScopeBuilder"/>.
/// </summary>
public static class BucketBuilderExtensions
{
/// <summary>
/// Register an interface based on <see cref="INamedCollectionProvider"/> which will be injected
/// with the default scope/collection.
/// </summary>
/// <typeparam name="T">Interface inherited from <see cref="INamedCollectionProvider"/>. Must not add any members.</typeparam>
/// <param name="builder">The bucket builder.</param>
/// <returns>The <see cref="IScopeBuilder"/> for the default scope, used for chaining.</returns>
public static IScopeBuilder AddDefaultCollection<T>(this IBucketBuilder builder)
where T : class, INamedCollectionProvider =>
builder.AddDefaultScope().AddDefaultCollection<T>();

/// <summary>
/// Begin building the default scope.
/// </summary>
/// <param name="builder">The bucket builder.</param>
/// <returns>The <see cref="IScopeBuilder"/> for building the scope.</returns>
public static IScopeBuilder AddDefaultScope(this IBucketBuilder builder) =>
builder.AddScope("_default");

/// <summary>
/// Register an interface based on <see cref="INamedCollectionProvider"/> which will be injected
/// with the default scope/collection.
/// </summary>
/// <typeparam name="T">Interface inherited from <see cref="INamedCollectionProvider"/>. Must not add any members.</typeparam>
/// <param name="builder">The scope builder.</param>
/// <returns>The <see cref="IScopeBuilder"/> for chaining.</returns>
private static IScopeBuilder AddDefaultCollection<T>(this IScopeBuilder builder)
where T : class, INamedCollectionProvider =>
builder.AddCollection<T>("_default");
}
}
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFrameworks>netstandard2.0;netstandard2.1;netcoreapp2.1;netcoreapp3.1</TargetFrameworks>
<TargetFrameworks>netstandard2.0;netstandard2.1;netcoreapp2.1;netcoreapp3.1;net5.0</TargetFrameworks>
<VersionPrefix>3.0.1</VersionPrefix>
<VersionSuffix>local-$([System.DateTime]::UtcNow.ToString('yyyyMMddHHmm'))</VersionSuffix>
<AssemblyName>Couchbase.Extensions.DependencyInjection</AssemblyName>
Expand All @@ -15,13 +15,15 @@
<Version>3.0.1</Version>
<SignAssembly>false</SignAssembly>
<AssemblyOriginatorKeyFile>Couchbase.snk</AssemblyOriginatorKeyFile>
<LangVersion>8</LangVersion>
<LangVersion>9</LangVersion>

<PackageLicenseExpression>Apache-2.0</PackageLicenseExpression>
<PackageProjectUrl>https://github.com/couchbase/couchbase-net-client</PackageProjectUrl>
<PackageIconUrl>https://raw.githubusercontent.com/couchbaselabs/Linq2Couchbase/master/Packaging/couchbase-logo.png</PackageIconUrl>
<AssemblyVersion>3.0.1.0</AssemblyVersion>
<FileVersion>3.0.1.0</FileVersion>

<Nullable>enable</Nullable>
</PropertyGroup>

<PropertyGroup Condition="'$(SignAssembly)'=='true'">
Expand Down
17 changes: 17 additions & 0 deletions src/Couchbase.Extensions.DependencyInjection/IBucketBuilder.cs
@@ -0,0 +1,17 @@
using System;

namespace Couchbase.Extensions.DependencyInjection
{
/// <summary>
/// Applies additional configuration to a bucket for dependency injection.
/// </summary>
public interface IBucketBuilder
{
/// <summary>
/// Begin building a scope with one or more collections.
/// </summary>
/// <param name="scopeName">Name of the scope.</param>
/// <returns>The <see cref="IScopeBuilder"/> for building the scope.</returns>
IScopeBuilder AddScope(string scopeName);
}
}
@@ -0,0 +1,42 @@
using System;
using System.Threading.Tasks;
using Couchbase.KeyValue;
using Microsoft.Extensions.DependencyInjection;

namespace Couchbase.Extensions.DependencyInjection
{
/// <summary>
/// Base interface for injecting specific Couchbase collections.
/// </summary>
/// <remarks>
/// Inherit an empty interface from this interface, and then use <see cref="IScopeBuilder.AddCollection{T}(string)"/>
/// to register the interface in the <see cref="IServiceCollection"/>.
/// </remarks>
/// <example>
/// <code>
/// services.AddCouchbaseBucket&lt;IMyBucket&gt;("my-bucket", builder => {
/// builder.AddDefaultCollection&lt;IMyDefaultCollection&gt;();
/// builder.AddScope("my-scope")
/// .AddCollection&lt;IMyCollection&gt;("my-collection");
/// });
/// </code>
/// </example>
public interface INamedCollectionProvider
{
/// <summary>
/// Name of the scope.
/// </summary>
string ScopeName { get; }

/// <summary>
/// Name of the collection.
/// </summary>
string CollectionName { get; }

/// <summary>
/// Returns the collection.
/// </summary>
/// <returns>The <see cref="ICouchbaseCollection" />.</returns>
ValueTask<ICouchbaseCollection> GetCollectionAsync();
}
}
20 changes: 20 additions & 0 deletions src/Couchbase.Extensions.DependencyInjection/IScopeBuilder.cs
@@ -0,0 +1,20 @@
using System;

namespace Couchbase.Extensions.DependencyInjection
{
/// <summary>
/// Applies additional configuration to a scope for dependency injection.
/// </summary>
public interface IScopeBuilder
{
/// <summary>
/// Register an interface based on <see cref="INamedCollectionProvider"/> which will be injected
/// with a specific scope and collection name.
/// </summary>
/// <typeparam name="T">Interface inherited from <see cref="INamedCollectionProvider"/>. Must not add any members.</typeparam>
/// <param name="collectionName">Name of the collection.</param>
/// <returns>The <see cref="IScopeBuilder"/> for chaining.</returns>
IScopeBuilder AddCollection<T>(string collectionName)
where T : class, INamedCollectionProvider;
}
}
@@ -0,0 +1,41 @@
using System;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;

namespace Couchbase.Extensions.DependencyInjection.Internal
{
/// <summary>
/// Default implementation of <see cref="IBucketBuilder"/>.
/// </summary>
internal class BucketBuilder : IBucketBuilder
{
private readonly IServiceCollection _services;
private readonly Type _bucketProviderType;
private readonly bool _tryAddMode;

public BucketBuilder(IServiceCollection services, Type bucketProviderType, bool tryAddMode)
{
_services = services ?? throw new ArgumentNullException(nameof(services));
_bucketProviderType = bucketProviderType ?? throw new ArgumentNullException(nameof(bucketProviderType));
_tryAddMode = tryAddMode;
}

/// <inheritdoc />
public IScopeBuilder AddScope(string scopeName) => new ScopeBuilder(this, scopeName);

internal void AddCollection(Type collectionProviderType, string scopeName, string collectionName)
{
var proxyType =
NamedCollectionProxyGenerator.Instance.GetProxy(collectionProviderType, _bucketProviderType, scopeName, collectionName);

if (_tryAddMode)
{
_services.TryAddTransient(collectionProviderType, proxyType);
}
else
{
_services.AddTransient(collectionProviderType, proxyType);
}
}
}
}
Expand Up @@ -8,7 +8,7 @@ namespace Couchbase.Extensions.DependencyInjection.Internal
internal class ClusterProvider : IClusterProvider
{
private readonly ILoggerFactory _loggerFactory;
private AsyncLazy<ICluster> _cluster;
private AsyncLazy<ICluster>? _cluster;
private bool _disposed = false;

public ClusterProvider(IOptions<ClusterOptions> options, ILoggerFactory loggerFactory)
Expand All @@ -30,7 +30,7 @@ public virtual ValueTask<ICluster> GetClusterAsync()
throw new ObjectDisposedException(nameof(ClusterProvider));
}

return new ValueTask<ICluster>(_cluster.Value);
return new ValueTask<ICluster>(_cluster!.Value);
}

/// <summary>
Expand Down
Expand Up @@ -50,7 +50,7 @@ public static void Generate(AssemblyBuilder assemblyBuilder, ModuleBuilder modul
var attributeConstructor = attributeType.GetConstructor(new[] {typeof(string)});

var attributeBuilder = new CustomAttributeBuilder(attributeConstructor!,
new object[] {Assembly.GetExecutingAssembly().GetName().Name});
new object[] {typeof(IgnoresAccessChecksToAttributeGenerator).Assembly.GetName().Name!});
assemblyBuilder.SetCustomAttribute(attributeBuilder);
}
}
Expand Down
@@ -1,6 +1,5 @@
using System;
using System.Threading.Tasks;
using Couchbase.Core;

namespace Couchbase.Extensions.DependencyInjection.Internal
{
Expand Down
@@ -1,8 +1,8 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Reflection;
using System.Reflection.Emit;
using System.Resources;

namespace Couchbase.Extensions.DependencyInjection.Internal
{
Expand All @@ -11,84 +11,55 @@ namespace Couchbase.Extensions.DependencyInjection.Internal
/// </summary>
internal class NamedBucketProxyGenerator
{
private static readonly AssemblyName DynamicAssemblyName =
#if SIGNING
new AssemblyName("Couchbase.Extensions.DependencyInjection.Dynamic, PublicKeyToken=9112ac8688e923b2")
{
KeyPair = new StrongNameKeyPair(GetKeyPair())
};
#else
new AssemblyName("Couchbase.Extensions.DependencyInjection.Dynamic");
#endif

private readonly object _lock = new object();
private ModuleBuilder _moduleBuilder;
public static NamedBucketProxyGenerator Instance { get; } = new(ProxyModuleBuilder.Instance);

private readonly ConcurrentDictionary<Type, Type> _proxyTypeCache = new ConcurrentDictionary<Type, Type>();
private readonly ProxyModuleBuilder _proxyModuleBuilder;
private readonly Dictionary<(Type type, string bucketName), Type> _proxyTypeCache = new();

public T GetProxy<T>(IBucketProvider bucketProvider, string bucketName)
where T: class, INamedBucketProvider
public NamedBucketProxyGenerator(ProxyModuleBuilder proxyModuleBuilder)
{
var proxyType = _proxyTypeCache.GetOrAdd(typeof(T), CreateProxyType);

return (T)Activator.CreateInstance(proxyType, bucketProvider, bucketName);
_proxyModuleBuilder = proxyModuleBuilder ?? throw new ArgumentNullException(nameof(proxyModuleBuilder));
}

private Type CreateProxyType(Type interfaceType)
public Type GetProxy(Type bucketProviderInterface, string bucketName)
{
if (_moduleBuilder == null)
if (!_proxyTypeCache.TryGetValue((bucketProviderInterface, bucketName), out var proxyType))
{
lock (_lock)
{
if (_moduleBuilder == null)
{
var assemblyBuilder = AssemblyBuilder.DefineDynamicAssembly(DynamicAssemblyName,
AssemblyBuilderAccess.Run);
_moduleBuilder = assemblyBuilder.DefineDynamicModule("Module");

IgnoresAccessChecksToAttributeGenerator.Generate(assemblyBuilder, _moduleBuilder);
}
}
proxyType = CreateProxyType(bucketProviderInterface, bucketName);
_proxyTypeCache.Add((bucketProviderInterface, bucketName), proxyType);
}

var typeBuilder = _moduleBuilder.DefineType(interfaceType.Name, TypeAttributes.Class | TypeAttributes.Public,
return proxyType;
}

#if NET5_0_OR_GREATER
// Make our use of reflection safe for trimming
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(NamedBucketProvider))]
#endif
private Type CreateProxyType(Type interfaceType, string bucketName)
{
var moduleBuilder = _proxyModuleBuilder.GetModuleBuilder();

var typeBuilder = moduleBuilder.DefineType($"{interfaceType.Name}+{bucketName}", TypeAttributes.Class | TypeAttributes.Public,
typeof(NamedBucketProvider));

typeBuilder.AddInterfaceImplementation(interfaceType);

var parameterTypes = new[] {typeof(IBucketProvider), typeof(string)};
var baseConstructor = typeof(NamedBucketProvider).GetConstructor(parameterTypes);
var baseConstructor = typeof(NamedBucketProvider).GetConstructor(
new[] { typeof(IBucketProvider), typeof(string) });

var constructorBuilder = typeBuilder.DefineConstructor(MethodAttributes.Public,
CallingConventions.Standard | CallingConventions.HasThis, parameterTypes);
CallingConventions.Standard | CallingConventions.HasThis,
new[] { typeof(IBucketProvider) });

var ilGenerator = constructorBuilder.GetILGenerator();
ilGenerator.Emit(OpCodes.Ldarg_0); // push "this"
ilGenerator.Emit(OpCodes.Ldarg_1); // push the I
ilGenerator.Emit(OpCodes.Ldarg_2); // push the param
ilGenerator.Emit(OpCodes.Ldarg_1); // push the IBucketProvider
ilGenerator.Emit(OpCodes.Ldstr, bucketName); // push the bucketName
ilGenerator.Emit(OpCodes.Call, baseConstructor!);
ilGenerator.Emit(OpCodes.Ret);

return typeBuilder.CreateTypeInfo()!.AsType();
}

#if SIGNING
private static byte[] GetKeyPair()
{
using var stream =
typeof(NamedBucketProxyGenerator).Assembly.GetManifestResourceStream(
"Couchbase.Extensions.DependencyInjection.Dynamic.snk");

if (stream == null)
{
throw new MissingManifestResourceException("Resource 'Couchbase.Extensions.DependencyInjection.Dynamic.snk' not found.");
}

var keyLength = (int)stream.Length;
var result = new byte[keyLength];
stream.Read(result, 0, keyLength);
return result;
}
#endif
}
}

0 comments on commit 520a6be

Please sign in to comment.