Skip to content

Commit

Permalink
Added additional targeting to better support dependency contexts. Fix…
Browse files Browse the repository at this point in the history
…ed issues with attributes not working as expected. Added customizable configuration at a per function app level. +semver: minor
  • Loading branch information
david-driscoll committed Dec 11, 2017
1 parent 204796c commit 2548ee7
Show file tree
Hide file tree
Showing 11 changed files with 172 additions and 28 deletions.
52 changes: 51 additions & 1 deletion samples/FunctionApp1/Function1.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
using System;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Threading;
using FunctionApp1;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
Expand All @@ -10,12 +12,17 @@
using Microsoft.Azure.WebJobs.Host;
using Microsoft.Azure.WebJobs.Host.Config;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.DependencyModel;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using Rocket.Surgery.Azure.Functions;
using Rocket.Surgery.Conventions;
using Rocket.Surgery.Conventions.Reflection;
using Rocket.Surgery.Conventions.Scanners;
using Rocket.Surgery.Extensions.Configuration;
using Rocket.Surgery.Extensions.DependencyInjection;
using Rocket.Surgery.Hosting;

[assembly: Convention(typeof(Contribution))]

Expand All @@ -35,6 +42,48 @@ public HelloWorld()
public string Value => $"Hello World! ({_myValue})";
}

//class DIConfig : IFunctionConfiguration
//{
// public IServiceProvider BuildServiceProvider(ExtensionConfigContext context, IServiceCollection services, ILogger logger,
// IHostingEnvironment environment)
// {
// var dependencyContext = DependencyContext.Load(typeof(DIConfig).Assembly);
// // DependencyContextLoader.Default.Load()

// var assemblyCandidateFinder = new DependencyContextAssemblyCandidateFinder(dependencyContext, logger);
// var assemblyProvider = new DependencyContextAssemblyProvider(dependencyContext, logger);
// var scanner = new AggregateConventionScanner(assemblyCandidateFinder);

// var extBuilder = new Microsoft.Extensions.Configuration.ConfigurationBuilder();
// var configurationBuilder = new ConfigurationBuilder(
// scanner,
// environment,
// new Microsoft.Extensions.Configuration.ConfigurationBuilder().Build(),
// extBuilder,
// logger
// );

// configurationBuilder.Build();
// var configuration = extBuilder.Build();

// services
// .AddLogging()
// .Replace(ServiceDescriptor.Singleton(context.Config.LoggerFactory));

// var builder = new ServicesBuilder(
// scanner,
// assemblyProvider,
// assemblyCandidateFinder,
// services,
// configuration,
// environment,
// logger
// );

// return builder.Build();
// }
//}

class Contribution : IServiceConvention
{
public void Register(IServiceConventionContext context)
Expand All @@ -48,7 +97,8 @@ public static class Function1
[FunctionName("Health")]
public static void Health(
[HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "health")]HttpRequest req,
[_] HelloWorld container)
[_] HelloWorld container,
CancellationToken token)
{

}
Expand Down
6 changes: 5 additions & 1 deletion samples/FunctionApp1/FunctionApp1.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<AzureFunctionsVersion>v2</AzureFunctionsVersion>
<PreserveCompilationContext>true</PreserveCompilationContext>
<GenerateRuntimeConfigurationFiles>true</GenerateRuntimeConfigurationFiles>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Sdk.Functions" Version="1.0.6" />
Expand All @@ -22,4 +24,6 @@
<CopyToPublishDirectory>Never</CopyToPublishDirectory>
</None>
</ItemGroup>
</Project>

<Import Project="..\..\src\Azure.Functions\build\Rocket.Surgery.Azure.Functions.targets" />
</Project>
4 changes: 4 additions & 0 deletions src/Azure.Functions/Rocket.Surgery.Azure.Functions.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,8 @@
<ItemGroup Condition="'$(TargetFramework)'=='netstandard1.3' or '$(TargetFramework)'=='net451'">
<PackageReference Include="NETStandard.Library" Version="1.6.1" NoWarn="true" PrivateAssets="All" />
</ItemGroup>
<ItemGroup>
<None Include="build/*.*" Pack="true" PackagePath="build" />
<None Include="buildMultiTargeting/*.*" Pack="true" PackagePath="buildMultiTargeting" />
</ItemGroup>
</Project>
28 changes: 22 additions & 6 deletions src/Azure.Functions/ServiceBindingProvider.cs
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
using System;
using System.Collections.Concurrent;
using System.Linq;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Azure.WebJobs.Description;
using Microsoft.Azure.WebJobs.Host;
using Microsoft.Azure.WebJobs.Host.Bindings;
using Microsoft.Extensions.DependencyInjection;
Expand Down Expand Up @@ -30,22 +32,36 @@ public ServiceBindingProvider(

public Task<IBinding> TryCreateAsync(BindingProviderContext context)
{
var isServiceAttribute = context.Parameter.GetCustomAttributes()
.Any(x => (x is _Attribute) || (x is InjectAttribute) || (x is ServiceAttribute));
if (isServiceAttribute)
{
return Task.FromResult(CreateBinding(context));
}

if (context.Parameter.GetCustomAttributes()
.Any(z => z.GetType().GetCustomAttributes().Any(x => x is BindingAttribute)))
{
return Task.FromResult<IBinding>(null);
}

if (_collection.All(z => z.ServiceType != context.Parameter.ParameterType)
&& context.Parameter.ParameterType != typeof(ILogger)
)
{
Console.Error.WriteLine($"Unable to bind service {context.Parameter.ParameterType.FullName}");
_logger.LogInformation("Unable to bind service {Type}", context.Parameter.ParameterType.FullName);
return null;
return Task.FromResult<IBinding>(null);
}

IBinding binding = new ServiceBinding(
bindingContext => GetScope(bindingContext.FunctionInstanceId),
context.Parameter.ParameterType
);
return Task.FromResult(binding);
return Task.FromResult(CreateBinding(context));
}

private IBinding CreateBinding(BindingProviderContext context) =>
new ServiceBinding(CreateScope,context.Parameter.ParameterType);

private IServiceScope CreateScope(BindingContext bindingContext) => GetScope(bindingContext.FunctionInstanceId);

public IServiceScope GetScope(BindingContext context)
{
return GetScope(context.FunctionInstanceId);
Expand Down
87 changes: 69 additions & 18 deletions src/Azure.Functions/ServiceConfiguration.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Reflection;
Expand All @@ -18,7 +19,18 @@

namespace Rocket.Surgery.Azure.Functions
{
public class ServiceConfiguration : IExtensionConfigProvider
public interface IFunctionConfiguration
{
IServiceProvider BuildServiceProvider(
ExtensionConfigContext context,
IEnumerable<Assembly> assemblies,
IServiceCollection services,
ILogger logger,
IHostingEnvironment environment
);
}

public class ServiceConfiguration : IExtensionConfigProvider, IFunctionConfiguration
{
public void Initialize(ExtensionConfigContext context)
{
Expand All @@ -29,19 +41,68 @@ public void Initialize(ExtensionConfigContext context)
null
);

//var logger = context.Config.LoggerFactory.CreateLogger("Worker.Container");
var logger = new TraceWriterLogger(context.Trace);

try
{
var assemblyCandidateFinder = new AppDomainAssemblyCandidateFinder(logger: logger);
var assemblyProvider = new AppDomainAssemblyProvider();
var assemblies = context.Config.TypeLocator.GetTypes()
.Where(x => !x.FullName.StartsWith("Microsoft."))
.Where(x => !x.FullName.StartsWith("System."))
.Where(x => !x.FullName.StartsWith("Azure."))
.Select(x => x.GetTypeInfo().Assembly)
.Distinct()
.Except(new[] { typeof(ServiceConfiguration).Assembly })
.ToArray();

var containerInvoker = assemblies
.SelectMany(x => x.GetTypes())
.Where(x => x.IsClass)
.FirstOrDefault(typeof(IFunctionConfiguration).IsAssignableFrom) ?? typeof(ServiceConfiguration);

var services = new ServiceCollection();
var invoker = Activator.CreateInstance(containerInvoker) as IFunctionConfiguration;
var container = invoker.BuildServiceProvider(context, assemblies, services, logger, env);

var injectBindingProvider = new ServiceBindingProvider(services, container, logger);

//context.AddBindingRule<_Attribute>().Bind(injectBindingProvider);
//context.AddBindingRule<InjectAttribute>().Bind(injectBindingProvider);
//context.AddBindingRule<ServiceAttribute>().Bind(injectBindingProvider);
context.Config.RegisterBindingExtension(injectBindingProvider);

var registry = context.Config.GetService<IExtensionRegistry>();
registry.RegisterExtension(typeof(IFunctionInvocationFilter), injectBindingProvider);
registry.RegisterExtension(typeof(IFunctionExceptionFilter), injectBindingProvider);
}
catch (Exception e)
{
logger.LogCritical(e, "error could not configure service");
}
}

public IServiceProvider BuildServiceProvider(
ExtensionConfigContext context,
IEnumerable<Assembly> assemblies,
IServiceCollection services,
ILogger logger,
IHostingEnvironment environment
)
{
try
{
var dependencyContext = assemblies
.Aggregate<Assembly, DependencyContext>(
null, (ctx, assembly) => ctx == null ? DependencyContext.Load(assembly) : ctx.Merge(DependencyContext.Load(assembly))
);

var assemblyCandidateFinder = new DependencyContextAssemblyCandidateFinder(dependencyContext, logger);
var assemblyProvider = new DependencyContextAssemblyProvider(dependencyContext, logger);
var scanner = new AggregateConventionScanner(assemblyCandidateFinder);

var extBuilder = new Microsoft.Extensions.Configuration.ConfigurationBuilder();
var configurationBuilder = new ConfigurationBuilder(
scanner,
env,
environment,
new Microsoft.Extensions.Configuration.ConfigurationBuilder().Build(),
extBuilder,
logger
Expand All @@ -50,7 +111,6 @@ public void Initialize(ExtensionConfigContext context)
configurationBuilder.Build();
var configuration = extBuilder.Build();

var services = new ServiceCollection();
services
.AddLogging()
.Replace(ServiceDescriptor.Singleton(context.Config.LoggerFactory));
Expand All @@ -61,25 +121,16 @@ public void Initialize(ExtensionConfigContext context)
assemblyCandidateFinder,
services,
configuration,
env,
environment,
logger
);

var container = builder.Build();

var injectBindingProvider = new ServiceBindingProvider(services, container, logger);

context.AddBindingRule<_Attribute>().Bind(injectBindingProvider);
context.AddBindingRule<InjectAttribute>().Bind(injectBindingProvider);
context.AddBindingRule<ServiceAttribute>().Bind(injectBindingProvider);

var registry = context.Config.GetService<IExtensionRegistry>();
registry.RegisterExtension(typeof(IFunctionInvocationFilter), injectBindingProvider);
registry.RegisterExtension(typeof(IFunctionExceptionFilter), injectBindingProvider);
return builder.Build();
}
catch (Exception e)
{
logger.LogCritical(e, "error could not configure service");
throw;
}
}
}
Expand Down
4 changes: 3 additions & 1 deletion src/Azure.Functions/TraceWriterLogger.cs
Original file line number Diff line number Diff line change
Expand Up @@ -57,16 +57,18 @@ internal static TraceLevel GetLogLevel(LogLevel logLevel)
case LogLevel.None:
return TraceLevel.Off;
case LogLevel.Error:
case LogLevel.Critical:
return TraceLevel.Error;
case LogLevel.Warning:
return TraceLevel.Warning;
case LogLevel.Information:
return TraceLevel.Info;
case LogLevel.Debug:
case LogLevel.Trace:
return TraceLevel.Verbose;
default:
throw new InvalidOperationException($"'{logLevel}' is not a valid level.");
}
}
}
}
}
2 changes: 1 addition & 1 deletion src/Azure.Functions/_Attribute.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,5 @@
namespace Rocket.Surgery.Azure.Functions
{
[Binding, AttributeUsage(AttributeTargets.Parameter)]
public class _Attribute : Attribute { }
public sealed class _Attribute : Attribute { }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
<Project>
</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<Project>
<Target Name="MoveDepsJson" AfterTargets="_GenerateFunctionsPostBuild">
<!-- TODO: CopyFilesToOutputDirectory does not look at the outdir to copy the pdbs. hence copying it manually. -->
<!-- Copy the application pdb to the bin folder-->
<Move SourceFiles="$(TargetDir)$(TargetName).deps.json" DestinationFiles="$(TargetDir)bin\$(TargetName).deps.json" OverwriteReadOnlyFiles="true" Condition="Exists('$(TargetDir)$(TargetName).deps.json')" ContinueOnError="true" />
<Move SourceFiles="$(TargetDir)$(TargetName).runtimeconfig.json" DestinationFiles="$(TargetDir)bin\$(TargetName).runtimeconfig.json" OverwriteReadOnlyFiles="true" Condition="Exists('$(TargetDir)$(TargetName).runtimeconfig.json')" ContinueOnError="true" />
<Move SourceFiles="$(TargetDir)$(TargetName).runtimeconfig.dev.json" DestinationFiles="$(TargetDir)bin\$(TargetName).runtimeconfig.dev.json" OverwriteReadOnlyFiles="true" Condition="Exists('$(TargetDir)$(TargetName).runtimeconfig.dev.json')" ContinueOnError="true" />
</Target>
</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<Project>
<Import Project="..\build\$(MSBuildThisFile)" />
</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<Project>
<Import Project="..\build\$(MSBuildThisFile)" />
</Project>

0 comments on commit 2548ee7

Please sign in to comment.