Copied to clipboard

Flag this post as spam?

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


  • ewuski 88 posts 234 karma points
    Sep 26, 2023 @ 14:12
    ewuski
    0

    How to plug our custom recipients into IRecipientListProvider?

    I am trying Newsletter Studio and went through the documentation: https://www.newsletterstudio.org/documentation/package/10.0.0/ to understand how it works.

    All pretty straightforward until we need to extend it.

    Trying to use custom RecipientListProvider as per the example here: https://www.newsletterstudio.org/documentation/package/10.0.0/develop/recipient-list-providers/ but it raises multiple questions:

    1. How do I inject our service to get exiting recipient?

    When I try this

    public class NewsletterSubscriberProvider : IRecipientListProvider
    {
        public string DisplayName => "Demo Provider";
        public string DisplayNameLocalizationKey => "site/demoProvider";
        public string Prefix => "demo";
        public bool CanRedirectToEdit => false;
    
        public NewsletterSubscriberProvider(ISubscriberService subscriptionService)
        {
    
        }
    }
    

    I am getting an error on startup about injecting into singleton.

    2. Why there are two places where to assign recipient model (and email):

    public List<EmailReceiver> GetReceiversForList(RecipientListIdentifier listId, GetReceiversForListParams parameters)
    {
        var list = new List<EmailReceiver>();
        list.Add(new EmailReceiver(new EmailReceiverIdentifier(Prefix, 1), "[email protected]", "Foo Bar")); // <- here
    
        if (listId.Identifier == "2")
            list.Add(new EmailReceiver(new EmailReceiverIdentifier(Prefix, 2), "[email protected]", "Foo Bar"));
    
        return list;
    }
    
    public RecipientProviderRecipientDataModel GetDataModel(EmailReceiverIdentifier receiverId)
    {
        var model = new RecipientProviderRecipientDataModel();
        model.Email = "[email protected]"; // <- and here
        model.ProviderRecipientId = receiverId.ToString();
    
        model.Model = new InMemoryRecipient(1975, "London", "Test Company Inc.");
    
        return model;
    }
    

    3. Is there any way of seeing the custom recipients in the backoffice under recipients/ mailing lists?

    Before they are actually used in the campaign?

    I cannot see any recipient there even though the NewsletterSubscriberProvider seems implemented. GetLists and GetReceiversForList are hit only upon sending an email.

    4. Do we need to implement our Email Service Provider? as per here: https://www.newsletterstudio.org/documentation/package/10.0.0/develop/email-service-providers/

    or it is automatically picked up from our appsettings by the default email sender configured in Settings section for the workspace?

    Thanks

  • Markus Johansson 1911 posts 5757 karma points MVP c-trib
    Sep 26, 2023 @ 16:12
    Markus Johansson
    0

    Hi!

    Excided to see that you're looking to create your own Recipient List Provider and sorry that the documentation left you with questions. I'll make sure to update it based on our conversation here.

    Let me take your questions one by one.

    1. How do I inject our service to get exiting recipients?

    This is probably because you have registered your ISubscriberService as either Scoped or Transient, we create a Singleton for the Provider - is there anyway you can make your service a Singleton as well?

    2. Why there are two places where to assign recipient model (and email)

    This is a little unclear and I'll make sure to update the documentation around this.

    • GetReceiversForList Is used when the send-out engine creates the queue for the campaign based on the selected lists. This queue only contains a limited amount of information about the recipients to be able to exclude duplicates etc.

    • GetDataModel Is called during send out by the rendering engine for each recipient passing the EmailReceiverIdentifier that you created in GetReceiversForList. This method should return more details (if any) about the recipient. The model returned from this method is used e.g for Merge Fields and is also passed to any macros inside the email. If you want to create merge fields based on this model you need to implement a Merge Field Provider as well.

    Notice the type-check in the example here:

    https://www.newsletterstudio.org/documentation/package/10.0.0/develop/merge-field-providers/

    if(request.RecipientProviderRecipientDataModel.Model is InMemoryRecipient recipient)
    

    If you don't need extra information for merge-fields etc. you don't have to return a detailed model here.

    3. Is there any way of seeing the custom recipients in the backoffice under recipients/ mailing lists?

    No. we only show the mailing lists that are built into the package. If you need to display the recipients from your custom source you would need to create some kind of custom view for this. Most of the time there is already some other way to display this information (e.g. Umbraco Members, webshop, CRM etc).

    4. Do we need to implement our Email Service Provider?

    There is a built-in Email Service Provider to send via SMTP that you can configure in the Workspace-administration. If you want to send via some kind of API or similar you could create your own provider but this is not mandatory.

    Example of custom provider: https://github.com/enkelmedia/NewsletterStudio.Plugins.Mailjet

    We do not fall back to any settings in appSettings at the moment so if you want to send via SMTP you need to configure this in the Workspace administration as well.

    Another thing to notice is that you need to activate any custom Recipient List Provider or Merge Field Provider for your workspace in the administration section as well.

    enter image description here

    I hope that my answers bring some more clarity please feel free to reach out at any time!

  • ewuski 88 posts 234 karma points
    Oct 02, 2023 @ 15:29
    ewuski
    0

    Thanks very much for quick reply!

    We'll see if we can make DI working from our side and will test the recipients provider.

  • ewuski 88 posts 234 karma points
    Oct 13, 2023 @ 19:14
    ewuski
    0

    Ok, we've solved 1 & noted 3.

    In regards of point 2:

    1.Model issue

    I am not sure how we can populate our Model merge fields in GetDataModel(EmailReceiverIdentifier receiverId)?

    I understand that we need to do our own query to get full subscribers data from our own source in order to populate our custom Model in GetDataModel, is that correct?

    The problem is that this function:

    public RecipientProviderRecipientDataModel GetDataModel(EmailReceiverIdentifier receiverId)
    {
        var model = new RecipientProviderRecipientDataModel();
        model.Email = "[email protected]";
        model.ProviderRecipientId = receiverId.ToString();
    
        // The "Model"-property is used to pass a custom model that can be used inside 
        // a IMergeFieldProvider to translate properties into Merge Fields.
        model.Model = new ClientRecipient("PromoCode", "Firstname", "Surname", "CountryName", DateTime.Now, DateTime.Now);
    
        return model;
    }
    

    only receives EmailReceiverIdentifier receiverId as parameter (=provider prefix + some id - id of what? - we always get 1here) so we:

    a. need to get the email of the recipient from the receivers list - how? if we only have some unidentified id and the provider prefix?

    b. only then we can potentially query our own data to get the particular properties for our custom model by the recipient's email address.

    Or is there any other way of matching the EmailReceiverIdentifier with the recipient's id from the NS send list?

    Btw we could get emails from

    List<EmailReceiver> GetReceiversForList(RecipientListIdentifier listId, GetReceiversForListParams parameters)
    

    but it reads listId which we don't have in

    RecipientProviderRecipientDataModel GetDataModel(EmailReceiverIdentifier receiverId)
    

    2. Email issue

    Why do we need to populate the receiver's email again in here:

    RecipientProviderRecipientDataModel GetDataModel(EmailReceiverIdentifier receiverId)
    

    in this line:

    model.Email = "[email protected]";
    

    as the email that is send to is from the receivers' list, and this one seems to be ignored?

    3. What is this function for?

    EmailReceiver GetByEmail(string email, Guid? workspaceKey = null)
    

    or can we use it to populate our model, in this case - where do we get Guid? workspaceKey from? And see question 1.

  • Markus Johansson 1911 posts 5757 karma points MVP c-trib
    Oct 18, 2023 @ 15:29
    Markus Johansson
    0

    Hi!

    2.1

    Yes, you need to get the full details (that you care about in the context of the package) in this method.

    If you always get 1 as the parameter you need to check the code that creates the EmailReceiverIdentifier (should be the GetReceiversForList-method), this method needs to create a unique identifier for the recipient that you can use in GetDataModel.

    2.2

    Because they are different concepts and in terms of code they are separated.

    2.3

    Its use to find a recipient by it's email, we use this for some features related to transactional emails to identify if a recipient exists or not. It should be safe to just return null from this method if you can't query your data source by email.

    A side note:

    The merge fields are extracted from the model using a "Merge Field Provider", have a look at this documentation: https://www.newsletterstudio.org/documentation/package/12.0.0/develop/merge-field-providers/.

    Basically, the merge field provider would get a reference to the model that you create and extract the values from there. The Merge Field Provider is also responsible to expose a list of properties that it uses so that the e-mail editor (and other parts of the UI) can show properties and options for the user to pick from.

  • ewuski 88 posts 234 karma points
    Oct 20, 2023 @ 23:07
    ewuski
    0

    Just a side comment on the issue 1:

    It is quite limiting to use singletons as for example is not possible to use UmbracoHelper which is a trouble for use when we get to getting data for our subsccribers.

    Also on the Umbraco documentation it is not recommended neither:

    Generally speaking, if you are writing software these days you should be using Dependency Injection principles. If you do this, you probably aren't using Singleton or Statics (and for the most part you shouldn't be!). Since Umbraco 9 comes with dependency injection out of the box, there really isn't any reason to use singletons or statics. It makes your code very difficult to test but more importantly using Singletons and Statics in your code makes it very hard to manage, APIs become leaky, and ultimately you'll end up with more problems than when you started.

    https://docs.umbraco.com/umbraco-cms/v/10.latest-lts/reference/common-pitfalls#static-references-to-scoped-instances-such-as-umbracohelper

    Maybe you could consider to replace singletons with scoped services in the future releases?

  • Markus Johansson 1911 posts 5757 karma points MVP c-trib
    Oct 23, 2023 @ 09:09
    Markus Johansson
    0

    Hi!

    Let look at your problem here and I'm sure that we can find a solution.

    While probably not the "best approach", there are plenty of ways to use a scoped or transient service even inside a singleton-lifetime dependency if needed - a common approach is to use the IServiceProvider as a dependency to the Singleton. One example from the Umbraco core can be found here:

    https://github.com/umbraco/Umbraco-CMS/blob/contrib/src/Umbraco.Core/IO/MediaFileManager.cs

    Note that the ctor takes in an IServiceProvider that can be used to resolve Transient, Scoped, and Singleton dependencies.

    The documentation that you're referring to has been around since Umbraco 6/7 and has only been slightly tweaked for the modern .NET-versions so I would argue that it's misleading and that the conclusions that you draw based on it are not accurate. The main takeaway should be that you should not implement your OWN singleton patterns (rather use the DI-containers singleton lifetime) and you should be careful with how use use static properties since they are shared by all threads.

    There is nothing wrong at all with using singletons with DI. Umbraco uses this heavily in the core for almost all services and repositories:

    https://github.com/umbraco/Umbraco-CMS/blob/223598b037408f4204f1175a092296a73186a262/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs#L279

    The reason behind this is of course that a singleton lifetime is better for performance since the instance is reused which will not put the same pressure on the GC (Garbage Collector). So while a solution with mostly transient dependencies might be "easier" to work with it would not be ideal from a performance standpoint.

    If you need to use the UmbracoHelper I'm assuming that you want to query the published content cache or media. The UmbracoHelper is just a "convenience"-class that is using other services under the hood. The UmbracoHelper is a scoped dependency and in Umbraco this means that is tied to a given HTTP-request. It contains methods that for example depend on the current locale of the http-request (for dictionaries).

    Since Newsletter Studio runs the send-out process in a background job the UmbracoHelper should not be used. The better approach is to use the IUmbracoContextFactory to get a reference to the UmbracoContext. I would argue that this is the best practice for any service that you use both on the front end or in background jobs as this approach will work with both.

    https://docs.umbraco.com/umbraco-cms/v/10.latest-lts/implementation/services#accessing-published-content-outside-of-a-http-request

    So in your services, event-handler or whatever, you should avoid depending on the UmbracoHelperor IUmbracoHelperAccesor. Your class should take in the IUmbracoContextFactory as a dependency and when you need to access the cache you would do something like this:

    using (UmbracoContextReference umbracoContextReference = _umbracoContextFactory.EnsureUmbracoContext())
    {
        IPublishedContentCache contentCache = umbracoContextReference.UmbracoContext.Content;
    }
    

    Please give this a try and let me know if you still have any issues - I'm more than happy to help =D

    // m

Please Sign in or register to post replies

Write your reply to:

Draft