Reputation: 372
I am trying to inject a collection of implementations of IResourceService
into another implementation of IResourceService
so that the first implementation is able to redirect to the correct service in the case where it can't operate on the resource. Every implementation of this I have tried so far has lead to a Cyclic Dependency - so I either want to know how to work around this or how to design this so that it isn't a problem.
I'm creating a number of controllers that all look after different resources. I'll simplify for the case of this question - but in effect I have a PetController
that is able to do simple operations on pets which are all provided Ids. So for example navigating to /pet/1234
will give you a summary of the pet's details and what type of pet it is.
I then have a number of controllers for each pet type I.e. DogController
, CatController
and HamsterController
. All of these inherit from a BasePetController
which is actually doing all of the heavy lifting. In the scenario where you perform an operation on /dog/1234
but PetId 1234 actually corresponds to a Hamster, I want the DogController
to return an appropriate response (Redirect on a Get, Confict on an update etc).
So I have something like this:
BasePetController : IResourceController
{
BasePetController(IResourceController[] resourceControllers){}
abstract Type SupportedResourceType { get; }
}
DogController : BasePetController
{
DogController(IResourceController[] resourceControllers) : base(resourceControllers) {}
Type SupportedResourceType => typeof(Dog);
}
This is then added to DI like so:
services.AddSingleton<IResourceController, DogController>();
services.AddSingleton<IResourceController, CatController>(); // etc.
This leads to a cyclic dependency because the DogController
needs itself in order to create itself effectively.
I have tried to inject a ResourceControllerSelector
to separate out this conflict a little further but no dice.
What I want is for the controller to go (In Pseudo Code)
If (can't update dog because it isn't a dog) {
controller = getResourceControllerForPet()
action = controller.GetActionForUpdateOperation()
return information for the user to go to the correct action
}
I think the individual pet controllers should be responsible for knowing whether or not that can handle a specific resource - rather than another service knowing all of this information + which actions to call etc. But can't figure out how to give each pet controller the ability to go and get this information themselves and/or in the API layer without creating a cyclic dependency.
Any help on this would be much appreciated. Whether its help with the dependency injection problem, or the design of the services required or even the API itself.
Adding implementation of the ResourceControllerSelector
where resourceControllers
is an IEnumerable<IResourceController>
.
public IResourceController SelectController(Type resourceType)
{
EnsureArg.IsNotNull(resourceType, nameof(resourceType));
return this.resourceControllers.SingleOrDefault(x => x.SupportedResourceType == resourceType)
?? throw new ArgumentOutOfRangeException(nameof(resourceType), resourceType, "No resource controller has been configured to handle the provided resource.");
}
Upvotes: 1
Views: 1225
Reputation: 372
I think this question has diverged, there is the question itself (which was my first attempt at solving the problem I had) and then the problem I had. So I thought I'd update the question with what I have learned.
Short answer is this isn't possible with the ASP.Net standard dependency injection, it isn't advanced enough. However with things like Autofac and/or SimpleInjector you can do conditional injection or context driven injection. In this world you can say inject all Interface implementations that don't meet a certain criteria. So you can say don't inject if the implementation matches the on you are trying to create at the moment. I haven't tried this in anger but in theory this would get around the problem
The reason I wanted this was so that I could return accurate SeeOther
(303) responses if you tried to perform an action using the DogController
on a Hamster. In my mind that meant injecting the other controllers so that it could query them for information (I.e. find the actionName and controllerName of the correct action). However another way is to use reflection and attributes to denote what the controller is responsible for. So I instead went with something like this:
I decorate each resource controller like so: [ResourceController(Type = typeof(Dog), ControllerName = "Dog")] public class DogController : BasePetController
And then I have a helper class here called SeeOtherResultResolver
that scans the repository for the correct controller and then for the right HttpGet
or HttpDelete
etc attribute.
private static readonly IDictionary<string, Type> AttributeMap
= new Dictionary<string, Type>
{
{ "GET", typeof(HttpGetAttribute) },
{ "DELETE", typeof(HttpDeleteAttribute) },
{ "PUT", typeof(HttpPutAttribute) },
};
public string ResolveForSeeOtherResult(IUrlHelper urlHelper, object resource, object routeValues = null)
{
string scheme = urlHelper.ActionContext.HttpContext.Request.Scheme;
var httpAttribute = AttributeMap[urlHelper.ActionContext.HttpContext.Request.Method];
var resourceControllerTypes = resource.GetType().Assembly.GetTypes()
.Where(type => type.IsDefined(typeof(ResourceControllerAttribute), false));
foreach (var type in resourceControllerTypes)
{
var resourceControllerAttribute = type.GetCustomAttributes(false).OfType<ResourceControllerAttribute>().Single();
if (resourceControllerAttribute.Value == resource.GetType())
{
var methodInfo = type.GetMethods().Where(x => x.IsDefined(httpAttribute, false)).Single();
var result = urlHelper.Action(methodInfo.Name, resourceControllerAttribute.ControllerName, routeValues, scheme);
return result;
}
}
var errorMessage = string.Format(ErrorMessages.UnrecognisedResourceType, resource.GetType());
throw new InvalidOperationException(errorMessage);
}
Thanks for the help everyone. I really do appreciate it, you made me realise I was barking up the wrong tree a little bit!
Upvotes: 1
Reputation: 574
i think i get your problem.
The solution is simple just create stubbed interfaces for each pet concrete type so it will look like this. And each controller will use its own resource.
public DogController {
public DogController(IDogResource dogResource)
}
public CatController {
public CatController(ICatResource dogResource)
}
public interface IDogResource : IResource
{
}
public interface ICatResource : IResource
{
}
public interface IResource{
//your common logic
}
services.AddSingleton<DogResource, IDogResource>();
services.AddSingleton<CatResource, ICatResource>();
**edit in case, if you have some common logic in base controller you need to modify above code to this form and the rest is same
public BasePetController {
protected IResource resource;
public BasePetController (IResource resource)
{
this.resource = resource;
}
}
public DogController : BasePetController
{
public DogController(IDogResource dogResource): base (dogResource)
}
public CatController : BasePetController
{
public CatController(ICatResource dogResource): base (dogResource)
}
** one more edit
Thanks for the response but this isn't quite what I was looking for - what I effectively want here is to inject ICatResource and IHamsterResource into the DogController and the Dog+Hamster into the Cat one etc. But I wanted that done automatically (in theory it doesn't matter if the Dog one gets itself)
So if you want do to smth like this just take the above code and change it. Dont use concrete interface types like IDogResource etc, only use IResource, and the code will look like this
public class BasePetController {
protected IResource[] resources;
public BasePetController (IResource[] resources)
{
this.resources = resources;
}
}
public class DogController : BasePetController
{
public DogController(IResource[] resources): base (resources)
}
public class CatController : BasePetController
{
public CatController(IResource[] resources): base (resources)
}
public class DogResource : IResource
{
}
public class DogResource : IResource
{
}
public interface IResource{
}
services.AddSingleton<DogResource, IResource>();
services.AddSingleton<CatResource, IResource>();
Upvotes: 0