How to inject the Content and Media Services from a C# Class
Umb 8.6.3. VS projects configured as Core (custom code) and Web (Umbraco web site).
I have a custom C# class that needs access to the Content and Media Services to create pages and download images from an external API feed.
What I can't find in the docs is the method of CALLING a custom C# class with the Content and Media Services as parameters.
I have:-
logger.Info<CustomBackgroundTask>("Running Property Import API task");
IContentService contentService = new ApplicationContext.Current.Services.ContentService();
IMediaService mediaService = new ApplicationContext.Current.Services.MediaService();
int propertyRootNodeId = Convert.ToInt32(ConfigurationManager.AppSettings["propertyListNodeId"]);
int siteWideSettingsNodeId = Convert.ToInt32(ConfigurationManager.AppSettings["siteWideSettingsNodeId"]);
IContent siteWideSettings = contentService.GetById(siteWideSettingsNodeId);
ImportProperty importProperty = new ImportProperty(contentService, mediaService, propertyRootNodeId, siteWideSettings);
ImportProperties ImportProperties = new ImportProperties(contentService, importProperty, propertyRootNodeId, siteWideSettings);
ImportProperties.GetPropertyList();
Which fails because
IContentService contentService = new ApplicationContext.Current.Services.ContentService();
and
IMediaService mediaService = new ApplicationContext.Current.Services.MediaService();
Both complain about ApplicationContext not being available. The type or namespace "ApplicationContext" could not be found.
The "usings" for the calling class are:-
using System.Threading.Tasks;
using Umbraco.Core;
using Umbraco.Core.Composing;
using Umbraco.Core.Logging;
using Umbraco.Web.Scheduling;
using Umbraco.Core.Sync;
using System.Threading;
using System.Configuration;
using System;
using Umbraco.Core.Services;
using Umbraco.Core.Models;
That's the problem though, they're already in the constructor. I now need to call the TestClass with a MediaService and a ContentService. That's what I can't generate at the point of where I'm calling it.
Everything I read talks about how to construct the class being called, but there's never any information on how to call it to use it.
The TestClass doesn't have to be initiated by yourself, the dependency injection will take care of that. You'll have to add the TestClass to the dependency injection and then also get it from the constructor when you want to use it. So you would be using this where you want to use the TestClass
public TestController(TestClass testClass)
It's kinda difficult to help more without seeing the full classes and the logic being used.
I've just read a couple of books on IoC DI and managed to get an example working from scratch in MVC so I'm just about hanging in there with the concept. But getting it to work with the Umbraco services is where I'm stuck. As you asked, here's the plan/code:-
The class registration.....
using Core.Interfaces;
using Umbraco.Core;
using Umbraco.Core.Composing;
namespace Core.Services
{
public class Composer : IUserComposer
{
public void Compose(Composition composition)
{
composition.Register<IImportProperties, ImportProperties>();
}
}
}
The class that has been registered with it's requirements injected......
namespace Core.Services
{
public class ImportProperties : IImportProperties {
private readonly IContentService _contentService;
private readonly int _propertyRootNodeId;
private IContent _siteWideSettings;
private IImportProperty _importProperty;
public ImportProperties(IContentService contentService, IImportProperty importProperty, int propertyRootNodeId, IContent siteWideSettings) {
if(contentService == null) {
throw new ArgumentException("Content Service missing");
}
this._contentService = contentService;
if(propertyRootNodeId == 0) {
throw new ArgumentException("Property Page Id missing");
}
this._propertyRootNodeId = propertyRootNodeId;
if(siteWideSettings == null) {
throw new ArgumentException("Site Wide Settings missing");
}
this._siteWideSettings = siteWideSettings;
if(importProperty == null) {
throw new ArgumentException("Import Property object missing");
}
this._importProperty = importProperty;
}
public void GetPropertyList() {
var APIEndPoint = _siteWideSettings.GetValue("aPIEndpointList").ToString();
// Do more stuff
}
}
}
The point at which the registered class is used, which is a background task manager (courtesy of Mr Jump), runs every hour to import an estate agents properties.......
public override Task<bool> PerformRunAsync(CancellationToken token)
{
if (RunOnThisServer())
{
logger.Info<CustomBackgroundTask>("Running Property Import API task");
IContentService contentService = new ApplicationContext.Current.Services.ContentService();
IMediaService mediaService = new ApplicationContext.Current.Services.MediaService();
int propertyRootNodeId = Convert.ToInt32(ConfigurationManager.AppSettings["propertyListNodeId"]);
int siteWideSettingsNodeId = Convert.ToInt32(ConfigurationManager.AppSettings["siteWideSettingsNodeId"]);
IContent siteWideSettings = contentService.GetById(siteWideSettingsNodeId);
ImportProperty importProperty = new ImportProperty(contentService, mediaService, propertyRootNodeId, siteWideSettings);
ImportProperties ImportProperties = new ImportProperties(contentService, importProperty, propertyRootNodeId, siteWideSettings);
ImportProperties.GetPropertyList();
};
// returning true if you want your task to run again, false if you don't
return Task.FromResult<bool>(true);
}
Ah, I see now. ImportProperties is added to the dependency injection, but you're still trying to initialize it yourself. In your background task manager, you'll want to inject the ImportProperties in your constructor. This does mean that you'll have to remove the importProperty, propertyRootNodeId and siteWideSettings, because they aren't injected in the dependency injection.
You'll probably have to pass them to the class with a seperate function.
Thanks for your persistence but I'm now totally and utterly confused. All the descriptions give you the impression that stuff should just "be there", but it isn't. You have to "new" stuff up at some point to inject down into the constructor of the class doing the work because it's constructor is asking for it. VS won't allow you to leave them out and assume they'll be provided by the DI Container (which is how I originally thought this stuff worked).
After 300 pages of reading it seems I still have a long way to go. Is it worth it, you have to ask.
I'm converting what was a fully functioning system that was being called externally by an API call (and therefore had a request context) to use DI and be called internally by another class on a timer (and therefore without a request context). Probably needs to be re-architected specifically for DI.
Could you convert your task that is running every hour to a controller? Or have a controller call the class every hour?
We have a similar process, alhtough ours only runs once a day; in our case an Azure logic app calls a controller method, but the magic of DI is in the instantiation of the controller; we have this as our constructor:
private readonly ICourseSpecificationService _courseSpecService;
private readonly IContactImportService _contactImportService;
private readonly ICourseImportService _courseImportService;
private readonly IControllerWaitFlagService _dbService;
private readonly ILogger _logger;
//this is now instantiated via Dependency injection so we should always have our services available:
public DataImportController(ICourseImportService courseImportService, IContactImportService contactImportService,
ICourseSpecificationService courseSpecificationService, IControllerWaitFlagService dbService, ILogger logger)
{
_courseImportService = courseImportService;
_contactImportService = contactImportService;
_courseSpecService = courseSpecificationService;
_dbService = dbService;
_logger = logger;
}
You can see our custom imports are instantiated by DI and we also run our imports asynchronously - when we need to call the import methods they're "just there" via the controller, which DI knows how to create with the correct dependencies; I think the bit you're getting stuck on is creating the custom class.
So you've registered your import methods with the DI Container?
But what does the signature look like of the call that calls the DataImportController? That's the bit I can't work out. VS is telling me I need to provide them like a "normal" function call would. Which is why I'm thrashing about trying to get hold of the Content and Media Services and failing.
Sorry, I don't think I've been clear enough - here's our registration for the course import interface and class (I'll just do the one to avoid it getting confusing)! :
public class RegisterDataImportComposer : IUserComposer
{
public void Compose(Composition composition)
{
composition.Register<ICourseImportService, CourseImportService>(Lifetime.Transient);
}
}
The controller is never instantiated in the code; here's the controller method that actually does the work (well, calls another method which does the work - the process runs for a while so we have to avoid a timeout):
[HttpPost]
public async Task<HttpResponseMessage> ImportCourses(bool archive = false)
{
try
{
Guid id = Guid.NewGuid(); //Generate tracking Id
runningTasks.Add(id, "false");
string filePrefix = "";
var queryString = Request.GetQueryNameValuePairs();
if(queryString.Any() && queryString.Any(x=> x.Key.Equals("ts", StringComparison.CurrentCultureIgnoreCase)))
{
var kvp = queryString.FirstOrDefault(x => x.Key.Equals("ts", StringComparison.CurrentCultureIgnoreCase));
if (!kvp.Equals(default(KeyValuePair<string, string>)))
{
filePrefix = kvp.Value;
}
}
//if(queryStrings[])
//await System.Threading.Tasks.Task.Run(() => ImportCoursesProcessor(id, archive));
new Thread(() => ImportCoursesProcessor(id, archive, filePrefix)).Start();
//await System.Threading.Tasks.Task.Run(() => ImportCoursesProcessor(id, archive));
var acceptMessage = Request.CreateResponse(HttpStatusCode.Accepted);
acceptMessage.Headers.Add("location", string.Format("{0}://{1}/umbraco/api/DataImport/GetStatus/{2}", Request.RequestUri.Scheme, Request.RequestUri.Host, id)); //Where the engine will poll to check status
acceptMessage.Headers.Add("retry-after", "110"); //How many seconds it should wait (20 is default if not included)
return acceptMessage;
}
catch(Exception ex)
{
Logger.Error(typeof(DataImportController), "Error importing courses", ex);
}
var responseMessage = Request.CreateResponse(HttpStatusCode.InternalServerError);
return responseMessage;
}
The method above is called via the Azure logic app (or some test code in JavaScript for testing).
That method calls this method which does the actual work:
The CourseImportService Constructor signature looks like this - again, this is never called from our code; the DI sets it all up:
/// <summary>
/// Imports data from the XML file provided by the client
/// </summary>
public class CourseImportService : ICourseImportService
{
private readonly ICourseDataService _courseDataService = null;
private readonly ILogger _logger;
private readonly UmbracoContext _context;
private readonly IContentService _contentService;
private readonly IContentTypeService _contentTypeService;
private readonly IMediaService _mediaService;
#region Constructors
//public CourseImportService(ILogger logger, UmbracoContext context) : this(ApplicationContext.Current, logger, context) { }
public CourseImportService(IContentService contentService, IMediaService mediaService, IContentTypeService contentTypeService, ILogger logger,ICourseDataService courseDataService, UmbracoContext context)
{ _contentService = contentService;
_mediaService = mediaService;
_contentTypeService = contentTypeService;
_logger = logger;
_context = context;
_courseDataService = courseDataService;
//rest of code here
}
The only call that is ever made to the controller is to call the method ImportCourses, and the CourseImportService is also never instantitated in code - all of the instantiation of services etc. is done by the DI container.
If you wanted to get an instance of a class rather than a controller I think you would have to do it via the DI container itself - that's probably possible but not something you'd usually expect to do.
Thanks for this. The mist "might" be clearing a bit. I'll have to give something a try and get back. If I get it working I will post it here so as not to waste everyone's time.
Sounds like that if a class is registered then you don't have to instantiate it and then call it's methods, you can just call it's methods and because it's constructor has the things the methods need, they just work. I'm still a bit sceptical about the Umbraco services but I'll give it a bash ;)
Sounds like that if a class is registered then you don't have to instantiate it and then call it's methods, you can just call it's methods and because it's constructor has the things the methods need, they just work. I'm still a bit sceptical about the Umbraco services but I'll give it a bash ;)
Not quite - that works with controllers but they're a bit of a special case (MVC/Umbraco create them "under the hood" for you).
You've still got to get an instance of a non-static class to call a method on it - if you need a custom class to do what you're trying above then either you'll need to inject it into a controller's constructor or get a method as per my post above - otherwise you'll just get a null reference exception.
I'm just looking at getting the contentService in the class and not getting far:-
public class ImportProperties : IImportProperties {
private IContentService _contentService;
private IImportProperty _importProperty;
public ImportProperties(IContentService contentService, IImportProperty importProperty) {
if(contentService == null) {
throw new ArgumentException("Content Service is missing");
}
this._contentService = contentService;
if(importProperty == null) {
throw new ArgumentException("ImportProperty is missing");
}
this._importProperty = importProperty;
}
private readonly int _propertyRootNodeId = Convert.ToInt32(ConfigurationManager.AppSettings["propertyListNodeId"]);
private readonly int siteWideSettingsNodeId = Convert.ToInt32(ConfigurationManager.AppSettings["siteWideSettingsNodeId"]);
IContent _siteWideSettings = _contentService.GetById(siteWideSettingsNodeId);
public void GetPropertyList() {
// Do Stuff
}
The line IContent _siteWideSettings = _contentService.GetById(siteWideSettingsNodeId); has a red squiggle under _contentService and siteWideSettingsNodeId. They both complain "A field analyser can not reference a non-static field". If I make _contentService Static then I can't assign contentService to it. Same with _siteWideSettings.
Outside a method or constructor - this code will run before the constructor, so the _contentService doesn't exist at this point; you'd need to call it inside the method (this isn't a DI thing, it's a .Net thing :)).
I would likely move this sort of logic to a different method, e.g. your "GetPropertyList" method; you could also if you wanted make _siteWideSettings static but null initially and then check for null in GetPropertyList, e.g.
static IContent _siteWideSettings = null;
public void GetPropertyList() {
if(_siteWideSettings == null) {
int siteWideSettingsNodeId = Convert.ToInt32(ConfigurationManager.AppSettings["siteWideSettingsNodeId"])
_siteWideSettings = _contentService.GetById(siteWideSettingsNodeId);
}
// Do Stuff
}
Or use the Initialise method if this is an IComponent.
Hi Chris, thanks for sticking with me, nearly there, lol
You lost me at "Or use the Initialise method if this is an IComponent." Luckily it was your last sentence ;)
_siteWideSettings is used in various methods in the class so rather than be un-DRY, I put it in the constructor. Seems to work ;) Though I "might" put it in a helper class later on as other classes use it too.
I have mine all working now, well very nearly, but the injections are working so I have the ContentService and MediaService working now as well as a couple of custom classes :)
How to inject the Content and Media Services from a C# Class
Umb 8.6.3. VS projects configured as Core (custom code) and Web (Umbraco web site).
I have a custom C# class that needs access to the Content and Media Services to create pages and download images from an external API feed.
What I can't find in the docs is the method of CALLING a custom C# class with the Content and Media Services as parameters.
I have:-
Which fails because
and
Both complain about ApplicationContext not being available.
The type or namespace "ApplicationContext" could not be found.
The "usings" for the calling class are:-
What would be the right thing to do here?
Thanks.
/ Craig
You are able to inject the different services in your constructor. So in your constructor you can do the following:
Make sure to add your class to the dependency injection container with a composer though, more about that here: https://our.umbraco.com/documentation/reference/using-ioc/
If for some reason you aren't able to do the above, you can always fall back to this code:
This will get the IMediaService from the dependency injection container.
Thanks Patrick.
That's the problem though, they're already in the constructor. I now need to call the TestClass with a MediaService and a ContentService. That's what I can't generate at the point of where I'm calling it.
Everything I read talks about how to construct the class being called, but there's never any information on how to call it to use it.
The TestClass doesn't have to be initiated by yourself, the dependency injection will take care of that. You'll have to add the TestClass to the dependency injection and then also get it from the constructor when you want to use it. So you would be using this where you want to use the TestClass
It's kinda difficult to help more without seeing the full classes and the logic being used.
I've just read a couple of books on IoC DI and managed to get an example working from scratch in MVC so I'm just about hanging in there with the concept. But getting it to work with the Umbraco services is where I'm stuck. As you asked, here's the plan/code:-
The class registration.....
The Interface:-
The class that has been registered with it's requirements injected......
The point at which the registered class is used, which is a background task manager (courtesy of Mr Jump), runs every hour to import an estate agents properties.......
Ah, I see now. ImportProperties is added to the dependency injection, but you're still trying to initialize it yourself. In your background task manager, you'll want to inject the ImportProperties in your constructor. This does mean that you'll have to remove the importProperty, propertyRootNodeId and siteWideSettings, because they aren't injected in the dependency injection.
You'll probably have to pass them to the class with a seperate function.
Thanks for your persistence but I'm now totally and utterly confused. All the descriptions give you the impression that stuff should just "be there", but it isn't. You have to "new" stuff up at some point to inject down into the constructor of the class doing the work because it's constructor is asking for it. VS won't allow you to leave them out and assume they'll be provided by the DI Container (which is how I originally thought this stuff worked).
After 300 pages of reading it seems I still have a long way to go. Is it worth it, you have to ask.
I'm converting what was a fully functioning system that was being called externally by an API call (and therefore had a request context) to use DI and be called internally by another class on a timer (and therefore without a request context). Probably needs to be re-architected specifically for DI.
So to break this down a bit, from the Umbraco Docs for V8(https://our.umbraco.com/documentation/Getting-Started/Code/Umbraco-Services/)
If we have
What would you use to call Initialize? I would suggest it would have to be
SubscribeToContentSavedEventComponent(mediaService).Initialize();
Which begs the question, where do you get
mediaService
from? It's the same problem, just pushed up the tree a step.Hi Craig,
Could you convert your task that is running every hour to a controller? Or have a controller call the class every hour?
We have a similar process, alhtough ours only runs once a day; in our case an Azure logic app calls a controller method, but the magic of DI is in the instantiation of the controller; we have this as our constructor:
You can see our custom imports are instantiated by DI and we also run our imports asynchronously - when we need to call the import methods they're "just there" via the controller, which DI knows how to create with the correct dependencies; I think the bit you're getting stuck on is creating the custom class.
Does that help?
Chris.
Hi Chris,
I kind of am doing that. The code came from here initially: https://github.com/KevinJump/DoStuffWithUmbraco/tree/master/Src/DoStuff.Core/BackgroundTasks
So you've registered your import methods with the DI Container?
But what does the signature look like of the call that calls the DataImportController? That's the bit I can't work out. VS is telling me I need to provide them like a "normal" function call would. Which is why I'm thrashing about trying to get hold of the Content and Media Services and failing.
/ Craig
Hi Craig,
Sorry, I don't think I've been clear enough - here's our registration for the course import interface and class (I'll just do the one to avoid it getting confusing)! :
The controller is never instantiated in the code; here's the controller method that actually does the work (well, calls another method which does the work - the process runs for a while so we have to avoid a timeout):
The method above is called via the Azure logic app (or some test code in JavaScript for testing).
That method calls this method which does the actual work:
The CourseImportService Constructor signature looks like this - again, this is never called from our code; the DI sets it all up:
The only call that is ever made to the controller is to call the method ImportCourses, and the CourseImportService is also never instantitated in code - all of the instantiation of services etc. is done by the DI container.
If you wanted to get an instance of a class rather than a controller I think you would have to do it via the DI container itself - that's probably possible but not something you'd usually expect to do.
Thanks,
Chris.
PS - here's the call to the DataImport controller that we use for testing done in a JavaScript dashboard.
Hi Chris,
Thanks for this. The mist "might" be clearing a bit. I'll have to give something a try and get back. If I get it working I will post it here so as not to waste everyone's time.
Sounds like that if a class is registered then you don't have to instantiate it and then call it's methods, you can just call it's methods and because it's constructor has the things the methods need, they just work. I'm still a bit sceptical about the Umbraco services but I'll give it a bash ;)
Thanks,
/ Craig
No worries - DI is a bit of a nightmare the first few times you use it!
There is some documentation about how you could get an instance of your custom class here:
https://our.umbraco.com/documentation/reference/using-ioc/#accessing-lightinject-container
It looks as if you could get an instance of your class like this:
Time for dinner - let us know how you get on! :)
Not quite - that works with controllers but they're a bit of a special case (MVC/Umbraco create them "under the hood" for you).
You've still got to get an instance of a non-static class to call a method on it - if you need a custom class to do what you're trying above then either you'll need to inject it into a controller's constructor or get a method as per my post above - otherwise you'll just get a null reference exception.
I'm just looking at getting the contentService in the class and not getting far:-
The line
IContent _siteWideSettings = _contentService.GetById(siteWideSettingsNodeId);
has a red squiggle under _contentService and siteWideSettingsNodeId. They both complain "A field analyser can not reference a non-static field". If I make _contentService Static then I can't assign contentService to it. Same with _siteWideSettings.Slowly losing the will to live.
Ok - you can't have the code:
Outside a method or constructor - this code will run before the constructor, so the _contentService doesn't exist at this point; you'd need to call it inside the method (this isn't a DI thing, it's a .Net thing :)).
I would likely move this sort of logic to a different method, e.g. your "GetPropertyList" method; you could also if you wanted make _siteWideSettings static but null initially and then check for null in GetPropertyList, e.g.
Or use the Initialise method if this is an IComponent.
Hi Chris, thanks for sticking with me, nearly there, lol
You lost me at "Or use the Initialise method if this is an IComponent." Luckily it was your last sentence ;)
_siteWideSettings
is used in various methods in the class so rather than be un-DRY, I put it in the constructor. Seems to work ;) Though I "might" put it in a helper class later on as other classes use it too.Thanks,
/ Craig
Comment author was deleted
Have you tried this:
but then replacing ISiteService with IContentService ... should be an easy way to get to the ContentService without the extra DI stuff...
In this case I'm using a custom Service with some helper methods on a static class (and methods)
Comment author was deleted
Was from a quick test I did,
So I have an InterFace
then the implementation
the usercomposer
(but if you just want to access to content service ...) you don't need all those steps
Thanks Tim,
Very interesting.
I have mine all working now, well very nearly, but the injections are working so I have the ContentService and MediaService working now as well as a couple of custom classes :)
/ Craig
Comment author was deleted
Sweet, but good to know this one:
var contentService = Umbraco.Web.Composing.Current.Factory.GetInstance
Cheers, Tim
is working on a reply...
This forum is in read-only mode while we transition to the new forum.
You can continue this topic on the new forum by tapping the "Continue discussion" link below.