Hi Darryl, I've been using your Local to Global OptionSet Converter for the last couple of days - firstly many thanks for making an immensely useful tool! I don't know what I'd have done if this wasn't available, probably ended up copying the option set to the other entity where I needed it and put up with the double entry...
Anyway, I have some feedback for you as the result of some pretty intensive testing! The first problem I encountered were data validation issues for old data coming from my plugins. Those caused the data migration to error out on the first run. That freaked me out somewhat since all the Views and Forms had been updated by that stage, and I'm working on the live system since we're on CRM Online with no Sandbox instance. So, I quickly implemented a 'Revert unsuccessful migration' option to undo a partially done migration (http://screencast.com/t/QaIxlI31).
Later I noticed that you had implemented in the data migration a "catch-up" option to speed up partially done migrations. Once I realised that you'd taken that into account, I quickly switched the order of the steps so that data migration is done before updating views and forms. That way, the data migration can take as long as it takes, and can be repeated endlessly, with no effect on the end user - the views and forms only get switched once a full migration has successfully completed.
I made the same change for the migrate to global section.
In your UpdateViews method, I found it necessary to trap for attempted changes to non-Public views, as Dynamics gave me an error when trying to change those programatically - they must be updated manually.
I made some additions to your AssertCanDelete method, to present to the user the name of the dependent component - much more helpful than just giving the Id.
Finally, AssertCanDelete was always returning errors because of SavedQueryVizualisation dependencies - ie. Charts! So I implemented an UpdateCharts method too.
I think that's it. I used your code from Codeplex, as I did download from Github but there were errors on trying to run XrmToolbox related to your components. The Codeplex version worked fine. I'm going to paste my updated code below as I'm sorry I didn't use your Github version, and nor am I particularly au fait with doing pull requests etc in Git, but you can easily do a file compare on your version and mine and see where the differences are.
public bool MigrateData { get; set; }
public bool ConvertValuesToGlobal { get; set; }
public bool SupportsExecuteMultipleRequest { get; set; }
public string TempPostfix { get; private set; }
public IOrganizationService Service { get; private set; }
public HashSet<int> ValidLanguageCodes { get; private set; }
private const int CRM2013 = 6;
private const int CRM2011 = 5;
private const int Rollup12 = 3218;
[Flags]
public enum Steps
{
CreateTemp = 1,
MigrateToTemp = 2,
RemoveLocal = 4,
CreateGlobal = 8,
MigrateToGlobal = 16,
RemoveTemp = 32,
RevertUnsuccessful = 64,
}
public Logic(IOrganizationService service, McTools.Xrm.Connection.ConnectionDetail connectionDetail, string tempPostFix, bool migrateData, bool convertValuesToGlobal)
{
SupportsExecuteMultipleRequest = connectionDetail.OrganizationMajorVersion >= CRM2013 ||
(connectionDetail.OrganizationMajorVersion >= CRM2011 && int.Parse(connectionDetail.OrganizationVersion.Split(new char[1] { '.' })[3]) >= Rollup12);
Service = service;
TempPostfix = tempPostFix;
MigrateData = migrateData;
ConvertValuesToGlobal = convertValuesToGlobal;
ValidLanguageCodes = GetValidLanguageCodes();
}
private HashSet<int> GetValidLanguageCodes()
{
var resp = (RetrieveAvailableLanguagesResponse)Service.Execute(new RetrieveAvailableLanguagesRequest());
return new HashSet<int>(resp.LocaleIds);
}
public void Run(string entityName, string attributeName, string globalOptionSetName, string globalOptionSetPrefix, Steps stepsToPerform)
{
// Create Temp
AttributeMigrationState state = GetCrmOptionSetLocalToGlobalState(Service, entityName, attributeName, globalOptionSetName, stepsToPerform.HasFlag(Steps.RevertUnsuccessful));
var oldAtt = state.Old;
var tmpAtt = state.Temp;
var newAtt = state.New;
//see if we're undoing a previous unsuccessful run
if (stepsToPerform.HasFlag(Steps.RevertUnsuccessful))
{
UpdateCharts(Service, tmpAtt, oldAtt);
UpdateViews(Service, tmpAtt, oldAtt);
UpdateForms(Service, tmpAtt, oldAtt);
PublishEntity(Service, oldAtt.EntityLogicalName);
AssertCanDelete(Service, tmpAtt);
DeleteField(Service, tmpAtt);
return;
}
AssertValidStepsForState(attributeName, stepsToPerform, state);
AssertValidGlobalOptionSet(state, stepsToPerform, globalOptionSetName, globalOptionSetPrefix);
if (stepsToPerform.HasFlag(Steps.CreateTemp))
{
tmpAtt = CreateTempAttribute(Service, oldAtt);
}
if (oldAtt != null)
{
if (stepsToPerform.HasFlag(Steps.MigrateToTemp))
{
if (MigrateData)
{
CopyData(Service, oldAtt, tmpAtt, globalOptionSetPrefix);
}
// Replace Old Attribute with Tmp Attribute
UpdateCharts(Service, oldAtt, tmpAtt);
UpdateViews(Service, oldAtt, tmpAtt);
UpdateForms(Service, oldAtt, tmpAtt);
PublishEntity(Service, oldAtt.EntityLogicalName);
}
if (stepsToPerform.HasFlag(Steps.RemoveLocal))
{
AssertCanDelete(Service, oldAtt);
DeleteField(Service, oldAtt);
}
}
// Create new Attribute
if (stepsToPerform.HasFlag(Steps.CreateGlobal))
{
newAtt = newAtt ?? CreateNewAttribute(Service, tmpAtt, state.GlobalOptionSet, globalOptionSetName, globalOptionSetPrefix);
}
if (stepsToPerform.HasFlag(Steps.MigrateToGlobal))
{
if (MigrateData)
{
CopyData(Service, tmpAtt, newAtt, globalOptionSetPrefix);
}
// Replace Tmp Attribute with New Attribute
UpdateCharts(Service, tmpAtt, newAtt);
UpdateViews(Service, tmpAtt, newAtt);
UpdateForms(Service, tmpAtt, newAtt);
PublishEntity(Service, newAtt.EntityLogicalName);
}
if (stepsToPerform.HasFlag(Steps.RemoveTemp))
{
AssertCanDelete(Service, tmpAtt);
DeleteField(Service, tmpAtt);
}
}
private void AssertValidGlobalOptionSet(AttributeMigrationState state, Steps stepsToPerform, string globalOptionSetName, string globalOptionSetPrefix)
{
if (MigrateData &&
(stepsToPerform.HasFlag(Steps.MigrateToGlobal) || stepsToPerform.HasFlag(Steps.MigrateToTemp)))
{
var localOptionSet = (state.Old ?? state.Temp);
if (localOptionSet == null)
{
throw new InvalidOperationException("Unable to Migrate Data! No local Option Set was found!");
}
if (state.GlobalOptionSet != null)
{
if (!state.GlobalOptionSet.IsGlobal.GetValueOrDefault()) // Don't think this is possible
{
throw new InvalidOperationException("Unable to Migrate Data! " + globalOptionSetName +
" is invalid because it already exists and is not a global option set!");
};
var globalOptions = GetOptions(state.GlobalOptionSet);
foreach (var option in localOptionSet.Options)
{
var value = option.Value;
if (ConvertValuesToGlobal)
{
if (value > 9999)
{
throw new InvalidOperationException("Unable to Convert Option Set for Migrate! " + option.Value +
" is an invalid local set value. Local Option Set Values can not be greater than 9999");
}
value = ConvertLocalValueToGlobalValue(globalOptionSetPrefix, value);
}
if (!globalOptions.Any(osv => osv.Value == option.Value))
{
var displayText = option.Label == null ? String.Empty : option.Label.GetLocalOrDefaultText("N/A");
throw new InvalidOperationException(String.Format("Unable to Migrate Data! Option Set Value \"{0}\" ({1}) does not have a corresponding value in the global option set {2} ({3})",
displayText, option.Value, globalOptionSetName, value));
}
}
}
else if (ConvertValuesToGlobal)
{
foreach (var option in localOptionSet.Options.Where(o => o.Value.GetValueOrDefault() > 9999))
{
throw new InvalidOperationException("Unable to Convert Option Set for Migrate! " + option.Value +
" is an invalid local set value. Local Option Set Values can not be greater than 9999");
}
}
}
}
private static List<OptionMetadata> GetOptions(OptionSetMetadataBase optionSet)
{
List<OptionMetadata> options;
var boolOS = optionSet as BooleanOptionSetMetadata;
if (boolOS == null)
{
options = ((OptionSetMetadata)optionSet).Options.ToList();
}
else
{
options = new List<OptionMetadata>(){
boolOS.FalseOption, boolOS.TrueOption
};
}
return options;
}
private AttributeMigrationState GetCrmOptionSetLocalToGlobalState(IOrganizationService service, string entityName, string attributeName, string globalOptionSetName, bool revert)
{
AttributeMigrationState state;
state = new AttributeMigrationState();
try
{
Trace("Searching for Option Set " + entityName + "." + attributeName);
state.Old = (AttributeMetadata)((RetrieveAttributeResponse)service.Execute(new RetrieveAttributeRequest() { EntityLogicalName = entityName, LogicalName = attributeName })).AttributeMetadata;
Trace("Option Set " + entityName + "." + attributeName + " found");
if (state.Old.OptionSet.IsGlobal.GetValueOrDefault())
{
state.New = state.Old;
state.Old = null;
state.Temp = GetTempAttribute(service, entityName, attributeName);
Trace("Option Set " + entityName + "." + attributeName + " is already a global option set");
}
else
{
try
{
Trace("Searching for Option Set " + entityName + "." + attributeName + TempPostfix);
state.Temp = (AttributeMetadata)((RetrieveAttributeResponse)service.Execute(new RetrieveAttributeRequest() { EntityLogicalName = entityName, LogicalName = attributeName + TempPostfix })).AttributeMetadata;
Trace("Option Set " + entityName + "." + attributeName + TempPostfix + " found");
}
catch { Trace("Option Set " + entityName + "." + attributeName + TempPostfix + " not found"); }
}
}
catch (System.ServiceModel.FaultException<OrganizationServiceFault> ex)
{
if (ex.Message.Contains("Could not find attribute"))
{
if (state.New == null)
{
Trace("Option Set " + entityName + "." + attributeName + " not found. Seaching for Temp");
state.Temp = GetTempAttribute(Service, entityName, attributeName);
}
else
{
throw new Exception(state.New.LogicalName + " is already a global option set, and no tmp optionset found to remove");
}
}
else
{
throw;
}
}
try
{
state.GlobalOptionSet = ((RetrieveOptionSetResponse)Service.Execute(new RetrieveOptionSetRequest() { Name = globalOptionSetName })).OptionSetMetadata;
}
catch {}
return state;
}
private void AssertValidStepsForState(string attributeName, Steps stepsToPerform, AttributeMigrationState state)
{
if (stepsToPerform.HasFlag(Steps.CreateTemp) && state.Temp != null)
{
throw new InvalidOperationException("Unable to Create Temp! Temp " + state.Temp.EntityLogicalName + "." + state.Temp.LogicalName + " already exists!");
}
if (stepsToPerform.HasFlag(Steps.MigrateToTemp))
{
// Can only Migrate if old already exists
if (state.Old == null)
{
throw new InvalidOperationException("Unable to Migrate! Local Option Set Value Attribute " + attributeName + " does not exist!");
}
// Can only Migrate if Tmp already exists, or temp will be created
if (!(state.Temp != null || stepsToPerform.HasFlag(Steps.CreateTemp)))
{
throw new InvalidOperationException("Unable to Migrate! Temporary Attribute " + attributeName + TempPostfix + " does not exist!");
}
}
if (stepsToPerform.HasFlag(Steps.RemoveLocal))
{
if (state.Old == null)
{
AssertInvalidState("Unable to Remove Local! Local Option Set Value Attribute " + attributeName + " does not exist!");
}
// Can only Remove local if Tmp already exists, or temp will be created
if (!(state.Temp != null || stepsToPerform.HasFlag(Steps.CreateTemp)))
{
AssertInvalidState("Unable to Remove Local! Temporary Attribute " + attributeName + TempPostfix + " does not exist!");
}
// Can only Remove local if Tmp will be migrated, or has been migrated
if (!stepsToPerform.HasFlag(Steps.MigrateToTemp))
{
try
{
AssertCanDelete(Service, state.Old);
}
catch
{
AssertInvalidState("Unable to Remove Local! Local Option Set Value Attribute " + attributeName + " has not been migrated to Temporary Attribute!");
}
}
}
if (stepsToPerform.HasFlag(Steps.CreateGlobal))
{
// Can only Create Global if Local does not exist or will be removed
if (!(state.Old == null || stepsToPerform.HasFlag(Steps.RemoveLocal)))
{
AssertInvalidState("Unable to create Global! Local Option Set Value Attribute " + attributeName + " still exists!");
}
// Can only Create Global if doesn't already exist
if (!(state.New == null))
{
AssertInvalidState("Unable to create Global! Global Option Set Value Attribute " + attributeName + " already exists!");
}
}
if (stepsToPerform.HasFlag(Steps.MigrateToGlobal))
{
if (!(state.Temp != null || stepsToPerform.HasFlag(Steps.CreateTemp)))
{
AssertInvalidState("Unable to Migrate! Temp Attribute " + attributeName + TempPostfix + " does not exist!");
}
// Can only Migrate if Global Already exists, or Global will be created
if (!(state.New != null || stepsToPerform.HasFlag(Steps.CreateGlobal)))
{
AssertInvalidState("Unable to Migrate! Global Option Set Value Attribute " + attributeName + " does not exist!");
}
}
if (stepsToPerform.HasFlag(Steps.RemoveTemp))
{
// Can Only remove Temp if it exists, or will exist
if (!(state.Temp != null || stepsToPerform.HasFlag(Steps.CreateTemp)))
{
AssertInvalidState("Unable to Remove Temp! Temp Attribute " + attributeName + TempPostfix + " does not exist!");
}
// Can Only remove Temp if Global Already exists, or Global will be created
if (!(state.New != null || stepsToPerform.HasFlag(Steps.CreateGlobal)))
{
AssertInvalidState("Unable to Migrate! Global Option Set Value Attribute " + attributeName + " does not exist!");
}
// Can only Remove tmp if global will be migrated, or has been migrated
if (!stepsToPerform.HasFlag(Steps.MigrateToGlobal))
{
try
{
AssertCanDelete(Service, state.Temp);
}
catch
{
AssertInvalidState("Unable to Remove Global! Local Option Set Value Attribute " + attributeName + " has not been migrated to Temporary Attribute!");
}
}
}
}
private void AssertInvalidState(string message)
{
throw new InvalidOperationException(message);
}
private AttributeMetadata GetTempAttribute(IOrganizationService service, string entityName, string attributeName)
{
return (AttributeMetadata)((RetrieveAttributeResponse)service.Execute(new RetrieveAttributeRequest() { EntityLogicalName = entityName, LogicalName = attributeName + TempPostfix })).AttributeMetadata;
}
private AttributeMetadata CreateNewAttribute(IOrganizationService service, AttributeMetadata tmpAtt, OptionSetMetadataBase globalOptionSet, string globalOptionSetName, string globalOSPrefix)
{
// Create Global OptionSet If needed
if (globalOptionSet == null)
{
globalOptionSet = GetOptionSet(tmpAtt);
globalOptionSet.IsGlobal = true;
globalOptionSet.MetadataId = null;
globalOptionSet.Name = globalOptionSetName;
if (ConvertValuesToGlobal)
{
foreach (var option in GetOptions(globalOptionSet))
{
option.Value = ConvertLocalValueToGlobalValue(globalOSPrefix, option.Value);
}
}
Trace("Creating Global Option Set " + globalOptionSetName);
try
{
var response = (CreateOptionSetResponse)service.Execute(new CreateOptionSetRequest()
{
OptionSet = globalOptionSet
});
globalOptionSet.MetadataId = response.OptionSetId;
}
catch
{
Trace("Error Creating Option Set " + globalOptionSet.Name);
throw;
}
}
AttributeMetadata att = CloneAttribute(tmpAtt, globalOptionSet, s => s.Replace(TempPostfix, String.Empty));
ClearOptions(globalOptionSet);
Trace("Creating Attribute " + att.EntityLogicalName + "." + att.LogicalName);
try
{
service.Execute(new CreateAttributeRequest()
{
Attribute = att,
EntityName = att.EntityLogicalName,
});
}
catch
{
Trace("Error Creating Attribute " + att.LogicalName);
throw;
}
PublishEntity(service, att.EntityLogicalName);
return att;
}
private void ClearOptions(OptionSetMetadataBase globalOptionSet)
{
var options = globalOptionSet as OptionSetMetadata;
if (options == null)
{
var boolOptions = (BooleanOptionSetMetadata)globalOptionSet;
boolOptions.TrueOption = null;
boolOptions.FalseOption = null;
}
else
{
options.Options.Clear();
}
}
private void DeleteField(IOrganizationService service, AttributeMetadata att)
{
Trace("Deleting Field " + att.EntityLogicalName + "." + att.LogicalName);
service.Execute(new DeleteAttributeRequest()
{
EntityLogicalName = att.EntityLogicalName,
LogicalName = att.LogicalName
});
}
private void AssertCanDelete(IOrganizationService service, AttributeMetadata oldAtt)
{
Trace("Checking for Delete Dependencies for " + oldAtt.EntityLogicalName + "." + oldAtt.LogicalName);
var depends = (RetrieveDependenciesForDeleteResponse)service.Execute(new RetrieveDependenciesForDeleteRequest()
{
ComponentType = (int)componenttype.Attribute,
ObjectId = oldAtt.MetadataId.Value
});
var errors = new List<String>();
foreach (var d in depends.EntityCollection.ToEntityList<Dependency>())
{
var type = (componenttype) d.DependentComponentType.GetValueOrDefault();
string err = type + " " + d.DependentComponentObjectId.Value;
if (type == componenttype.EntityRelationship)
{
var response =
(RetrieveRelationshipResponse) service.Execute(new RetrieveRelationshipRequest {MetadataId = (Guid) d.DependentComponentObjectId});
Trace("Entity Relationship {0} must be manually removed/added", response.RelationshipMetadata.SchemaName);
}
else if (type == componenttype.SavedQueryVisualization)
{
SavedQueryVisualization sq = service.GetEntity<SavedQueryVisualization>(d.DependentComponentObjectId.Value);
if (sq != null)
err = String.Format("{0} ({1} - {2})", err, sq.Name, sq.CreatedBy.Name);
}
else if (type == componenttype.SavedQuery)
{
SavedQuery sq = service.GetEntity<SavedQuery>(d.DependentComponentObjectId.Value);
if (sq != null)
err = String.Format("{0} ({1} - {2})", err, sq.Name, sq.CreatedBy.Name);
}
errors.Add(err);
}
if (errors.Count > 0)
{
throw new Exception("Dependencies found:\r\n\t" + String.Join("\r\n\t", errors));
}
}
private void UpdateCharts(IOrganizationService service, AttributeMetadata from, AttributeMetadata to)
{
StringBuilder summary = new StringBuilder();
Dictionary<string, string> conditions = new Dictionary<string, string>();
conditions.Add("name=\"" + from.LogicalName + "\"", "name=\"#REPLACE#\"");
foreach (KeyValuePair<string, string> kvp in conditions)
{
Trace("Retrieving System Charts with cond: " + kvp.Key);
var charts = service.GetEntities<SavedQueryVisualization>(
new ConditionExpression("datadescription", ConditionOperator.Like, "%<entity name=\"" + from.EntityLogicalName + "\">%" + kvp.Key + "%"));
foreach (var chart in charts)
{
Trace("Updating Chart " + chart.Name);
chart.DataDescription = chart.DataDescription.Replace(kvp.Key, kvp.Value.Replace("#REPLACE#", to.LogicalName));
service.Update(chart);
}
}
foreach (KeyValuePair<string, string> kvp in conditions)
{
Trace("Retrieving User Charts with cond: " + kvp.Key);
var charts = service.GetEntities<UserQueryVisualization>(
new ConditionExpression("datadescription", ConditionOperator.Like, "%<entity name=\"" + from.EntityLogicalName + "\">%" + kvp.Key + "%"));
foreach (var chart in charts)
{
Trace("Updating Chart " + chart.Name);
chart.DataDescription = chart.DataDescription.Replace(kvp.Key, kvp.Value.Replace("#REPLACE#", to.LogicalName));
service.Update(chart);
}
}
if (summary.Length > 0)
Trace(summary.ToString());
}
private void UpdateForms(IOrganizationService service, AttributeMetadata from, AttributeMetadata to)
{
Trace("Retrieving Forms");
var forms = service.GetEntities<SystemForm>(
"objecttypecode", from.EntityLogicalName,
new ConditionExpression("formxml", ConditionOperator.Like, "%<control id=\"" + from.LogicalName + "\"%"));
foreach (var form in forms)
{
//if (form.IsManaged.Value || !form.IsCustomizable.CanBeChanged)
// Trace(String.Format("Cannot update form '{0}' Is not Customizable - you must update this manually!", form.Name));
//else
//{
Trace("Updating Form " + form.Name);
form.FormXml = form.FormXml.Replace("<control id=\"" + from.LogicalName + "\"", "<control id=\"" + to.LogicalName + "\"").
Replace("datafieldname=\"" + from.LogicalName + "\"", "datafieldname=\"" + to.LogicalName + "\"");
service.Update(form);
//}
}
}
private void UpdateViews(IOrganizationService service, AttributeMetadata from, AttributeMetadata to)
{
StringBuilder summary = new StringBuilder();
Dictionary<string, string> conditions = new Dictionary<string, string>();
conditions.Add("name=\"" + from.LogicalName + "\"", "name=\"#REPLACE#\"");
conditions.Add("attribute=\"" + from.LogicalName + "\"", "attribute=\"#REPLACE#\"");
foreach (KeyValuePair<string, string> kvp in conditions)
{
Trace("Retrieving Views with cond: " + kvp.Key);
var queries = service.GetEntities<SavedQuery>(
new ConditionExpression("fetchxml", ConditionOperator.Like, "%<entity name=\"" + from.EntityLogicalName + "\">%" + kvp.Key + "%"));
foreach (var query in queries)
{
if (query.QueryType != 0)
{
Trace(String.Format("Cannot update view '{0}' as this is not a Public view - you must update this manually!", query.Name));
summary.AppendFormat("Update view '{0}' manually!", query.Name);
}
else
{
Trace("Updating View " + query.Name);
query.FetchXml = query.FetchXml.Replace(kvp.Key, kvp.Value.Replace("#REPLACE#", to.LogicalName));
if (query.LayoutXml != null)
{
query.LayoutXml = query.LayoutXml.Replace(kvp.Key, kvp.Value.Replace("#REPLACE#", to.LogicalName));
}
service.Update(query);
}
}
}
if (summary.Length > 0)
Trace(summary.ToString());
}
private void CopyData(IOrganizationService service, OptionSetAttributeContainer from, OptionSetAttributeContainer to, string globalOSPrefix)
{
if (!MigrateData) { return; }
var total = GetRecordCount(service, from);
var count = 0;
Trace("Copying data from {0} to {1}", from.LogicalName, to.LogicalName);
var requests = new OrganizationRequestCollection();
// Grab from and to, and only update if not equal. This is to speed things up if it has failed part way through
foreach (var entity in service.RetrieveAllList<Entity>(new QueryExpression(from.EntityLogicalName) { ColumnSet = new ColumnSet("name", from.LogicalName, to.LogicalName)}))
{
if (count++%10 == 0 || count == total)
{
if (requests.Any())
{
PerformUpdates(service, requests);
}
Trace("Copying {0} / {1}", count, total);
requests.Clear();
}
var value = entity.GetAttributeValue<OptionSetValue>(from.LogicalName);
var toValue = entity.GetAttributeValue<OptionSetValue>(to.LogicalName);
if (value != null)
{
if (ConvertValuesToGlobal && to.OptionSet.IsGlobal.GetValueOrDefault() && !from.OptionSet.IsGlobal.GetValueOrDefault())
{
value = new OptionSetValue(ConvertLocalValueToGlobalValue(globalOSPrefix, value.Value).GetValueOrDefault());
}
if (!value.Equals(toValue))
{
Trace(String.Format("Updating: {0} | {1} | {2}", entity.LogicalName, entity["name"], entity.Id));
entity.Attributes[to.LogicalName] = value;
requests.Add(new UpdateRequest() {Target = entity});
}
}
else if (toValue != null)
{
Trace(String.Format("Updating: {0} | {1} | {2}", entity.LogicalName, entity["name"], entity.Id));
entity.Attributes[to.LogicalName] = null;
requests.Add(new UpdateRequest() { Target = entity });
}
}
if (requests.Any())
{
PerformUpdates(service, requests);
}
Trace("Data Migration Complete", count, total);
}
private void PerformUpdates(IOrganizationService service, OrganizationRequestCollection requests)
{
if (SupportsExecuteMultipleRequest)
{
var response = (ExecuteMultipleResponse) service.Execute(
new ExecuteMultipleRequest()
{
Settings = new ExecuteMultipleSettings()
{
ContinueOnError = false,
ReturnResponses = false
},
Requests = requests,
});
if (response.IsFaulted)
{
var fault = response.Responses.First().Fault;
while (fault.InnerFault != null)
{
fault = fault.InnerFault;
}
var errorDetails = String.Empty;
if (fault.ErrorDetails.ContainsKey("CallStack"))
{
errorDetails = Environment.NewLine + fault.ErrorDetails["CallStack"];
}
errorDetails = String.Format("{0}{1}{1}TRACE TEXT:{1}{2}", errorDetails, Environment.NewLine, fault.TraceText);
throw new Exception(fault.Message + errorDetails);
}
}
else
{
foreach (var request in requests)
{
service.Save(((UpdateRequest) request).Target);
}
}
}
private static int? ConvertLocalValueToGlobalValue(string globalOSPrefix, int? value)
{
if (value.HasValue)
{
return int.Parse(globalOSPrefix + value.ToString().PadLeft(4, '0'));
}
else
{
return null;
}
}
private int GetRecordCount(IOrganizationService service, AttributeMetadata from)
{
Trace("Retrieving {0} id attribute name", from.EntityLogicalName);
var response = (RetrieveEntityResponse)service.Execute(new RetrieveEntityRequest() { LogicalName = from.EntityLogicalName, EntityFilters = EntityFilters.Entity });
Trace("Determining record count (accurate only up to 50000)");
var xml = String.Format(@"
<fetch distinct='false' mapping='logical' aggregate='true'>
<entity name='{0}'>
<attribute name='{1}' alias='{1}_count' aggregate='count'/>
</entity>
</fetch>", from.EntityLogicalName, response.EntityMetadata.PrimaryIdAttribute);
int total;
try
{
var resultEntity = service.RetrieveMultiple(new FetchExpression(xml)).Entities.First();
total = resultEntity.GetAliasedValue<int>(response.EntityMetadata.PrimaryIdAttribute + "_count");
}
catch (Exception ex)
{
if (ex.Message.Contains("AggregateQueryRecordLimit exceeded"))
{
total = 50000;
}
else
{
throw;
}
}
return total;
}
private OptionSetAttributeContainer CreateTempAttribute(IOrganizationService service, AttributeMetadata oldAtt)
{
var optionSet = GetOptionSet(oldAtt);
optionSet.MetadataId = null;
optionSet.Name = optionSet.Name + TempPostfix;
AttributeMetadata att = CloneAttribute(oldAtt, optionSet, n => n += TempPostfix);
var createAttReq = new CreateAttributeRequest()
{
EntityName = att.EntityLogicalName,
Attribute = att
};
try
{
var response = (CreateAttributeResponse)service.Execute(createAttReq);
att.MetadataId = response.AttributeId;
}
catch
{
Trace("Error Creating Attribute " + att.LogicalName);
throw;
}
// http://msdn.microsoft.com/en-us/library/microsoft.crm.sdk.messages.publishxmlrequest.parameterxml.aspx
PublishEntity(service, oldAtt.EntityLogicalName);
return att;
}
private AttributeMetadata CloneAttribute(AttributeMetadata att, OptionSetMetadataBase optionSet, Func<string, string> convertName)
{
AttributeMetadata clone = CopyAttribute(att, optionSet);
clone.CanModifyAdditionalSettings = att.CanModifyAdditionalSettings;
clone.Description = att.Description;
clone.DisplayName = att.DisplayName;
clone.ExtensionData = att.ExtensionData;
clone.IsAuditEnabled = att.IsAuditEnabled;
clone.IsCustomizable = att.IsCustomizable;
clone.IsRenameable = att.IsRenameable;
clone.IsSecured = att.IsSecured;
clone.IsValidForAdvancedFind = att.IsValidForAdvancedFind;
clone.LinkedAttributeId = att.LinkedAttributeId;
clone.RequiredLevel = att.RequiredLevel;
// Fix for issue 1468 Inactive Language Causing Error
RemoveInvalidLanguageLocalizedLabels(att.Description);
RemoveInvalidLanguageLocalizedLabels(att.DisplayName);
clone.LogicalName = convertName(att.LogicalName);
clone.SchemaName = convertName(att.SchemaName);
// Update EntityLogicalName for other methods to use
SetEntityLogicalName(clone, att.EntityLogicalName);
return clone;
}
private AttributeMetadata CopyAttribute(AttributeMetadata existingAtt, OptionSetMetadataBase optionSet)
{
AttributeMetadata att;
switch (existingAtt.AttributeType.GetValueOrDefault())
{
case AttributeTypeCode.Boolean:
var boolAtt = (BooleanAttributeMetadata)existingAtt;
var tmpBool = new BooleanAttributeMetadata()
{
DefaultValue = boolAtt.DefaultValue,
OptionSet = (BooleanOptionSetMetadata)optionSet,
};
RemoveInvalidLanguageLocalizedLabels(tmpBool.OptionSet.Description);
RemoveInvalidLanguageLocalizedLabels(tmpBool.OptionSet.DisplayName);
RemoveInvalidLanguageLocalizedLabels(tmpBool.OptionSet.TrueOption.Description);
RemoveInvalidLanguageLocalizedLabels(tmpBool.OptionSet.TrueOption.Label);
RemoveInvalidLanguageLocalizedLabels(tmpBool.OptionSet.FalseOption.Description);
RemoveInvalidLanguageLocalizedLabels(tmpBool.OptionSet.FalseOption.Label);
att = tmpBool;
break;
case AttributeTypeCode.Picklist:
att = new PicklistAttributeMetadata();
break;
case AttributeTypeCode.State:
att = new StateAttributeMetadata();
break;
case AttributeTypeCode.Status:
att = new StatusAttributeMetadata();
break;
default:
throw new EnumCaseUndefinedException<AttributeTypeCode>(existingAtt.AttributeType.GetValueOrDefault());
}
EnumAttributeMetadata enumAtt = existingAtt as EnumAttributeMetadata;
if (enumAtt != null)
{
var tmp = att as EnumAttributeMetadata;
tmp.DefaultFormValue = enumAtt.DefaultFormValue;
tmp.OptionSet = (OptionSetMetadata)optionSet;
// Fix for issue 1468 Inactive Language Causing Error
RemoveInvalidLanguageLocalizedLabels(tmp.OptionSet.Description);
RemoveInvalidLanguageLocalizedLabels(tmp.OptionSet.DisplayName);
foreach (var label in tmp.OptionSet.Options.Select(o => o.Label))
{
RemoveInvalidLanguageLocalizedLabels(label);
}
foreach (var description in tmp.OptionSet.Options.Select(o => o.Description))
{
RemoveInvalidLanguageLocalizedLabels(description);
}
}
return att;
}
private void RemoveInvalidLanguageLocalizedLabels(Label label)
{
if (label == null)
{
return;
}
var labelsToRemove = new List<LocalizedLabel>();
foreach (var local in label.LocalizedLabels)
{
if (!ValidLanguageCodes.Contains(local.LanguageCode))
{
labelsToRemove.Add(local);
}
}
if (label.UserLocalizedLabel != null && !ValidLanguageCodes.Contains(label.UserLocalizedLabel.LanguageCode))
{
Trace("UserLocalizedLabel was invalid. Removing Localization Label '{0}' for language code '{1}'", label.UserLocalizedLabel.Label, label.UserLocalizedLabel.LanguageCode);
label.UserLocalizedLabel = null;
}
foreach (var local in labelsToRemove)
{
Trace("Removing Localization Label '{0}' for language code '{1}'", local.Label, local.LanguageCode);
label.LocalizedLabels.Remove(local);
}
labelsToRemove.Clear();
}
private OptionSetMetadataBase GetOptionSet(AttributeMetadata att)
{
OptionSetMetadataBase optionSet = null;
switch (att.AttributeType.GetValueOrDefault())
{
case AttributeTypeCode.Boolean:
optionSet = ((BooleanAttributeMetadata)att).OptionSet;
break;
case AttributeTypeCode.Picklist:
case AttributeTypeCode.EntityName:
case AttributeTypeCode.State:
case AttributeTypeCode.Status:
optionSet = ((EnumAttributeMetadata)att).OptionSet;
break;
}
return optionSet;
}
private void SetEntityLogicalName(AttributeMetadata att, string entityLogicalName)
{
var prop = att.GetType().GetProperty("EntityLogicalName");
prop.SetValue(att, entityLogicalName);
}
private void PublishEntity(IOrganizationService service, string logicalName)
{
Trace("Publishing Entity " + logicalName);
service.Execute(new PublishXmlRequest()
{
ParameterXml = "<importexportxml>"
+ " <entities>"
+ " <entity>" + logicalName + "</entity>"
+ " </entities>"
+ "</importexportxml>"
});
}
private void Trace(string message)
{
OnLog(message);
}
private void Trace(string messageFormat, params object[] args)
{
OnLog(String.Format(messageFormat, args));
}
private class OptionSetAttributeContainer
{
public AttributeMetadata Attribute { get; set; }
public List<OptionMetadata> Options { get; set; }
public OptionSetMetadataBase OptionSet { get; private set; }
public string LogicalName { get { return Attribute.LogicalName; } }
public string EntityLogicalName { get { return Attribute.EntityLogicalName; } }
private OptionSetMetadataBase GetOptionSet(AttributeMetadata att)
{
OptionSetMetadataBase value = null;
if (att != null)
{
switch (att.AttributeType.GetValueOrDefault())
{
case AttributeTypeCode.Boolean:
var boolOptionSet = ((BooleanAttributeMetadata)att).OptionSet;
Options = new List<OptionMetadata>(){
boolOptionSet.FalseOption, boolOptionSet.TrueOption
};
value = boolOptionSet;
break;
case AttributeTypeCode.State:
//case AttributeTypeCode.EntityName:
case AttributeTypeCode.Picklist:
case AttributeTypeCode.Status:
var enumOptionSet = ((EnumAttributeMetadata)att).OptionSet;
Options = enumOptionSet.Options.ToList();
value = enumOptionSet;
break;
default:
throw new EnumCaseUndefinedException<AttributeTypeCode>(att.AttributeType.GetValueOrDefault());
}
}
return value;
}
public static implicit operator OptionSetAttributeContainer(AttributeMetadata att)
{
var container = new OptionSetAttributeContainer() { Attribute = att };
container.OptionSet = container.GetOptionSet(att);
return container;
}
public static implicit operator AttributeMetadata(OptionSetAttributeContainer container)
{
return container.Attribute;
}
}
private class AttributeMigrationState
{
public OptionSetAttributeContainer Old { get; set; }
public OptionSetAttributeContainer Temp { get; set; }
public OptionSetAttributeContainer New { get; set; }
public OptionSetMetadataBase GlobalOptionSet { get; set; }
}
}