MicrosoftDocs / azure-docs

Open source documentation of Microsoft Azure
https://docs.microsoft.com/azure
Creative Commons Attribution 4.0 International
10.28k stars 21.46k forks source link

Entity Model DI + Bindings + External Initialization #44707

Open JoeBrockhaus opened 4 years ago

JoeBrockhaus commented 4 years ago

Issue #40741 requests some better examples for external initialization. I'm not sure if those changes have been included yet.

For Bindings:

Instead, binding data must be captured in the entry-point function declaration and then passed to the DispatchAsync<T> method. Any objects passed to DispatchAsync<T> will be automatically passed into the entity class constructor as an argument.

public class BlobBackedEntity
{
    [JsonIgnore]
    private readonly CloudBlobContainer container;

    public BlobBackedEntity(CloudBlobContainer container)
    {
        this.container = container;
    }

    // ... entity methods can use this.container in their implementations ...

    [FunctionName(nameof(BlobBackedEntity))]
    public static Task Run(
        [EntityTrigger] IDurableEntityContext context,
        [Blob("my-container", FileAccess.Read)] CloudBlobContainer container)
    {
        // passing the binding object as a parameter makes it available to the
        // entity class constructor
        return context.DispatchAsync<BlobBackedEntity>(container);
    }
}

For DI:

public class HttpEntity
{
    [JsonIgnore]
    private readonly HttpClient client;

    public class HttpEntity(IHttpClientFactory factory)
    {
        this.client = factory.CreateClient();
    }

    public Task<int> GetAsync(string url)
    {
        using (var response = await this.client.GetAsync(url))
        {
            return (int)response.StatusCode;
        }
    }

    [FunctionName(nameof(HttpEntity))]
    public static Task Run([EntityTrigger] IDurableEntityContext ctx)
        => ctx.DispatchAsync<HttpEntity>();
}

But what about combining the 2? Will bindings automatically resolve in the Entity constructor without being explicitly passed?

Should this work? (Will DispatchAsync() auto-resolve missing arguments?)

    public class BoundInjectedEntity(
        IHttpClientFactory factory, CloudBlobContainer cloudBlobContainer)
    {
        this.client = factory.CreateClient();
        this.cloudBlobContainer = cloudBlobContainer;
    }

    [FunctionName(nameof(BoundInjectedEntity))]
    public static Task Run([EntityTrigger] IDurableEntityContext ctx,
        [Blob("my-container", FileAccess.Read)] CloudBlobContainer container)
        => ctx.DispatchAsync<HttpEntity>(container);

Or is this required (manually chaining/plumbing injections)?

    public class BoundInjectedEntity(
        IHttpClientFactory factory, CloudBlobContainer container)
    {
        this.client = factory.CreateClient();
        this.container = container;
    }

    [FunctionName(nameof(BoundInjectedEntity))]
    public static Task Run([EntityTrigger] IDurableEntityContext ctx,
        IHttpClientFactory factory, 
        [Blob("my-container", FileAccess.Read)] CloudBlobContainer container)
        => ctx.DispatchAsync<HttpEntity>(factory, container);

For the second example, since there are multiple phases of Dependency Resolution at play (presumably in different memory/execution spaces), will this cause an unnecessary or unexpected use of those Injected Services and/or Resolved Instances? (ie: double-resolving transient instances, etc.)

ie: By forcing the resolution of entity-instance-consumed Services in the Dipatching context, will the Dispatching context incur higher overhead? Are Entity-Instance Services resolved and then dispatched across an App Domain, or are they resolved in the child context only? (If the former, this is perhaps moot. If the latter, that feels like a nontrivial - or at best inconsistent - perf bottleneck or create some difficult to diagnose issues. For naive examples, it's kinda trivial, but as soon as we get into multiple layers of DI it becomes important.)

Perhaps a different question would be: What is blocking the non-static Entity Constructor from being able to leverage Bindings?


Document Details

Do not edit this section. It is required for docs.microsoft.com ➟ GitHub issue linking.

DashleenBhandari-MSFT-zz commented 4 years ago

@JoeBrockhaus , Thanks for raising the issue. We will investigate and update you.

JoeBrockhaus commented 4 years ago

After navigating some of the code, perhaps the documentation here could be updated to reflect a bit more of the conditions around injection. For instance:

namespace Microsoft.Azure.WebJobs.Extensions.DurableTask
{
    //
    // Summary:
    //     Extends the durable entity context to support reflection-based invocation of
    //     entity operations.
    public static class TypedInvocationExtensions
    {
        //
        // Summary:
        //     Dynamically dispatches the incoming entity operation using reflection.
        //
        // Parameters:
        //   context:
        //     Context object to use to dispatch entity operations.
        //
        //   constructorParameters:
        //     Parameters to feed to the entity constructor. Should be primarily used for output
        //     bindings. Parameters must match the order in the constructor after ignoring parameters
        //     populated on constructor via dependency injection.
        //
        // Type parameters:
        //   T:
        //     The class to use for entity instances.
        //
        // Returns:
        //     A task that completes when the dispatched operation has finished.
        //
        // Exceptions:
        //   T:System.Reflection.AmbiguousMatchException:
        //     If there is more than one method with the given operation name.
        //
        //   T:System.MissingMethodException:
        //     If there is no method with the given operation name.
        //
        //   T:System.InvalidOperationException:
        //     If the method has more than one argument.
        //
        // Remarks:
        //     If the entity's state is null, an object of type T is created first. Then, reflection
        //     is used to try to find a matching method. This match is based on the method name
        //     (which is the operation name) and the argument list (which is the operation content,
        //     deserialized into an object array).
        public static Task DispatchAsync<T>(this IDurableEntityContext context, params object[] constructorParameters) where T : class;
    }
}