Copied to clipboard

Flag this post as spam?

This post will be reported to the moderators as potential spam to be looked at


  • Craig100 1136 posts 2523 karma points c-trib
    Jun 30, 2020 @ 11:58
    Craig100
    0

    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;
    

    What would be the right thing to do here?

    Thanks.

    / Craig

  • Patrick de Mooij 72 posts 622 karma points MVP 3x c-trib
    Jun 30, 2020 @ 13:06
    Patrick de Mooij
    0

    You are able to inject the different services in your constructor. So in your constructor you can do the following:

    public TestClass(IMediaService mediaService, IContentService contentService)
    

    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:

    DependencyResolver.Current.GetService<IMediaService>();
    

    This will get the IMediaService from the dependency injection container.

  • Craig100 1136 posts 2523 karma points c-trib
    Jun 30, 2020 @ 13:14
    Craig100
    0

    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.

  • Patrick de Mooij 72 posts 622 karma points MVP 3x c-trib
    Jun 30, 2020 @ 13:29
    Patrick de Mooij
    0

    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.

  • Craig100 1136 posts 2523 karma points c-trib
    Jun 30, 2020 @ 13:39
    Craig100
    0

    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 Interface:-

    namespace Core.Interfaces
    {
        public interface IImportProperties
        {
            void GetPropertyList();
        }
    }
    

    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);
    }
    
  • Patrick de Mooij 72 posts 622 karma points MVP 3x c-trib
    Jun 30, 2020 @ 13:53
    Patrick de Mooij
    0

    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.

  • Craig100 1136 posts 2523 karma points c-trib
    Jun 30, 2020 @ 14:50
    Craig100
    0

    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.

  • Craig100 1136 posts 2523 karma points c-trib
    Jun 30, 2020 @ 15:32
    Craig100
    0

    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

        private readonly IMediaService _mediaService;
        public SubscribeToContentSavedEventComponent(IMediaService mediaService)
        {
            _mediaService = mediaService;
        }
    
        public void Initialize()
        {
            //Do some stuff
        }
    

    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.

  • Chris Norwood 131 posts 642 karma points
    Jun 30, 2020 @ 15:33
    Chris Norwood
    0

    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:

    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.

    Does that help?

    Chris.

  • Craig100 1136 posts 2523 karma points c-trib
    Jun 30, 2020 @ 15:38
    Craig100
    0

    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

  • Chris Norwood 131 posts 642 karma points
    Jun 30, 2020 @ 15:51
    Chris Norwood
    0

    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)! :

    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:

    private void ImportCoursesProcessor(Guid id, bool archive = false, string filePrefix = "")
            {
                try
                {
                    //Logger.Debug(typeof(DataImportController), "attempting to get course import service");
                    //var courseImportService = new Services.CourseImportService(Services.ContentService, Services.MediaService, Services.ContentTypeService, Logger, UmbracoContext);
                    Logger.Debug(typeof(DataImportController), "Calling import courses");
                    CreateDBFlag(id, "Update Courses");
                    _courseImportService.ImportFromXmlFile(archive, filePrefix);
                    runningTasks[id] = "true";
                    DeleteDBFlag(id);
                }
                catch(Exception ex)
                {
                    Logger.Error(typeof(DataImportController), "Error importing courses", ex);
                    runningTasks[id] = string.Format("Error importing courses: {0} ", ex.Message);
                }
            }
    

    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,

    Chris.

  • Chris Norwood 131 posts 642 karma points
    Jun 30, 2020 @ 16:00
    Chris Norwood
    0

    PS - here's the call to the DataImport controller that we use for testing done in a JavaScript dashboard.

    $scope.testCourseImport = function () {
            //alert('loading courses!');
            $scope.loading = true;
            $http.post("/umbraco/Api/DataImport/ImportCourses?archive=false&ts=2020-03-15T23_33_27.3873139Z").success(function (r) {
                $scope.loading = false;
                $scope.setSearchSettingsResult = r;
            }).error(function (r) {
                $scope.loading = false;
                alert("An error occurred during the AJAX request");
            });
        };
    
  • Craig100 1136 posts 2523 karma points c-trib
    Jun 30, 2020 @ 16:05
    Craig100
    0

    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

  • Chris Norwood 131 posts 642 karma points
    Jun 30, 2020 @ 16:08
    Chris Norwood
    0

    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:

    var container = composition.Concrete as LightInject.ServiceContainer;
    var instance = container.GetInstance<IImportProperties>();
    

    Time for dinner - let us know how you get on! :)

  • Chris Norwood 131 posts 642 karma points
    Jun 30, 2020 @ 16:27
    Chris Norwood
    0

    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.

  • Craig100 1136 posts 2523 karma points c-trib
    Jun 30, 2020 @ 17:13
    Craig100
    0

    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.

    Slowly losing the will to live.

  • Chris Norwood 131 posts 642 karma points
    Jun 30, 2020 @ 17:23
    Chris Norwood
    0

    Ok - you can't have the code:

    IContent _siteWideSettings = _contentService.GetById(siteWideSettingsNodeId);
    

    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.

  • Craig100 1136 posts 2523 karma points c-trib
    Jun 30, 2020 @ 17:37
    Craig100
    0

    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

    Jun 30, 2020 @ 19:11

    Have you tried this:

     var siteService = Umbraco.Web.Composing.Current.Factory.GetInstance<ISiteService>();
    
     return siteService.GetWebsite(umbracoHelper.AssignedContentItem.Id);
    

    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

    Jun 30, 2020 @ 19:14

    Was from a quick test I did,

    So I have an InterFace

     public interface ISiteService
    {
        Website GetWebsite(int id);
        ZoekPagina GetZoekPagina(int id);
    }
    

    then the implementation

        public class SiteService : ISiteService
    {
        private readonly IUmbracoContextFactory _umbracoContextFactory;
    
        public SiteService(IUmbracoContextFactory umbracoContextFactory)
        {
            _umbracoContextFactory = umbracoContextFactory;
    
        }
        public Website GetWebsite(int id)
        {
            Website website = null;
            var alias = (typeof(Website).GetCustomAttributes(true).FirstOrDefault(x => x.GetType() == typeof(PublishedModelAttribute)) as PublishedModelAttribute).ContentTypeAlias;
    
            using (UmbracoContextReference umbracoContextReference = _umbracoContextFactory.EnsureUmbracoContext())
            {
    
                website = umbracoContextReference.UmbracoContext.Content.GetById(id).AncestorOrSelf(alias) as Website;
            }
    
            return website;
        }
    
        public ZoekPagina GetZoekPagina(int id)
        {
            ZoekPagina zoekpagina = null;
            var alias = (typeof(ZoekPagina).GetCustomAttributes(true).FirstOrDefault(x => x.GetType() == typeof(PublishedModelAttribute)) as PublishedModelAttribute).ContentTypeAlias;
    
            using (UmbracoContextReference umbracoContextReference = _umbracoContextFactory.EnsureUmbracoContext())
            {
    
                zoekpagina = GetWebsite(id).DescendantOfType(alias) as ZoekPagina;
            }
    
            return zoekpagina;
        }
    }
    

    the usercomposer

     [RuntimeLevel(MinLevel = RuntimeLevel.Run)]
    public class RegisterSiteServiceComposer : IUserComposer
    {
        public void Compose(Composition composition)
        {
            composition.Register<ISiteService, SiteService>(Lifetime.Singleton);
        }
    }
    

    (but if you just want to access to content service ...) you don't need all those steps

  • Craig100 1136 posts 2523 karma points c-trib
    Jun 30, 2020 @ 19:40
    Craig100
    0

    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

    Jun 30, 2020 @ 19:46

    Sweet, but good to know this one:

    var contentService = Umbraco.Web.Composing.Current.Factory.GetInstance

    Cheers, Tim

Please Sign in or register to post replies

Write your reply to:

Draft