简体   繁体   中英

Argument-bound lifestyle in Castle Windsor

I'm letting my IoC container manage the life of my object graph and seem to be reusing a particular lifestyle a lot that is not available in the Castle Windsor default set at the moment.

Example

I need to construct objects context-specifically by passing in resolution-time arguments into a factory and when I pass in the string Dorothy for instance, I would like to get back a singleton of Person that would be instantiated with string Dorothy, if it does not exist in the container yet, or otherwise the existing Dorothy instance. Same when passing the string argument Jane into my factory - please return singleton of Person Jane. And there might be Sally, Bob and many others, the complete set of inputs unravelling at runtime only.

Manual Implementation

Here's another example, implemented using a dictionary in my custom class:

private readonly static IDictionary<string,IRecruiter> Recruiters
                        = new Dictionary<string, IRecruiter>();

private IRecruiter GetRecruiter(string recruiterId)
    {
    IRecruiter recruiter;
    if (!Recruiters.TryGetValue(recruiterId, out recruiter))
                    {
                    recruiter = this.donorFactory.CreateRecruiter(recruiterId);
                    Recruiters.Add(recruiterId,recruiter);
                    }
    return recruiter;
    }

And where the donorFactory CreateRecruiter method does something like that:

return this.Create<IRecruiter>(new Arguments { { typeof(string), recruiterId }});

SRP and Reducing Boilerplate

I find I use this pattern a lot, so would like to know if it is somehow possible to implement using existing Castle Windsor lifestyles, or maybe there is a case made to add this pattern to Castle Windsor itself?

  • Registering components as .Named does not work in this case as the set of incoming arguments is not known at design-time
  • The pattern could have a general implementation that would work with any type of argument (not just string) or even sets of arguments, if GetHashCode or something like that was used as the key for the dictionary.

I can see one way to do it by creating a custom scope accessor:

public class ArgScopeAccessor : IScopeAccessor
{
    static readonly ConcurrentDictionary<string, ILifetimeScope> collection = new ConcurrentDictionary<string, ILifetimeScope>();

    public void Dispose()
    {
        foreach (var scope in collection)
            scope.Value.Dispose();
        collection.Clear();
    }

    public ILifetimeScope GetScope(CreationContext context)
    {
        string name = (string)context.AdditionalArguments["name"];
        return collection.GetOrAdd(name, n => new DefaultLifetimeScope());
    }
}

This creates a scope based on the name argument passed into the call to resolve the object. I'd then use the typed factory facility to create an IRecruiterFactory (with a parameter named name ):

public interface IRecruiterFactory
{
    IRecruiter Create(string name);
}

And assuming your IRecruiter and Recruiter are something like this:

public interface IRecruiter
{
    string Name { get; }
}

public class Recruiter : IRecruiter
{
    public Recruiter(string name)
    {
        Name = name;
    }

    public string Name { get; private set; }
}

Then you could set up your container to use this scope in a reusable way like this:

container.AddFacility<TypedFactoryFacility>();

container.Register(Component.For<IRecruiterFactory>()
                            .AsFactory());
container.Register(Component.For<IRecruiter>()
                            .ImplementedBy<Recruiter>()
                            .LifestyleScoped<ArgScopeAccessor>());

So now, resolving a recruiter by name will resolve the same instance:

IRecruiterFactory recruiterFactory = container.Resolve<IRecruiterFactory>();
IRecruiter jane1 = recruiterFactory.Create("Jane");
IRecruiter susan = recruiterFactory.Create("Susan");
IRecruiter jane2 = recruiterFactory.Create("Jane");

Console.WriteLine("Jane 1: " + jane1.GetHashCode());
Console.WriteLine("Jane 2: " + jane2.GetHashCode());
Console.WriteLine("Susan:  " + susan.GetHashCode());

Displays:

Jane 1: 60467532
Jane 2: 60467532
Susan:  63249743

Obviously you can create a different scope accessor to address any of the other ways you want to discriminate on types, such as hash code or a combination of arguments. I don't, however, see a way to change the name constant in the ArgScopeAccessor since there's no way to pass a constructor argument in when configuration the scope accessor on the container. But that could be solved with a base type and derived types just specifying the constant.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM