Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Release 7.0 #164

Merged
merged 18 commits into from
Mar 6, 2023
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/update-version-tag.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ jobs:
runs-on: ubuntu-latest
env:
# This is the tag that will be kept up to date with master.
DOC_VERSION: v6.0.0
DOC_VERSION: v7.0.0
# Don't update the tag at the same time another update is running.
concurrency: update-version-tag
steps:
Expand Down
1 change: 1 addition & 0 deletions docs/advanced/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ Advanced Topics
constructor-selection.rst
concurrency.rst
multitenant.rst
scopes-loadcontexts.rst
pipelines.rst
aggregate-services.rst
interceptors.rst
Expand Down
74 changes: 74 additions & 0 deletions docs/advanced/scopes-loadcontexts.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
AssemblyLoadContext and Lifetime Scopes
=======================================

In .NET Core `the AssemblyLoadContext was introduced <https://learn.microsoft.com/en-us/dotnet/core/dependency-loading/understanding-assemblyloadcontext>`_, allowing developers to dynamically load and unload assemblies from their application. This is very useful for developers writing applications with plugin-based architectures.

In order to unload the assemblies in an ``AssemblyLoadContext`` when an assembly is no longer needed, no references to types in that assembly can be held by anything outside the loaded context. This includes Autofac, which by default holds a variety of internal references and caches for types that have been registered.

As of Autofac 7.0, we've added support for indicating to Autofac that a given lifetime scope represents types loaded for a given ``AssemblyLoadContext``; when a lifetime scope created for a specific ``AssemblyLoadContext`` is unloaded, Autofac will make a *best-effort* attempt to remove all references we hold for types from the loaded context, to allow the ``AssemblyLoadContext`` to be unloaded.

You indicate that a lifetime scope is for an ``AssemblyLoadContext`` with the new ``BeginLoadContextLifetimeScope`` method. Here's a full example:

.. sourcecode:: csharp

//
// In PluginDefinition project.
//

public interface IPlugin
{
void DoSomething();
}

//
// In MyPlugin project.
//

public class MyPlugin : IPlugin
{
public void DoSomething()
{
Console.WriteLine("Hello World");
}
}

//
// In the application.
//

var builder = new ContainerBuilder();

// Components defined in the "Default" load context will be available in load context lifetime scopes.
builder.RegisterType<DefaultComponent>();

var container = builder.Build();

var loadContext = new AssemblyLoadContext("PluginContext", isCollectible: true);

using (var scope = container.BeginLoadContextLifetimeScope(loadContext, builder =>
{
var pluginAssembly = loadContext.LoadFromAssemblyPath("plugins/MyPlugin.dll");

builder.RegisterAssemblyTypes(pluginAssembly).AsImplementedInterfaces();
}))
{
// The application should reference the PluginDefinition project, which means the
// default load context will have loaded the IPlugin interface already. When the
// MyPlugin assembly gets loaded it should share the same type and allow resolution
// with the common interface.
var plugin = scope.Resolve<IPlugin>();

plugin.DoSomething();
}

loadContext.Unload();

.. note::

If you capture a reference to any resolved components, or any types in the loaded assembly, outside Autofac it's highly likely you won't be able to unload your load context.

AssemblyLoadContexts are tricky to use in such a way that unloading is guaranteed every time (whether using Autofac or not). See the dotnet documentation on `troubleshooting unloadability <https://learn.microsoft.com/en-us/dotnet/standard/assembly/unloadability#troubleshoot-unloadability-issues>`_ if you run into problems.

You can create additional lifetime scopes from your "load context scope" using the regular `BeginLifetimeScope` method, without needing to further track your load context.

That means you can load a plugin, then the plugin can resolve `ILifetimeScope` and create new scopes, with all the assembly metadata being isolated to that initial "load context scope".
148 changes: 147 additions & 1 deletion docs/register/prop-method-injection.rst
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,120 @@ To support :doc:`circular dependencies <../advanced/circular-dependencies>`, use

builder.Register(c => new A()).OnActivated(e => e.Instance.B = e.Context.Resolve<B>());

If the component is a :ref:`reflection component <register-registration-reflection-components>`, use the ``PropertiesAutowired()`` modifier to inject properties:
Required Properties
-------------------

From Autofac 7.0 onwards, for :ref:`reflection components <register-registration-reflection-components>`, all `required properties <https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/required>`_ are automatically resolved at the time of object construction, and are generally treated in much the same way as mandatory constructor arguments.

For example, given the following type:

.. sourcecode:: csharp

public class MyComponent
{
public required ILogger Logger { get; set; }

public required IConfigReader ConfigReader { get; set; }

public IDatabaseContext Context { get; set; }
}

When the component is resolved, Autofac will populate the ``Logger`` and ``ConfigReader`` properties as if they were constructor parameters. The ``Context`` property will be treated like a standard property and will not be populated by default.

Required property injection also works automatically in all base classes with required properties:

.. sourcecode:: csharp

public class ComponentBase
{
public required ILogger Logger { get; set; }
}

public class MyComponent : ComponentBase
{
public required IConfigReader ConfigReader { get; set; }
}

In the above example, resolving ``MyComponent`` would populate ``Logger`` in the base class, as well as ``ConfigReader`` in the component itself.

.. important::

Autofac does *not* consider the nullability of the type of a required property to indicate any sort of "optional" required property. If the property is marked as ``required``,
then it is required, and must be injected, or provided via a parameter, regardless of its nullability.

Required Properties and Constructors
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

You can mix-and-match constructors and required properties if you so wish:

.. sourcecode:: csharp

public class MyComponent
{
public MyComponent(ILogger logger)
{
Logger = logger;
}

private ILogger Logger { get; set; }

public required IConfigReader ConfigReader { get; set; }
}

When multiple constructors are available, by default Autofac selects the constructor with the most matching parameters (unless :doc:`custom constructor selection is used <../advanced/constructor-selection>`). This remains the case, and the set of required properties has no impact on the selected constructor.

Autofac has no idea whether or not you set a given required property inside a constructor. Take this example:

.. sourcecode:: csharp

public class MyComponent
{
public MyComponent()
{
}

public MyComponent(ILogger logger)
{
Logger = logger;
}

public required ILogger Logger { get; set; }
}

Here, the constructor that Autofac will pick is going to be the one that takes the ``ILogger`` parameter, which in turn sets the ``Logger`` property. However, since ``Logger`` is marked as a required property, Autofac will resolve ``ILogger`` a second time, and inject it into the required property.

To avoid this, mark constructors that set all your required properties with the `SetsRequiredMembers <https://learn.microsoft.com/en-us/dotnet/api/system.diagnostics.codeanalysis.setsrequiredmembersattribute>`_ attribute:

.. sourcecode:: csharp

using System.Diagnostics.CodeAnalysis;

public class MyComponent
{
public MyComponent()
{
}

[SetsRequiredMembers]
public MyComponent(ILogger logger)
{
Logger = logger;
}

public required ILogger Logger { get; set; }
}

Since the constructor is marked as setting all required members, no required property injection will occur in Autofac, when *that constructor* is used to create an instance of the component.

Required Properties and Parameters
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

Any ``TypedParameter`` provided at :doc:`registration <parameters>` or :doc:`resolve <../resolve/parameters>` will be considered when injecting required properties. However, ``NamedParameter`` and ``PositionalParameter`` are not considered valid parameters for property injection, since they are considered to only apply to constructor parameters.

PropertiesAutowired
-------------------

You can use the ``PropertiesAutowired()`` modifier at registration time to inject properties on any component:

.. sourcecode:: csharp

Expand All @@ -43,12 +156,45 @@ If the component is a :ref:`reflection component <register-registration-reflecti
// is important!
builder.RegisterType<C>().PropertiesAutowired(new MyCustomPropSelector());

.. note::

Properties set on a component because they are ``required`` may lead to duplicate injection of a property if ``PropertiesAutowired`` is also used on that component. Consider using ``PropertiesAutowired(PropertyWiringOptions.PreserveSetValues)`` to avoid repeating property injection.
tillig marked this conversation as resolved.
Show resolved Hide resolved

Manually Specifying Properties
------------------------------

If you have one specific property and value to wire up, you can use the ``WithProperty()`` modifier:

.. sourcecode:: csharp

builder.RegisterType<A>().WithProperty("PropertyName", propertyValue);

Overriding Required Properties
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tillig marked this conversation as resolved.
Show resolved Hide resolved

Any property values provided for required properties using the ``WithProperty`` method when registering a type will override the requirement to inject that property, and Autofac will use the provided value instead:

.. sourcecode:: csharp

public class MyComponent
{
public required ILogger Logger { get; set; }

public required IConfigReader ConfigReader { get; set; }
}

var builder = new ContainerBuilder();
builder.RegisterType<MyComponent>().WithProperty("Logger", new ConsoleLogger());

var container = builder.Build();

// This will not throw, despite ILogger not being registered.
// The Logger property is provided by WithProperty.
container.Resolve<MyComponent>();

Injecting Properties on an Existing Object
------------------------------------------

You can also populate *just the properties* on an object. Do this using the ``InjectUnsetProperties`` extension on a lifetime scope, which will resolve and populate properties that are *public, writable, and not yet set (null)*:

.. sourcecode:: csharp
Expand Down
38 changes: 38 additions & 0 deletions docs/register/registration.rst
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,44 @@ Note that you will still need to have the requisite parameters available at reso

.. note:: You can find advanced methods of customising which constructor to use :doc:`here <../advanced/constructor-selection>`.

Required Properties
-------------------

Starting in Autofac 7.0, in a reflection-based component, all `required properties <https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/required>`_ are automatically resolved, in the same manner as constructor parameters.

All required properties of the component *must* be resolvable services (or supplied as a :doc:`parameter <../resolve/parameters>`) otherwise an exception will be thrown when trying to resolve the component.

For example, consider a class with these properties:

.. sourcecode:: csharp

public class MyComponent
{
public required ILogger Logger { get; set; }

public required IConfigReader ConfigReader { get; set; }
}

You can register and use this class as you could if it had a constructor:

.. sourcecode:: csharp

var builder = new ContainerBuilder();
builder.RegisterType<MyComponent>();
builder.RegisterType<ConsoleLogger>().As<ILogger>();
builder.RegisterType<ConfigReader>().As<IConfigReader>();
var container = builder.Build();

using(var scope = container.BeginLifetimeScope())
{
// Logger and ConfigReader will be populated.
var component = scope.Resolve<MyComponent>();
}

Required properties are also set automatically on all base classes (if they are present); this makes required properties useful with deep object hierarchies, because it allows you to avoid having to invoke base constructors with the set of services; Autofac will set the base class properties for you.

.. note:: For more details on required property injection, see the dedicated section in the :doc:`property injection documentation <prop-method-injection>`.

Instance Components
===================

Expand Down
18 changes: 13 additions & 5 deletions docs/resolve/relationships.rst
Original file line number Diff line number Diff line change
Expand Up @@ -374,7 +374,19 @@ You might want to register that type and have an auto-generated function factory
// Throws a DependencyResolutionException:
var obj = func(1, 2, "three");

In a loosely coupled scenario where the parameters are matched on type, you shouldn't really know about the order of the parameters for a specific object's constructor. If you need to do something like this, you should use a custom delegate type instead:
Delegate Factories
^^^^^^^^^^^^^^^^^^

In a loosely coupled scenario where the parameters are matched on type, you shouldn't really know about the order of the parameters for a specific object's constructor. If you need to do something like this, you should use a :doc:`delegate factory, which you can read about in the advanced topics section <../advanced/delegate-factories>`.

RegisterGeneratedFactory
^^^^^^^^^^^^^^^^^^^^^^^^

.. important::

``RegisterGeneratedFactory`` is now marked as obsolete as of Autofac 7.0, this section is included for posterity; if you cannot use the ``Func<T>`` implicit relationship, use :doc:`delegate factories <../advanced/delegate-factories>`.

The now-obsolete way to handle a loosely coupled scenario where the parameters are matched on type was through the use of ``RegisterGeneratedFactory()``.

.. sourcecode:: csharp

Expand All @@ -394,8 +406,6 @@ Now the function will work:
var func = scope.Resolve<FactoryDelegate>();
var obj = func(1, 2, "three");

Another option you have is to use a :doc:`delegate factory, which you can read about in the advanced topics section <../advanced/delegate-factories>`.

Should you decide to use the built-in auto-generated factory behavior (``Func<X, Y, B>``) and only resolve a factory with one of each type, it will work but you'll get the same input for all constructor parameters of the same type.
tillig marked this conversation as resolved.
Show resolved Hide resolved

.. sourcecode:: csharp
Expand All @@ -406,8 +416,6 @@ Should you decide to use the built-in auto-generated factory behavior (``Func<X,
// new DuplicateTypes(1, 1, "three")
var obj = func(1, "three");

You can read more about delegate factories and the ``RegisterGeneratedFactory()`` method :doc:`in the advanced topics section <../advanced/delegate-factories>`.

Enumeration (IEnumerable<B>, IList<B>, ICollection<B>)
------------------------------------------------------
Dependencies of an *enumerable type* provide multiple implementations of the same service (interface). This is helpful in cases like message handlers, where a message comes in and more than one handler is registered to process the message.
Expand Down
1 change: 1 addition & 0 deletions docs/whats-new/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,6 @@ What's New
.. toctree::

releasenotes.rst
upgradingfrom6to7.rst
upgradingfrom5to6.rst
upgradingfrom3to4.rst
16 changes: 16 additions & 0 deletions docs/whats-new/upgradingfrom6to7.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
Upgrading from Autofac 6.x to 7.x
=================================

In the upgrade from Autofac 6.x to 7.x, the amount of breaking change was fairly minimal, and unlikely to impact most users, but there are a couple of distinct changes to make you aware of.

- We have dropped direct support for the `net5.0` TFM (and we recommend you upgrade to `net6.0` or higher), but you can still use this release on .NET 5.0 via our `netstandard2.1` TFM support.

- The introduction of :doc:`automatic injection for required properties <../register/prop-method-injection>` means that any existing required properties on components will now be automatically injected.

If a required property *cannot* be resolved during component activation, an exception will now be thrown.

Equally, if you previously used ``PropertiesAutowired()`` to populate required properties on components, those properties will now be doubly-resolved, so you may wish to remove ``PropertiesAutowired()`` in favour of the default functionality.
tillig marked this conversation as resolved.
Show resolved Hide resolved

- ``RegisterGeneratedFactory`` has been marked as obsolete. You should update your code to use the ``Func<T>`` `implicit relationship <../resolve/relationships>`_ or `delegate factories <../advanced/delegate-factories>`_.

- The definition of the ``ILifetimeScope`` interface has been changed to add the ``BeginLoadContextLifetimeScope`` method and its overloads, on .NET 5.0+. If you implement a "fake" lifetime scope for any tests, you will need to add an implementation of these methods. For most test use-cases, it's suitable to call the existing ``BeginLifetimeScope`` methods, ignoring the provided load context.