Dynamics CRM Create Plugin Helper: Duplicate Xrm.Utility.getResourceString for Plugin Side

Have you ever use Xrm.Utility.getResourceString?  Xrm.Utility.getResourceString is a function that we can use in the front-end to returns the localized string for a given key associated with the specified web resource. For instance, we can use it for storing localize error messages (for validation purposes). But the problem is we only can use it in the front-end, that is why we will duplicate this feature in the back-end so we can use it also.

If you want to learn how to create this feature. First, you need to know what is the behavior of the original function. Here are the resx web resources in DB (I used my on-premise CRM):

resx web resources

So based on this screenshot, the logic supposed to be try to take resx with format {name}.{languageId}.resx > {name}.1033.resx (English) > {name}.resx. Then if you see the second column, you will see that the content has been encoded. After I try to decode using Base64String, I can get the original file. That is why we will need to have this feature also in our code.

Preparation

First, we need to create a .NET Solution with two projects:

  1. Libs (classlib net472)
    1. Install-Package Niam.Xrm.Framework (via NuGet)
    2. Download and put this Entities.cs in the Lib folder.
    3. Add a reference to System.Windows.Forms.dll.
  2. Libs.Tests (xunit net472)
    1. Add reference for project Libs
    2. Install-Package Niam.Xrm.Framework (via NuGet)
    3. Install-Package FakeXrmEasy (via NuGet)
    4. Install-Package NSubstitute(via NuGet)
    5. The sample.resx file. Put in the Libs.Tests folder > right-click > set Copy to Output Directory: Copy if newer.

Libs.Tests

Sure we will follow the Test-Driven Development method. We will start everything with the tests. So these are the tests that we will prepare for the implementation (Libs.Tests/ServiceProviderExtensionsTest.cs):

using FakeXrmEasy;
using Microsoft.Xrm.Sdk;
using Niam.XRM.Framework;
using NSubstitute;
using System;
using System.IO;
using System.Linq;
using Xunit;

namespace Libs.Tests
{
    public class ServiceProviderExtensionsTest
    {
        [Fact]
        public void ServiceProviderExtensions_GetResourceString_NoWebResource()
        {
            var user = new Entities.SystemUser { Id = Guid.NewGuid() };

            var text = File.ReadAllText(Directory.GetCurrentDirectory() + "//sample.resx");
            var textBytes = System.Text.Encoding.UTF8.GetBytes(text);
            var base64String = Convert.ToBase64String(textBytes);

            var userSetting = new Entities.UserSettings { Id = Guid.NewGuid() }.
                Set(e => e.SystemUserId, user.Id).
                Set(e => e.UILanguageId, SharedKey.LanguageEnglishCode);

            var testContext = new XrmFakedContext();
            testContext.Initialize(new Entity[] { userSetting }.ToList());
            testContext.ProxyTypesAssembly = null;

            var pluginExecutionContext = Substitute.For<IPluginExecutionContext>();
            pluginExecutionContext.InitiatingUserId.Returns(user.Id);

            var serviceFactory = Substitute.For<IOrganizationServiceFactory>();
            serviceFactory.CreateOrganizationService(Arg.Any<Guid?>()).Returns(testContext.GetOrganizationService());

            var serviceProvider = Substitute.For<IServiceProvider>();
            serviceProvider.GetService(Arg.Is(typeof(IPluginExecutionContext))).Returns(pluginExecutionContext);
            serviceProvider.GetService(Arg.Is(typeof(IOrganizationServiceFactory))).Returns(serviceFactory);

            var error = Assert.Throws<InvalidPluginExecutionException>(() => 
                serviceProvider.GetResourceString("new_/strings/MyAppResources", "SYS001"));
            Assert.Equal("WebResource is empty.", error.Message);
        }

        [Fact]
        public void ServiceProviderExtensions_GetResourceString_DefaultEnglish()
        {
            var user = new Entities.SystemUser { Id = Guid.NewGuid() };

            var text = File.ReadAllText(Directory.GetCurrentDirectory() + "//sample.resx");
            var textBytes = System.Text.Encoding.UTF8.GetBytes(text);
            var base64String = Convert.ToBase64String(textBytes);

            var webResource = new Entities.WebResource { Id = Guid.NewGuid() }.
                Set(e => e.Name, "new_/strings/MyAppResources.1033.resx").
                Set(e => e.Content, base64String);

            var testContext = new XrmFakedContext();
            testContext.Initialize(new Entity[] { webResource }.ToList());
            testContext.ProxyTypesAssembly = null;

            var pluginExecutionContext = Substitute.For<IPluginExecutionContext>();
            pluginExecutionContext.InitiatingUserId.Returns(user.Id);

            var serviceFactory = Substitute.For<IOrganizationServiceFactory>();
            serviceFactory.CreateOrganizationService(Arg.Any<Guid?>()).Returns(testContext.GetOrganizationService());

            var serviceProvider = Substitute.For<IServiceProvider>();
            serviceProvider.GetService(Arg.Is(typeof(IPluginExecutionContext))).Returns(pluginExecutionContext);
            serviceProvider.GetService(Arg.Is(typeof(IOrganizationServiceFactory))).Returns(serviceFactory);

            Assert.Equal("This is SYS001 ERROR.", 
                serviceProvider.GetResourceString("new_/strings/MyAppResources", "SYS001"));
        }

        [Theory]
        [InlineData(1025)]
        [InlineData(1069)]
        [InlineData(1026)]
        public void ServiceProviderExtensions_GetResourceString_LanguageSame(int languageCode)
        {
            var user = new Entities.SystemUser { Id = Guid.NewGuid() };

            var text = File.ReadAllText(Directory.GetCurrentDirectory() + "//sample.resx");
            var textBytes = System.Text.Encoding.UTF8.GetBytes(text);
            var base64String = Convert.ToBase64String(textBytes);

            var webResource = new Entities.WebResource { Id = Guid.NewGuid() }.
                Set(e => e.Name, $"new_/strings/MyAppResources.{languageCode}.resx").
                Set(e => e.Content, base64String);

            var userSetting = new Entities.UserSettings { Id = Guid.NewGuid() }.
                Set(e => e.SystemUserId, user.Id).
                Set(e => e.UILanguageId, languageCode);

            var testContext = new XrmFakedContext();
            testContext.Initialize(new Entity[] { webResource, userSetting }.ToList());
            testContext.ProxyTypesAssembly = null;

            var pluginExecutionContext = Substitute.For<IPluginExecutionContext>();
            pluginExecutionContext.InitiatingUserId.Returns(user.Id);

            var serviceFactory = Substitute.For<IOrganizationServiceFactory>();
            serviceFactory.CreateOrganizationService(Arg.Any<Guid?>()).Returns(testContext.GetOrganizationService());

            var serviceProvider = Substitute.For<IServiceProvider>();
            serviceProvider.GetService(Arg.Is(typeof(IPluginExecutionContext))).Returns(pluginExecutionContext);
            serviceProvider.GetService(Arg.Is(typeof(IOrganizationServiceFactory))).Returns(serviceFactory);

            Assert.Equal("This is SYS001 ERROR.", serviceProvider.GetResourceString("new_/strings/MyAppResources", "SYS001"));
        }
    }
}

So we will make an extension in the IServiceProvider interface and name the method as GetResourceString with two parameters (following Xrm.Utility.getResourceString parameters: webResourceName and key). 

Lib

We will create Libs/SharedKey.cs  file and here is the code:

namespace Libs
{
    public static class SharedKey
    {
        public const int LanguageEnglishCode = 1033;
    }
}

Next is Libs/OrganizationServiceExtensions.cs:

using Microsoft.Xrm.Sdk;
using Microsoft.Xrm.Sdk.Query;
using Niam.XRM.Framework.Data;
using Niam.XRM.Framework;
using System;
using System.Linq;

namespace Libs
{
    public static class OrganizationServiceExtensions
    {
        public static Entities.WebResource GetWebResourceByName(this IOrganizationService service, string name)
        {
            var query = new QueryExpression(Entities.WebResource.EntityLogicalName)
            {
                ColumnSet = new ColumnSet<Entities.WebResource>(e => e.Content),
                NoLock = true,
                TopCount = 1
            };
            query.Criteria.AddCondition<Entities.WebResource>(e => e.Name, ConditionOperator.Equal, name);
            var result = service.RetrieveMultiple(query);

            return result.Entities.Any() ? result.Entities[0].ToEntity<Entities.WebResource>() : null;
        }

        public static Entities.UserSettings GetUserSettings(this IOrganizationService service, Guid userId)
        {
            var query = new QueryExpression(Entities.UserSettings.EntityLogicalName)
            {
                ColumnSet = new ColumnSet<Entities.UserSettings>(e => e.UILanguageId),
                NoLock = true,
                TopCount = 1
            };
            query.Criteria.AddCondition<Entities.UserSettings>(e => e.SystemUserId, ConditionOperator.Equal, userId);
            var result = service.RetrieveMultiple(query);

            return result.Entities.Any() ? result.Entities[0].ToEntity<Entities.UserSettings>() : new Entities.UserSettings();
        }
    }
}

Then we will need Libs/StringExtensions.cs to Decode Base64String:

namespace Libs
{
    public static class StringExtensions
    {
        public static byte[] Base64Decode(this string base64EncodedData)
        {
            var base64EncodedBytes = System.Convert.FromBase64String(base64EncodedData);
            return base64EncodedBytes;
        }
    }
}

Here is the implementation of our main API (Libs/ServiceProviderExtensions.cs):

using Microsoft.Xrm.Sdk;
using Niam.XRM.Framework;
using System;
using System.IO;
using System.Resources;

namespace Libs
{
    public static class ServiceProviderExtensions
    {
        public static string GetResourceString(this IServiceProvider serviceProvider, string webResourceName, string key)
        {
            var serviceFactory = serviceProvider.GetService(typeof(IOrganizationServiceFactory)) as IOrganizationServiceFactory;
            var pluginExecutionContext = serviceProvider.GetService(typeof(IPluginExecutionContext)) as IPluginExecutionContext;

            var valid = serviceFactory != null && pluginExecutionContext != null;
            if (!valid) throw new InvalidPluginExecutionException("ServiceFactory/PluginExecutionContext is empty.");

            var adminService = serviceFactory.CreateOrganizationService(null);
            var userSetting = adminService.GetUserSettings(pluginExecutionContext.InitiatingUserId);

            var languageId = userSetting.Get(e => e.UILanguageId) ?? SharedKey.LanguageEnglishCode;

            var webResource = adminService.GetWebResourceByName($"{webResourceName}.{languageId}.resx") ??
                 adminService.GetWebResourceByName($"{webResourceName}.{SharedKey.LanguageEnglishCode}.resx") ??
                 adminService.GetWebResourceByName($"{webResourceName}.resx");

            if (webResource == null) throw new InvalidPluginExecutionException("WebResource is empty.");

            var content = webResource.Get(e => e.Content).Base64Decode();

            using (var resxGet = new ResXResourceSet(new MemoryStream(content)))
            {
                return resxGet.GetString(key);
            }
        }
    }
}

First, we will retrieve the preferred language-id (if empty, we will put English). The next step is to retrieve WebResource by the name that the user will supply. Here we will try 3 alternatives: based on preferred language id, if empty then use English language id, else go to default naming. The content that we retrieve is a Base64String, we need to decode it before we need to get the value based on ResXResourceSet (System.Windows.Forms.dll).

If you running the tests, here is the result:

xunit tests result

Demonstration

We will create another project for demonstration purposes. The idea of using a library is to make CRM developers aware that we indeed can reuse our code. Here are the steps:

  1. Create Demo.Plugins (classlib net472)
  2. Install-Package Niam.Xrm.Framework (via NuGet)
  3. Add a reference to the Lib project

Here is a simple demonstration of how to use the Lib (DemoPlugin.cs):

using System;
using Libs;
using Microsoft.Xrm.Sdk;

namespace Demo.Plugins
{
    public class DemoPlugin : IPlugin
    {
        public void Execute(IServiceProvider serviceProvider)
        {
            throw new InvalidPluginExecutionException(
                serviceProvider.GetResourceString("new_demoresx", "SYS001"));
        }
    }
}

Because we use third-party libs (Niam.Xrm.Framework and Libs), we need to merge this assembly using this method in this post.

This is the result after you register and add a new step for the entity you want:

You can get the full code in here.

2 thoughts on “Dynamics CRM Create Plugin Helper: Duplicate Xrm.Utility.getResourceString for Plugin Side

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.