Sunday, April 5, 2020

Convert Non-AC3 Audio to AC3 for movies

6/4/2020: 
1. Fixed a bug where it was only processing files where the channels were greater than 6, not when then codec name was not compatible
2. Fixed a bug where special characters in filenames were causing issues.

Plex issues on the XBOX One

The Plex on the XBOX One is a decent movie player, however there's been an issue for a while (years) where the XBOX won't play videos properly in the case when the video needs to be transcoded.

The XBOX can play most common video formats, but it can't play some of the newer audio formats natively (TRUEHD, DOLBY ATMOS, DTX:X) and it's can't play regular old DTS either.  In order to play these Plex transcodes and although it play fine on other devices, for some reason it causes massive buffering on the XBOX one when transcoding is required.

There doesn't seem to be a solution in sight, and the PLEX devs seemed to have given up, so I've written this script to to go through my existing video library and convert any audio streams to be natively compatible with the XBOX One.


Preparing the Script

The script is a Power Shell script and to use it it's quite simple.

1. Download FFMpeg:

https://ffmpeg.zeranoe.com/builds/

2. Unzip just the contents of the "bin" folder to a folder of your choice.  You can unzip the other files but they are not needed.

e.g. ffmpeg.exe and ffprobe.exe to a folder called "ConvertToAC3"

3. Create a new file in the same folder as the FFMpeg files and paste the script into it.  Make sure the extension of the file is ps1 (i.e. convert.ps1)

4. Alter the script at the top changing the following variables

$videos_path
This needs to be set to the path of your video library

$preview
When 0, this will convert the files when the script is run. When the value is 1 it will only show you what it will attempt to convert and not actually do a conversion.

5. Save the changes to the script

6. Before we run the script, make sure you have double the space available on your hard drive - as it will duplicate each file.


Allowing Powershell Scripts To Run

You may need to run the following command in a power script session before getting started as Power Shell Scripts are disabled by default.  You need run the session as an admin to allow this command to run to allow the script to execute.
set-executionpolicy remotesigned
NOTE: to run as admin, right click on the Windows Power Shell icon and click "Run As Administrator".


What Does the Script do? 

1. The script goes and find every MKV file under the path that you have set (recursively).
2. It them inspects each one (using FFProbe) to see if it needs to be converted
3. If it decects that he MKV needs to be converted, it will use FFMPEG to convert audio (only the main stream) and creates a new a video file with the untouched video and the new audio as AC3.
4. It will take a while depeding the on the number of videos you have.
5. Once it's finished file it will rename the original the *.mkvold, so that Plex will ignore it and you can delete it once you are happy that the audio has been converted corrected

Thanks To...

I'd like to thank Robin Clarke who's work with FFMPEG gave me a basis for my script.

Here's the Script





Tuesday, May 20, 2014

MS CRM Simulating OnChange Javascript event for Owner field

You might get a surprise when you add the On Change event to an owner field - you will find that it doesn't fire.  The reason for this is that the when changing the owner of a record the form is actually saved.

Lucky for you, there is a way to achieve the same functionality as the On Change, and it's quite straight forward.

Save Mode = Assign

Basically, you can add a On Save event (making sure you tick the pass the context box when you set it up), and then you can find out what triggered the On Save event inspecting the SaveMode and finding out if it was triggered by an "Assign".

OwnerId Is Dirty Check

The only other thing you'll need to do is check also if the OwnerId field is dirty.  Why is this needed?  Well, imagine that you've got a required field that you haven't filled in and then you go to change the owner. As changing the Owner causes an On Save event, this also triggers validation and you'll get an error message indicating the field is required before it gets to your logic, and the Save won't actually occur.  This leaves the form in state where the OwnerId has actually been changed but your "OnChange" logic won't fire on the next save because the logic is looking for an Save Mode of "Assign" - which won't be raised again until someone attempts to re-assign the record.  So, the next best thing is to do a check to see if the OwnerId is dirty (which will only happen if the Save attempt failed validation when the owner is changed) and your on change logic can fire then.  Not perfect, but good enough in most situations.

So here's an example of some code that will do the job:

function Form_OnSave(context) {

    var saveMode_Assign = 47;

    if (context.getEventArgs().getSaveMode() == saveMode_Assign 
        || Obs.getAttribute("ownerid").getIsDirty()) {
        OwnerId_OnChange(); // Put your logic here!
    }    
}




Saturday, March 22, 2014

S#arp Architecure, Multiple Database and Class Maps

Recently, I've been updated a ASP.NET MVC project which is based on the S#arp Architecture.  I needed to extend the existing function to talk to a second database.

In order to do this, I followed the S#arp Architecture guide to multiple databases which can be found here. This shows you how to add a second NHibernate configuration to your project and how to apply an attribute to your repository so it knows that which NHibernate configuration to use. This seemed to be perfect as out of the box, NHibernate didn't seem to have the facility to support multiple database sessions - well at least not easily.

So following the guide, I integrated the changes into the existing software - unfortunately this didn't work.

The issue was that I had the POCO classes and NHibernate Class Maps for both databases in the one assembly NHibernate would attempt to apply the class maps and POCO classes to only one database Session - causing NHibernate to throw "No Persister Found" errors and similar errors. As I didn't like the idea of splitting the project up into two assemblies for the sake of it (one for each database), I went looking for alternatives.

To fix the POCO issue was easy,  I just created an empty interface representing each database and applied that interface to each of the POCO objects accordingly, then in AutoPersistenceModelGenerator I simply had two different versions of the Generate method for each database which I used when settings up my Sessions in the calls to NHibernateSession.Init() and NHibernateSession.AddConfiguration().

The Class maps were not so easy, after doing a lot of research and finding that ClassMaps can't be filtered out in the Same way as POCO objects, I was very much stuck until I found this blog post.  Although this blog post had nothing to do with multiple databases, it did point out that in the S#arp Architecture you should not use ClassMaps, but instead use Overrides that are provided by the S#arp Architecture.

In the S#arp Architecture when using AutoMapping, Overrides represent those classes which shouldn't be AutoMapped on conventions and instead have custom mappings that don't match the convention.  The Overrides are basically the same ClassMaps and also use the same syntax.

To my surprise after converting the ClassMaps to Overrides (which only took a few minutes), it seems that S#arp Architecture could then work out which Override (i.e. Map) applied to which database and applied the where they were appropriate - no more errors.  And because there were no long any Class Maps, NHibernate wasn't trying to automatically assume that all ClassMaps belonged to the current Session that was being initialized.

Anyhow, thought I would document this in case someone else is facing the same situation.

:)




Saturday, February 2, 2013

Fiddler, CRM and Javascript Time Saver

UPDATE: Microsoft has changed the way they send the JavaScript to the browser (See here for more info) so this tip will only work up until Rollup 11.

To get some real productivity gains when working with Javascript and CRM you can introduce Fiddler into the mix.  Fiddler has a feature called 'Auto Responder' which allows you intercept a file requested by a browser from a web server and replace it with a different file of your choice. 

By using this feature, it means that you don't have to go through the somewhat annoying process of modifying a Javascript file, deploying it and publishing it before you can test the changes; instead you can simply modify a Javascript file directly on your hard drive and then simply refresh the CRM form to test your changes.  You only need to deploy and publish the change once you are happy with the modifications and want to commit them to CRM.

Here's how to set it up:

1. Install Fiddler on your local machine
- you can download it from here for free:

2. Choose an already published web resource file from CRM (e.g. new_js_accounts)



3. Open up fiddler and click on the "Auto Responder" Tab

4. Tick the "Enable Automatic Responses", then tick "Unmatches requests passthrough" (very important!)

5. Click the add button and add a new rule, setting the first Rule box to the name of your web resource name (as highlighted on the URL in the first image above), and use the second box to "Find a file...", choosing the file which you want to replace the web resource with from your local machine.  It should look something like this:





Once this is done, you should find that whenever you refresh your CRM Form now on it should point to the local copy of the file and ignore the server one.  In Fiddler you can also turn this on/off by using the tick box next to the rule at any time.

Don't forget to still upload and publish to CRM after you are done!














Saturday, September 15, 2012

Crm 2011 Developer Toolkit - Findings

I've started using the CRM 2011 Developer Toolkit from the SDK, and I have to say I'm pretty happy with it in general - although it does have some small issues.

The Developer Toolkit takes care of a lot of common development tasks all within Visual Studio, including:

- Code Generation of Entities (similar to CrmSvcUtil.exe)
- Code Generation of Plugin templates
- Entity Registration with CRM
- Web Resource Management
- Deployment of Web Resources, Plugins, Workflows etc..

Here's some of the small issues I've found with the tool - none that I've found to be show stoppers.


Error registering plugins and/or workflows. The resource string "ErrorSerializingRegFile" for the "RegisterPlugin" task cannot be found. Confirm that the resource name "ErrorSerializingRegFile" is correctly spelled, and the resource exists in the task's assembly. C:\Program Files (x86)\MSBuild\Microsoft\CRM\Microsoft.CrmDeveloperTools.CrmClient.targets 176 4 CrmPackage

For whatever reason, the "RegisterFile.crmregister" file in the CrmPackage Project needs to be editable, so if you have it source controlled with TFS or it's read only for some reason - you'll get this message.  Just simply check the file out or make the file writable.


The parameter is incorrect. (Exception from HRESULT: 0x80070057 (E_INVALIDARG))


When generating the entities, you might get the above message.  This is because of a bug which doesn't get on very well with Solution Folders in your solution.  To work-around, simply remove the solution folders from you solution.


The Context Class is missing when you generate the entities


For some reason, the context class is not generated when you generate the LINQ entities as it is in CrmSvcUtil.  Luckily, this is not required to use the LINQ functionality - but you'll need to make a few small changes
- Instead of creating a new context, you'll need to create a OrganizationServiceContext instead
- When you query against the OrganizationServiceContext, you'll need to use the CreateQuery<Entity>() syntax instead of the CrmContext.EntitySet syntax in your LINQ Queries

TIP: I'm just working on Javascript, and every time I deploy it deploys the plugins as well, how do I just deploy the Web Resources?


If you want to deploy the web resources but avoid deploying any workflow and plugins (because of the amount of time to register these things), simply change the CrmPackage "RegisterFile.crmregister" file's Build Action from "Register Plugin" to "None".  The Build Action option can be found in the Visual Studio properties for the file.  This will stop workflows and plugins from registering and save a lot of time when your just working on web resources.   If you want to enable registering of plugins & workflows again - simply do the reverse.




Monday, June 11, 2012

LINQ to CRM 2011 - Attribute Level Change Tracking Support


I really like LINQ to CRM 2011, it makes development against CRM easier.  However, out of the box LINQ to CRM has one big issue - it only supports entity level change tracking and not attribute level tracking.

This means that whenever you retrieve something via LINQ, mark the entity as "changed" and submit the changes, whether or not you have changed any attributes - it will update ALL of the attributes for that entity in the database - because it does not distinguish which attributes have changed and which haven't before it makes the update.

There are bad some side effects of this:
- The new Auditing feature in CRM 2011 will record all the fields as changed, not just the ones you actually changed.  This not only takes up database space, but makes it harder to read.
- Plugins and Workflows will trigger unnecessarily if you have them filtered on particular fields which you did not actually change.
- You may inadvertently overwrite changes that have occurred since you retrieved the record that you did not mean to.
- If you have field level security on some of those attributes, they may cause an exception even though you never changed it's value.

Luckily, LINQ to CRM allows you to intercept some useful events in it's life-cycle (Attach, SaveChanges, Execute) through the use of a sub-class or partial class, replacing it with your own functionality. Because of this, I've managed to write a class to overcome these issues and commit only the actual attribute changes to CRM rather than ALL of attributes.

So How does this class work?

- Intercepts when an entity is "Attached" or "Detached" from the context.  If the entity has a status of "unchanged" a shallow clone is taken of the entity and stored in an "originals" list; the clone is removed from the "originals" list if it is later detached.  Note: Entities will have a status of "unchanged" if they have been retrieved via LINQ from CRM or manually attached using the Attach() method.

- Intercepts the call to "SaveChanges()" method, inspects each entity that has been flagged as "changed" and compares it against the original version of the entity.  If a change in attributes is detected then a new entity is created containing only the changed attributes and the entity's key; this new entity is put into a new "Deltas" collection.  In addition, any entities that have not changed at all but are marked as "changed" are reset back to "unchanged", so they are not included in the commits that are made against CRM.

- Intercepts when the Execute Method is called to commit the data.  When the data is committed to the database, it uses the "UpdateRequest" class as you normally would using non-LINQ to CRM Entities.  When this is done for entity in the list of entities marked as "changed", the Target property of the UpdateRequest object used is reset to use the delta version of the entity rather than the complete entity itself.  This therefore avoids committing all attributes to the database, and instead only sends those fields that have changed.

Can I use it in a Plugin?

Yes. You should even be able to use it in the online version of CRM.

How do I use it?

- Place it in the same project as your LINQ to CRM context.
- Change the namespace so that it macthes the same namespace as your LINQ to CRM Context.
- Change the class name so that it macthes the same class name as your LINQ to CRM Context.
- If you are doing read-only operations, to remove the change tracking overhead you can disable the functionality with the "EnableAttributeChangeTracking" flag - set it to false as soon as you new up your context.

Any Gotya's?

- Attach(): If you are going to new up an Entity for an update without retrieving the entity from CRM first, make sure you set the key and then Attach() the entity before you change the attributes on that entity.  Otherwise it will not track the changes.

MergeOption: If you change the merge option, it may not work.  AppendOnly (the default) is currently the only supported merge option.

- EntityCollection: Currently, the EntityCollection attribute type is not supported when identifying changes between the original and the current version of an entity.  EntityCollection attributes are always assumed to be "Changed" if included in an attribute, and will always be submitted for update.

Cheers

Matt
                                                                     
                                                                     
                                                                                                                
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using Microsoft.Xrm.Sdk;
using Microsoft.Xrm.Sdk.Messages;

namespace Example.Linq
{
    public partial class CrmContext
    {
        private List<Entity> _originalEntities = new List<Entity>();
        private List<Entity> _deltaEntities = new List<Entity>();

        private bool _enableAttributeChangeTracking = true;

        /// <summary>
        /// Enables or disables Attribute Level Change tracking.
        /// Default is On.
        /// </summary>
        public bool EnableAttributeChangeTracking
        {
            get
            {
                return _enableAttributeChangeTracking;
            }

            set
            {
                if (_originalEntities.Count > 0 && value == false)
                    throw new Exception("You cannot disable attribute change tracking at this time, entities are already being tracked");

                _enableAttributeChangeTracking = value;
            }
        }

        /// <summary>
        /// Overrides the base OnBeginEntityTracking method
        /// When an entity is tracked, this adds the original unmodified version of that entity
        /// to the unchangedEntities list (which contains the original version of every entity).
        /// </summary>
        /// <param name="entity"></param>
        protected override void OnBeginEntityTracking(Entity entity)
        {
            base.OnBeginEntityTracking(entity);

            if (_enableAttributeChangeTracking && entity.EntityState == EntityState.Unchanged)
            {
                var exists = _originalEntities.Where(x => x.Id == entity.Id).FirstOrDefault();
                if (exists == null) _originalEntities.Add(ShallowClone(entity));
            }
        }

        /// <summary>
        /// Overrides the base OnEndEntityTracking method
        /// For any entity that is detached from entity tracking, this also 
        /// removes it from the unchangedEntities list (which contains the original version of every entity)
        /// </summary>
        /// <param name="entity"></param>
        protected override void OnEndEntityTracking(Entity entity)
        {
            base.OnEndEntityTracking(entity);
            if (_enableAttributeChangeTracking)
            {
                var exists1 = _originalEntities.Where(x => x.Id == entity.Id).FirstOrDefault();
                if (exists1 != null) _originalEntities.Remove(exists1);
                var exists2 = _deltaEntities.Where(x => x.Id == entity.Id).FirstOrDefault();
                if (exists2 != null) _deltaEntities.Remove(exists2);
            }
        }

        /// <summary>
        /// Overrides the base OnExecuting Method
        /// For UpdateRequests, replaces the target entity on the update message
        /// with an entity that only contains the key and the changed attributes.
        /// </summary>
        /// <param name="request"></param>
        protected override void OnExecuting(OrganizationRequest request)
        {
            if (_enableAttributeChangeTracking && typeof(UpdateRequest) == request.GetType())
            {
                var updateRequest = (UpdateRequest)request;
                var target = updateRequest.Target;
                var newTarget = _deltaEntities.Where(x => x.Id == target.Id).FirstOrDefault();
                updateRequest.Target = newTarget;
            }
            base.OnExecuting(request);

        }

        /// <summary>
        /// Overrides the base OnSavingChangesMethod
        /// For each entity, determines if an update is required.
        /// If no update is required, it detaches and reattaches to set the entity back to a "unchanged" state.
        /// Also create the entity that will ACTUALLY be subitted to the database (used in the OnExecuting method).
        /// </summary>
        /// <param name="options"></param>
        protected override void OnSavingChanges(Microsoft.Xrm.Sdk.Client.SaveChangesOptions options)
        {

            // Clear the list of entities to be sumbitted
            _deltaEntities.Clear();

            // Mark any entities as unchanged that
            // are only sending the key
            var updated = this.GetAttachedEntities().Where(x => x.EntityState == EntityState.Changed).ToList();

            foreach (var target in updated)
            {
                // Ignore updates where nothing has been updated
                var unchanged = _originalEntities.Where(x => x.Id == target.Id).FirstOrDefault();
                if (unchanged != null)
                {
                    var cloneOfTarget = ShallowClone(target);
                    RemoveUnchangedFields(cloneOfTarget, unchanged);
                    _deltaEntities.Add(cloneOfTarget);

                    // Test to see if it's only the key left... if so ignore the update otherwise you will get a blank audit record
                    if (cloneOfTarget.Attributes.Count == 1 && cloneOfTarget.Attributes.First().Value != null)
                    {
                        if (cloneOfTarget.Attributes.First().Value is Guid || cloneOfTarget.Attributes.First().Value is Guid?)
                        {
                            if (cloneOfTarget.Id == (Guid?)cloneOfTarget.Attributes.First().Value)
                            {
                                this.Detach(target);
                                _deltaEntities.Remove(cloneOfTarget);
                                target.EntityState = EntityState.Unchanged;
                                this.Attach(target);

                            }
                        }
                    }
                }
            }

            base.OnSavingChanges(options);
        }

        protected override void OnSaveChanges(SaveChangesResultCollection results)
        {
            _deltaEntities.Clear();
            base.OnSaveChanges(results);
        }

        /// <summary>
        /// Overrides the base OnExecute Method
        /// For Update Requests, if an error was thrown ignores it if Target is null (i.e. no need to update)
        /// </summary>
        /// <param name="request"></param>
        /// <param name="exception"></param>
        protected override void OnExecute(OrganizationRequest request, Exception exception)
        {
            ///
            if (_enableAttributeChangeTracking && typeof(UpdateRequest) == request.GetType())
            {
                var updateRequest = (UpdateRequest)request;

                if (updateRequest.Target != null)
                {
                    base.OnExecute(request, exception);
                }
            }
            else
            {
                base.OnExecute(request, exception);
            }
        }


        /// <summary>
        /// This method loops through comparing the changed entity with the unchanged entity
        /// and removed the unchanged fields from the changed entity.  Keys are kept.
        /// </summary>
        /// <param name="changed"></param>
        /// <param name="unchanged"></param>
        public void RemoveUnchangedFields(Entity changed, Entity unchanged)
        {

            // Lookp through the changed fields, if there is one missing from the unchanged list, add it as null so at least it will be compared.
            foreach (var changedAttribute in changed.Attributes)
            {
                var key = changedAttribute.Key;

                if (!unchanged.Contains(key))
                    unchanged.Attributes.Add(new KeyValuePair<string, object>(key, null));
            }

            // Loop through each attribute and compare properties
            foreach (var unchangedAttribute in unchanged.Attributes)
            {
                var key = unchangedAttribute.Key;

                var originalAttributeVal = unchanged.Attributes[key];
                var changedAttributeVal = changed.Attributes[key];

                // Fix any original value issues
                if (originalAttributeVal != null)
                {
                    // Fix any zero length strings
                    if (originalAttributeVal.GetType() == typeof(string) && string.IsNullOrWhiteSpace((string)originalAttributeVal))
                    {
                        originalAttributeVal = null;
                    }
                    else
                    {
                        // Fix any dates that aren't of a specified kind
                        if (originalAttributeVal.GetType() == typeof(DateTime) || originalAttributeVal.GetType() == typeof(DateTime?))
                        {
                            var date = (DateTime?)originalAttributeVal;
                            if (date.Value.Kind == DateTimeKind.Unspecified)
                                originalAttributeVal = date.Value.ToUniversalTime();
                        }
                    }
                }

                // Fix any changed value issues
                if (changedAttributeVal != null)
                {
                    // Fix any zero length strings
                    if (changedAttributeVal.GetType() == typeof(string) && string.IsNullOrWhiteSpace((string)changedAttributeVal))
                    {
                        changedAttributeVal = null;
                    }
                    else
                    {
                        // Fix any dates that aren't of a specified kind
                        if (changedAttributeVal.GetType() == typeof(DateTime) || changedAttributeVal.GetType() == typeof(DateTime?))
                        {
                            var date = (DateTime?)changedAttributeVal;
                            if (date.Value.Kind == DateTimeKind.Unspecified)
                                changedAttributeVal = date.Value.ToUniversalTime();
                        }
                    }
                }

                // Compare the values
                if (originalAttributeVal == null && changedAttributeVal == null)
                {
                    changed.Attributes.Remove(key);
                }
                else if (originalAttributeVal != null && changedAttributeVal == null)
                {
                    // Do nothing, keep the attribute        
                }
                else if (changedAttributeVal is EntityCollection)
                {
                    // Do nothing, Leave it in... (complicated to compare).
                }
                else if (changedAttributeVal.Equals(originalAttributeVal))
                {
                    if (unchangedAttribute.Value.GetType() == typeof(Guid) || unchangedAttribute.Value.GetType() == typeof(Guid?))
                    {
                        // Avoid removing the key
                        if (!((Guid?)unchangedAttribute.Value == unchanged.Id))
                        {
                            changed.Attributes.Remove(key);
                        }
                    }
                    else
                    {
                        changed.Attributes.Remove(key);
                    }
                }
            }
        }

        /// <summary>
        /// Take a shallow copy of an entity
        /// </summary>
        /// <param name="entity"></param>
        /// <returns></returns>
        private static Entity ShallowClone(Entity entity)
        {
            var clone = new Entity(entity.LogicalName);
            clone.EntityState = entity.EntityState;
            clone.Id = entity.Id;

            foreach (var attribute in entity.Attributes)
            {
                if (attribute.Value == null)
                {
                    clone.Attributes.Add(attribute.Key, null);
                }
                else if (attribute.Value is EntityReference)
                {
                    EntityReference entityReference = attribute.Value as EntityReference;
                    clone.Attributes.Add(attribute.Key, new EntityReference(entityReference.LogicalName, entityReference.Id));
                }
                else if (attribute.Value is Money)
                {
                    Money money = attribute.Value as Money;
                    clone.Attributes.Add(attribute.Key, new Money(money.Value));
                }
                else if (attribute.Value is OptionSetValue)
                {
                    OptionSetValue option = attribute.Value as OptionSetValue;
                    clone.Attributes.Add(attribute.Key, new OptionSetValue(option.Value));
                }
                else if (attribute.Value is EntityCollection)
                {
                    // Don't copy EntityCollection values, just re-reference it.
                    EntityCollection entityCollection = attribute.Value as EntityCollection;
                    clone.Attributes.Add(attribute.Key, entityCollection);
                }
                else // value types
                {
                    clone.Attributes.Add(attribute.Key, attribute.Value);
                }
            }

            return clone;
        }

    }
}

15/09/2012 Update: If you are using the entities generated by the CRM 2011 Developer Toolkit rather than the CrmSvcUtil.exe, it doesn't generate a context class for some reason - so, in order to use the partial class above, you'll just need to create a simple class for the Context similar to the code below and use the CrmContext.CreateQuery<Entity>() syntax instead of the CrmContext.EntitySet when querying.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.Xrm.Sdk.Client;

namespace Example.Linq
{
    public partial class CrmContext : OrganizationServiceContext
    {        
        /// <summary>
        /// Constructor.
        /// </summary>
        public CrmContext(Microsoft.Xrm.Sdk.IOrganizationService service) : 
                base(service)
        {
        }
        
    }
}

Sunday, April 15, 2012

CRM 4 or CRM 2011 - Change the recipient email address

Both CRM 4 and CRM 2011 always chooses to use the primary email address (emailaddress1) when sending an email to an entity (such as Account or Contact), even though on receiving emails, CRM will map the email to the entity based on any of it's email addresses.

Every now and then I've come across a customer request where they would like to choose which of an entities email address's are used to send an email to the entity (e.g. contact).

So after a lot of experimentation, I've found a reliable way of achieving this. The key is to use a plugin on the pre-event step of the "Send" message (Send Email Message) of the email. I've found that changing the email address to be used before this is unreliable as CRM often overrides the change and re-maps it back to the primary email address.

So, to summarise my example code below:

1) I've added an "new_emailpreference" optionset to the contact entity, which lets you choose which email address is used (emailaddress1, emailaddress2 or emailaddress3).


2) I've attached the plugin as a pre-operation step of the email entity to the "Send" message.

3) The Plugin does the following:
- Grabs the email
- Loops through the "to" address (you could easily extend this to include cc and bcc as well), finding those activity parties which map to a contact.
- For each contact found, it retrieves the contact and determines if emailaddress2 or emailaddress3 has been chosen as the preferred email address to be used instead of the default.
- For that activity party, it then changes the "addressused" property to the chosen email address and updates the email record.

4) When the email gets sent via Outlook or via the Email Router, the email address that is used in the "To" address is set correctly to the email address that was chosen.






Here's the code....Note that it's for CRM 2011, however the code for CRM 4 is very similar and all the fields are the same..

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.Xrm.Sdk;
using Microsoft.Xrm.Sdk.Query;

namespace ExamplePlugin
{
public class Plugin : IPlugin
{
public void Execute(IServiceProvider serviceProvider)
{
IPluginExecutionContext context = (IPluginExecutionContext)serviceProvider.GetService(typeof(IPluginExecutionContext));
IOrganizationServiceFactory factory = (IOrganizationServiceFactory)serviceProvider.GetService(typeof(IOrganizationServiceFactory));
IOrganizationService service = factory.CreateOrganizationService(context.UserId); // null for SYSTEM user, otherwise User Guid

ITracingService trace = (ITracingService)serviceProvider.GetService(typeof(ITracingService));
Guid emailId = Guid.Empty;

if (context.PrimaryEntityName != "email")
{
return;
}

if (context.InputParameters.Contains("EmailId") && context.InputParameters["EmailId"] is Guid)
{
emailId = (Guid)context.InputParameters["EmailId"];

if (emailId == Guid.Empty)
{
return;
}

try
{
// Retrieve the email
var email = service.Retrieve("email", emailId, new ColumnSet(true));
if(email.Contains("to"))
{
EntityCollection activityParties = (EntityCollection)email["to"];

foreach (var activtyParty in activityParties.Entities)
{
if (activtyParty.Contains("partyid") && ((EntityReference)activtyParty["partyid"]).LogicalName == "contact")
{
// Retrieve the contact
var contact = service.Retrieve("contact",
((EntityReference)activtyParty["partyid"]).Id,
new ColumnSet(
new string[]
{ "emailaddress1",
"emailaddress2",
"emailaddress3",
"new_emailpreference" }));

if (contact.Contains("new_emailpreference") && contact["new_emailpreference"] != null)
{
if (((OptionSetValue)contact["new_emailpreference"]).Value == 100000002)
{
if (contact.Contains("emailaddress2") && ((string)contact["emailaddress2"]) != null)
{
activtyParty["addressused"] = (string)contact["emailaddress2"];
}
}
else if (((OptionSetValue)contact["new_emailpreference"]).Value == 100000003)
{
if (contact.Contains("emailaddress3") && ((string)contact["emailaddress3"]) != null)
{
activtyParty["addressused"] = (string)contact["emailaddress3"];
}
}
}
}
}

service.Update(email);
}
}
catch (Exception ex)
{
trace.Trace("Plugin Exception Encountered");
throw new InvalidPluginExecutionException(String.Format("An error occured Excuting the Plugin {0}{1}{2}", ex.Message, Environment.NewLine, ex.StackTrace));
}
finally
{
trace.Trace("Plugin Finished Executing");
}
}
else
{
return;
}


}
}
}