Programmatically cloning Octopus Deploy Process steps and modifyng the cloned steps - octopus-deploy

We are developing a Pipeline for which we have to add over 100 steps and modify two things for each step: Step Name and PackageID.
Rather than going through the pain of doing this via the UI, we’d like to do this programmatically.
Below is some C# I’ve sketched out for this (I’m a C# developer with extremely limited PowerShell skills, that’s why I did this in C#).
The lines above the comment “From here on is where I'm fuzzy” are working code, but the lines below the comment are just pseudocode.
Can someone explain to me how to write the lines below the comment (or, the PowerShell equivalent)?
I wasn’t able to find API calls for this.
Thanks
namespace ODClientExample
{
class Program
{
static void Main(string[] args)
{
List<string> ListOfWindowsServices = new List<string>();
ListOfWindowsServices.Add("svc1");
ListOfWindowsServices.Add("svc2");
ListOfFWindowsServices.Add("svc3");
var server = "https://mysite.whatever/";
var apiKey = "API-xxxxxxxxxxxxxxxxxx"; // I generated this via the Octopus UI
var endpoint = new OctopusServerEndpoint(server, apiKey);
var repository = new OctopusRepository(endpoint);
var project = repository.Projects.FindByName("Windows Services");
// From here on is where I'm fuzzy:
//
var procesSteps = GetProcessSteps(project);
var processStepToClone = GetProcesStepByName(processSteps, "SomeProcessStep");
foreach (string svcName in ListofSvcNames)
{
processStepToClone.StepName = svcName;
processStepToClone.PackageID = svcName;
}
}
}
}
I've made a little more progress. I'm now able to access the Steps in the Process, and add a Step. However, when my code calls repository.DeploymentProcesses.Modify, I get this exception:
Please provide a value for the package ID.
Please select the feed that this package will be downloaded from.
Please select one or more roles that 'svc1' step will apply to.
Here's my latest code:
static void Main(string[] args)
{
List<string> ListOfFexWindowsServices = new List<string>();
ListOfFexWindowsServices.Add("svc2");
ListOfFexWindowsServices.Add("svc3");
ListOfFexWindowsServices.Add("svc4");
string server = "https://mysite.stuff/";
string apiKey = "API-xxxxxxxxxxxxxxxxxxxxxxxx"; // I generated this via the Octopus UI
OctopusServerEndpoint endpoint = new OctopusServerEndpoint(server, apiKey);
OctopusRepository repository = new OctopusRepository(endpoint);
ProjectResource projectResource = repository.Projects.FindByName("MyProject");
DeploymentProcessResource deploymentProcess = repository.DeploymentProcesses.Get(projectResource.DeploymentProcessId);
var projectSteps = deploymentProcess.Steps;
DeploymentStepResource stepToClone = new DeploymentStepResource();
foreach (DeploymentStepResource step in projectSteps)
{
if (step.Name == "svc1")
{
stepToClone = step;
break;
}
}
foreach (string serviceName in ListOfFexWindowsServices)
{
DeploymentStepResource newStep = new DeploymentStepResource();
PopulateNewStep(newStep, stepToClone, serviceName);
deploymentProcess.Steps.Add(newStep);
repository.DeploymentProcesses.Modify(deploymentProcess);
}
}
static void PopulateNewStep(DeploymentStepResource newStep, DeploymentStepResource stepToClone, string serviceName)
{
newStep.Name = serviceName;
newStep.Id = Guid.NewGuid().ToString();
newStep.StartTrigger = stepToClone.StartTrigger;
newStep.Condition = stepToClone.Condition;
DeploymentActionResource action = new DeploymentActionResource
{
Name = newStep.Name,
ActionType = "Octopus.TentaclePackage",
Id = Guid.NewGuid().ToString(),
};
PopulateActionProperties(action);
newStep.Actions.Add(action);
// ISSUE: Anything else to do (eg, any other things from stepToClone to copy, or other stuff to create)?
newStep.PackageRequirement = stepToClone.PackageRequirement;
}
static void PopulateActionProperties(DeploymentActionResource action)
{
action.Properties.Add(new KeyValuePair<string, PropertyValueResource>("Octopus.Action.WindowsService.CustomAccountPassword", "#{WindowsService.Password}"));
// TODO: Repeat this sort of thing for each Action Property you see in stepToClone.
}

void Main()
{
var sourceProjectName = "<source project name>";
var targetProjectName = "<target project name>";
var stepToCopyName = "<step name to copy>";
var repo = GetOctopusRepository();
var sourceProject = repo.Projects.FindByName(sourceProjectName);
var targetProject = repo.Projects.FindByName(targetProjectName);
if (sourceProject != null && targetProject != null)
{
var sourceDeploymentProcess = repo.DeploymentProcesses.Get(sourceProject.DeploymentProcessId);
var targetDeploymentProcess = repo.DeploymentProcesses.Get(targetProject.DeploymentProcessId);
if (sourceDeploymentProcess != null && targetDeploymentProcess != null)
{
Console.WriteLine($"Start copy from project '{sourceProjectName}' to project '{targetProjectName}'");
CopyStepToTarget(sourceDeploymentProcess, targetDeploymentProcess, stepToCopyName);
// Update or add the target deployment process
repo.DeploymentProcesses.Modify(targetDeploymentProcess);
Console.WriteLine($"End copy from project '{sourceProjectName}' to project '{targetProjectName}'");
}
}
}
private OctopusRepository GetOctopusRepository()
{
var octopusServer = Environment.GetEnvironmentVariable("OCTOPUS_CLI_SERVER");
var octopusApiKey = Environment.GetEnvironmentVariable("OCTOPUS_CLI_API_KEY");
var endPoint = new OctopusServerEndpoint(octopusServer, octopusApiKey);
return new OctopusRepository(endPoint);
}
private void CopyStepToTarget(DeploymentProcessResource sourceProcess, DeploymentProcessResource targetProcess, string sourceStepName, bool includeChannels = false, bool includeEnvironments = false)
{
var sourceStep = sourceProcess.FindStep(sourceStepName);
if (sourceStep == null)
{
Console.WriteLine($"{sourceStepName} not found in {sourceProcess.ProjectId}");
return;
}
Console.WriteLine($"-> copy step '{sourceStep.Name}'");
var stepToAdd = targetProcess.AddOrUpdateStep(sourceStep.Name);
stepToAdd.RequirePackagesToBeAcquired(sourceStep.RequiresPackagesToBeAcquired);
stepToAdd.WithCondition(sourceStep.Condition);
stepToAdd.WithStartTrigger(sourceStep.StartTrigger);
foreach (var property in sourceStep.Properties)
{
if (stepToAdd.Properties.ContainsKey(property.Key))
{
stepToAdd.Properties[property.Key] = property.Value;
}
else
{
stepToAdd.Properties.Add(property.Key, property.Value);
}
}
foreach (var sourceAction in sourceStep.Actions)
{
Console.WriteLine($"-> copy action '{sourceAction.Name}'");
var targetAction = stepToAdd.AddOrUpdateAction(sourceAction.Name);
targetAction.ActionType = sourceAction.ActionType;
targetAction.IsDisabled = sourceAction.IsDisabled;
if (includeChannels)
{
foreach (var sourceChannel in sourceAction.Channels)
{
targetAction.Channels.Add(sourceChannel);
}
}
if (includeEnvironments)
{
foreach (var sourceEnvironment in sourceAction.Environments)
{
targetAction.Environments.Add(sourceEnvironment);
}
}
foreach (var actionProperty in sourceAction.Properties)
{
if (targetAction.Properties.ContainsKey(actionProperty.Key))
{
targetAction.Properties[actionProperty.Key] = actionProperty.Value;
}
else
{
targetAction.Properties.Add(actionProperty.Key, actionProperty.Value);
}
}
}
}
The above code sample is available in the Octopus Client Api Samples

Related

Send Mail/Task/Appointment with Redemption

I am trying to programmatically send a Mail/Task/Appointment to a user and I have the problem, that every time I send them the first one will get stuck in my outbox... It seems that the first one sent to Outlook has no/looses its sent-date and will stay forever in my outbox.
Here is the code for example "sending a task notification"
using (MapiSession session = new MapiSession())
{
var item = session.CreateItem(Outlook.OlItemType.olTaskItem) as RDOTaskItem;
[..]
try
{
item.Subject = "SUBJECT";
item.Body = "BODY;
item.StartDate = DateTime.Now;
item.DueDate = DateTime.Now;
item.Recipients.Add("test#mail.com");
item.Recipients.Add("test2#mail.com");
item.Recipients.ResolveAll();
item.Assign();
item.Save();
item.Send();
}
[..]
}
Thanks in advance.
So I am not really shure what is the problem so far...
If all receivers are other persons then myself this should work...
Here is my code for sendig a TaskItem
private void NotifyByTask(OutlookNotificationTypes notificationType)
{
Application app = new Application();
var item = app.CreateItem(OlItemType.olTaskItem) as TaskItem;
try
{
item.Subject = this.Subject;
item.Body = this.Body;
if (this.Start != DateTime.MinValue)
{
item.StartDate = this.Start;
item.ReminderSet = true;
item.ReminderPlaySound = true;
item.ReminderTime = this.Start.AddMinutes(-5);
}
if (this.End != DateTime.MinValue)
{
item.DueDate = this.End;
}
item.PercentComplete = this.PercentComplete;
StringBuilder categories = new StringBuilder();
foreach (String category in this.Categories)
{
categories.Append(category);
}
item.Categories = categories.ToString();
bool sendingToMyself = false;
if (this.NotificationType == OutlookNotificationTypes.TaskRequest)
{
// this will add all recipients and checks if Receiver is yourself
sendingToMyself = item.AddRecipients(this.Recipients, OlMailRecipientType.olTo);
}
if (this.NotificationType == OutlookNotificationTypes.TaskRequest
&& (!sendingToMyself))
{
item.Recipients.ResolveAll();
item.Assign();
item.Save();
item.Send();
}
else
{
item.Save();
//insert element
if (this.ItemSaved != null)
{
Microsoft.Office.Interop.Outlook.MAPIFolder folder =
item.Parent as Microsoft.Office.Interop.Outlook.MAPIFolder;
if (folder != null)
{
try
{
String storeId = folder.StoreID;
String entryId = item.EntryID;
this.ItemSaved(storeId, entryId);
}
finally
{
System.Runtime.InteropServices.Marshal.ReleaseComObject(folder);
}
}
}
}
}
finally
{
System.Runtime.InteropServices.Marshal.ReleaseComObject(item);
}
}

Calling Dynamics Web API with Entity metadata early binding

I would like to consume my organizations dynamics oData endpoint but with early bound classes. However, there are a lot of early bound tools out there and I wanted to know which one provides the best developer experience/least resistance?
For example, there is this one:
https://github.com/daryllabar/DLaB.Xrm.XrmToolBoxTools
https://github.com/yagasoft/DynamicsCrm-CodeGenerator
and so on. Is there a developer preference/method out there?
Early bound classes are for use with the Organization Service which is a SOAP service. The normal way to generate those classes is using CrmSvcUtil.
OData can be used in Organization Data Service or Web API, but those don't have Early Bound classes.
Further reading: Introducing the Microsoft Dynamics 365 web services
It's not impossible to use with standard SOAP Early bound class. We just have to be creative. If we work just with basic attributes (fields, not relationships, ecc) it seems possible. For example. for create and update, OData will not accept the entire early bounded class, just pass the attibutes:
class Program
{
static void Main(string[] args)
{
string token = System.Threading.Tasks.Task.Run(() => GetToken()).Result;
CRMWebAPI dynamicsWebAPI = new CRMWebAPI("https:/ORG.api.crm4.dynamics.com/api/data/v9.1/",
token);
CRMGetListOptions listOptions = new CRMGetListOptions
{
Select = new string[] { "EntitySetName" },
Filter = "LogicalName eq 'contact'"
};
dynamic entityDefinitions = dynamicsWebAPI.GetList<ExpandoObject>("EntityDefinitions", listOptions).Result;
Contact contact = new Contact
{
FirstName = "Felipe",
LastName = "Test",
MobilePhone = "38421254"
};
dynamic ret = System.Threading.Tasks.Task.Run(async () => await dynamicsWebAPI.Create(entityDefinitions.List[0].EntitySetName, KeyPairValueToObject(contact.Attributes))).Result;
}
public static async Task<string> GetToken()
{
string api = "https://ORG.api.crm4.dynamics.com/";
ClientCredential credential = new ClientCredential("CLIENT_ID", "CLIENT_SECRET");
AuthenticationContext authenticationContext = new AuthenticationContext("https://login.microsoftonline.com/commom/oauth2/authorize");
return authenticationContext.AcquireTokenAsync(api, credential).Result.AccessToken;
}
public static object KeyPairValueToObject(AttributeCollection keyValuePairs)
{
dynamic expando = new ExpandoObject();
var obj = expando as IDictionary<string, object>;
foreach (var keyValuePair in keyValuePairs)
obj.Add(keyValuePair.Key, keyValuePair.Value);
return obj;
}
}
It's a simple approach and I didn't went further.
Maybe we have to serealize other objects as OptionSets, DateTime (pass just the string) and EntityReferences but this simple test worked fine to me. I'm using Xrm.Tools.WebAPI and Microsoft.IdentityModel.Clients.ActiveDirectory. Maybe it's a way.
[Edit]
And so I decided to go and created a not well tested method to cast the attributes. Problems: We have to follow OData statments to use the API. To update/create an entity reference we can use this to reference https://www.inogic.com/blog/2016/02/set-values-of-all-data-types-using-web-api-in-dynamics-crm/
So
//To EntityReference
entityToUpdateOrCreate["FIELD_SCHEMA_NAME#odata.bind"] = "/ENTITY_SET_NAME(GUID)";
So, it's the Schema name, not field name. If you use CamelCase when set you fields name you'll have a problem where. We can resolve that with a (to that cute) code
public static object EntityToObject<T>(T entity) where T : Entity
{
dynamic expando = new ExpandoObject();
var obj = expando as IDictionary<string, object>;
foreach (var keyValuePair in entity.Attributes)
{
obj.Add(GetFieldName(entity, keyValuePair), CastEntityAttibutesValueOnDynamicObject(keyValuePair.Value));
}
return obj;
}
public static object CastEntityAttibutesValueOnDynamicObject(object attributeValue)
{
if (attributeValue.GetType().Name == "EntityReference")
{
CRMGetListOptions listOptions = new CRMGetListOptions
{
Select = new string[] { "EntitySetName" },
Filter = $"LogicalName eq '{((EntityReference)attributeValue).LogicalName}'"
};
dynamic entitySetName = dynamicsWebAPI.GetList<ExpandoObject>("EntityDefinitions", listOptions).Result.List[0];
return $"/{entitySetName.EntitySetName}({((EntityReference)attributeValue).Id})";
}
else if (attributeValue.GetType().Name == "OptionSetValue")
{
return ((OptionSetValue)attributeValue).Value;
}
else if (attributeValue.GetType().Name == "DateTime")
{
return ((DateTime)attributeValue).ToString("yyyy-MM-dd");
}
else if (attributeValue.GetType().Name == "Money")
{
return ((Money)attributeValue).Value;
}
else if (attributeValue.GetType().Name == "AliasedValue")
{
return CastEntityAttibutesValueOnDynamicObject(((AliasedValue)attributeValue).Value);
}
else
{
return attributeValue;
}
}
public static string GetFieldName<T>(T entity, KeyValuePair<string, object> keyValuePair) where T : Entity
{
switch (keyValuePair.Value.GetType().Name)
{
case "EntityReference":
var entityNameList = System.Threading.Tasks.Task.Run(async () => await dynamicsWebAPI.GetEntityDisplayNameList()).Result;
var firstEntity = entityNameList.Where(x => x.LogicalName == entity.LogicalName).FirstOrDefault();
var attrNameList = System.Threading.Tasks.Task.Run(async () => await dynamicsWebAPI.GetAttributeDisplayNameList(firstEntity.MetadataId)).Result;
return attrNameList.Where(x => x.LogicalName == keyValuePair.Key).Single().SchemaName + "#odata.bind";
case "ActivityParty":
throw new NotImplementedException(); //TODO
default:
return keyValuePair.Key;
}
}
Please, note that this approach do not seems fast or good in anyway. It's better if you have all this values as static so we can save some fetches
[Edit 2]
I just found on XRMToolBox a plugin called "Early bound generator for Web API" and it seems to be the best option. Maybe you should give it a try if you're still curious about that. I guess its the best approach.
The final code is this:
static void Main(string[] args)
{
string token = Task.Run(() => GetToken()).Result;
dynamicsWebAPI = new CRMWebAPI("https://ORG.api.crm4.dynamics.com/api/data/v9.1/",
token);
Contact contact = new Contact
{
FirstName = "Felipe",
LastName = "Test",
MobilePhone = "38421254",
new_Salutation = new EntityReference(new_salutation.EntitySetName, new Guid("{BFA27540-7BB9-E611-80EE-FC15B4281C8C}")),
BirthDate = new DateTime(1993, 04, 14),
};
dynamic ret = Task.Run(async () => await dynamicsWebAPI.Create(Contact.EntitySetName, contact.ToExpandoObject())).Result;
Contact createdContact = dynamicsWebAPI.Get<Contact>(Contact.EntitySetName, ret, new CRMGetListOptions
{
Select = new string[] { "*" }
}).Result;
}
and you have to change the ToExpandoObject on Entity.cs class (generated by the plugin)
public ExpandoObject ToExpandoObject()
{
dynamic expando = new ExpandoObject();
var expandoObject = expando as IDictionary<string, object>;
foreach (var attributes in Attributes)
{
if (attributes.Key == GetIdAttribute())
{
continue;
}
var value = attributes.Value;
var key = attributes.Key;
if (value is EntityReference entityReference)
{
value = $"/{entityReference.EntitySetName}({entityReference.EntityId})";
}
else
{
key = key.ToLower();
if (value is DateTime dateTimeValue)
{
var propertyForAttribute = GetPublicInstanceProperties().FirstOrDefault(x =>
x.Name.Equals(key, StringComparison.InvariantCultureIgnoreCase));
if (propertyForAttribute != null)
{
var onlyDateAttr = propertyForAttribute.GetCustomAttribute<OnlyDateAttribute>();
if (onlyDateAttr != null)
{
value = dateTimeValue.ToString(OnlyDateAttribute.Format);
}
}
}
}
expandoObject.Add(key, value);
}
return (ExpandoObject)expandoObject;
}
Links:
https://github.com/davidyack/Xrm.Tools.CRMWebAPI
https://www.xrmtoolbox.com/plugins/crm.webApi.earlyBoundGenerator/
We currently use XrmToolkit which has it's own version of early binding called ProxyClasses but will allow you to generate early binding using the CRM Service Utility (CrmSvcUtil). It does a lot more than just early binding which is why we use it on all of our projects but the early binding features alone would have me sold on it. in order to regenerate an entity definition all you do is right click the cs file in visual studio and select regenerate and it is done in a few seconds.
For my first 3 years of CRM development I used the XrmToolbox "Early Bound Generator" plugin which is really helpful as well.

Unexpected character ''

Upon migrating from VS2013 to VS2017 i'm running into an unexpected character issue.
Everything builds fine in msbuild 12.0 and VS2013, but when moving to 15.0 I receive hundreds of:
CS1519 Invalid token '?' in class, struct, or interface member declaration
in msbuild command line.
Building inside VS2017 returns:
CS1056 Unexpected character ''
var businessRuleData = principal.GetBusinessRule(​
BusinessRuleEnum.CONTENT_REPOSITORY);
Error occurs at Ch66 which is (B in between that area. The character that is hidden becomes a ? in WordPad. However, as mentioned the same code builds fine in msbuild 12.0. Deletion of all code and re-downloading form TFS didn't solve the issue
Solution Code
Note: Search in code for change_me and make sure to change to whatever your desired items are.
using Microsoft.TeamFoundation.Client;
using Microsoft.TeamFoundation.VersionControl.Client;
using System;
using System.Collections.Generic;
using System.IO;
using System.Text.RegularExpressions;
namespace FixZeroWidthSpace
{
class Program
{
static void Main(string[] args)
{
// change to your directory
var files = Directory.GetFiles(#"D:\change_me", "*.cs", SearchOption.AllDirectories);
var counter = 0;
var counterEdited = 0;
var totalFiles = files.Length;
var failed = new List<string>();
var found = new List<string>();
TfsTeamProjectCollection tpc = null;
Workspace ws = null;
foreach (var file in files)
{
if(counter % 10 == 0)
{
Console.WriteLine("Searched {0} or {1} files, {2} have been edited.", counter, totalFiles, counterEdited);
}
// change to any folders you want to ignore or remove if none
if (!file.Contains("change_me_ignore_folder_name"))
{
string text = File.ReadAllText(file);
var regex = new Regex("[\u200B-\u200D\uFEFF]");
var newText = regex.Replace(text, "");
if (text != newText)
{
try
{
if (ws == null || tpc == null)
{
// change to your TFS server
tpc = new TfsTeamProjectCollection(new Uri(#"http://change_me_your_tfs_url/tfs/DefaultCollection"));
ws = FindWorkspaceByPath(tpc, file);
}
FileAttributes attributes = File.GetAttributes(file);
if ((attributes & FileAttributes.Hidden) == FileAttributes.Hidden)
{
attributes = RemoveAttribute(attributes, FileAttributes.Hidden);
File.SetAttributes(file, attributes);
}
ws.PendEdit(file);
if ((attributes & FileAttributes.ReadOnly) == FileAttributes.ReadOnly)
{
// Make the file RW
attributes = RemoveAttribute(attributes, FileAttributes.ReadOnly);
File.SetAttributes(file, attributes);
}
File.WriteAllText(file, newText);
found.Add(file);
counterEdited++;
}
catch(Exception ex)
{
failed.Add(file);
}
}
}
counter++;
}
tpc.Dispose();
File.WriteAllLines(Path.GetDirectoryName(System.Reflection.Assembly.GetEntryAssembly().Location) + "\\found.txt", found);
File.WriteAllLines(Path.GetDirectoryName(System.Reflection.Assembly.GetEntryAssembly().Location) + "\\failed.txt", failed);
Console.WriteLine("Press any key to exit...");
Console.ReadKey();
}
private static FileAttributes RemoveAttribute(FileAttributes attributes, FileAttributes attributesToRemove)
{
return attributes & ~attributesToRemove;
}
private static Workspace FindWorkspaceByPath(TfsTeamProjectCollection tfs, string workspacePath)
{
VersionControlServer versionControl = tfs.GetService<VersionControlServer>();
WorkspaceInfo workspaceInfo = Workstation.Current.GetLocalWorkspaceInfo(workspacePath);
if (workspaceInfo != null)
{
return versionControl.GetWorkspace(workspaceInfo);
}
// No Workspace found using method 1, try to query all workspaces the user has on this machine.
Workspace[] workspaces = versionControl.QueryWorkspaces(null, Environment.UserName, Environment.MachineName);
foreach (Workspace w in workspaces)
{
foreach (WorkingFolder f in w.Folders)
{
if (f.LocalItem.Equals(workspacePath))
{
return w;
}
}
}
if (workspaces.Length > 0)
{
return workspaces[0];
}
throw new Exception(String.Format("TFS Workspace cannot be determined for {0}.", workspacePath));
}
}
}
Solution
Remove all invalid characters as MSBuild 15.0 and VS2017 are more strict on these unicode characters.
The following code can be utilized to accomplish the removal of all code in a Folder that is an issue. I utilized this as the changes required were too large to be done by hand.
C# Code
Variables
[Insert Folder to Scan] : Example C:\TFS\Code\Branch\Folder
[Insert Folder To Ignore] : Example 3rdPartyCode
Code
using System;
using System.Collections.Generic;
using System.IO;
using System.Text.RegularExpressions;
namespace FixZeroWidthSpace
{
class Program
{
static void Main(string[] args)
{
var files = Directory.GetFiles(#"D:\TFS\210-219\212\MCET_212", "*.cs", SearchOption.AllDirectories);
var counter = 0;
var counterEdited = 0;
var totalFiles = files.Length;
var failed = new List<string>();
var found = new List<string>();
foreach (var file in files)
{
if(counter % 10 == 0)
{
Console.WriteLine("Searched {0} or {1} files, {2} have been edited.", counter, totalFiles, counterEdited);
}
if (!file.Contains("[Insert Folder To Ignore]"))
{
string text = File.ReadAllText(file);
var regex = new Regex("[\u200B-\u200D\uFEFF]");
var newText = regex.Replace(text, "");
if (text != newText)
{
try
{
FileAttributes attributes = File.GetAttributes(file);
if ((attributes & FileAttributes.Hidden) == FileAttributes.Hidden)
{
attributes = RemoveAttribute(attributes, FileAttributes.Hidden);
File.SetAttributes(file, attributes);
}
if ((attributes & FileAttributes.ReadOnly) == FileAttributes.ReadOnly)
{
// Make the file RW
attributes = RemoveAttribute(attributes, FileAttributes.ReadOnly);
File.SetAttributes(file, attributes);
}
File.WriteAllText(file, newText);
found.Add(file);
counterEdited++;
}
catch(Exception ex)
{
failed.Add(file);
}
}
}
counter++;
}
File.WriteAllLines(Path.GetDirectoryName(System.Reflection.Assembly.GetEntryAssembly().Location) + "\\found.txt", found);
File.WriteAllLines(Path.GetDirectoryName(System.Reflection.Assembly.GetEntryAssembly().Location) + "\\failed.txt", failed);
Console.WriteLine("Press any key to exit...");
Console.ReadKey();
}
private static FileAttributes RemoveAttribute(FileAttributes attributes, FileAttributes attributesToRemove)
{
return attributes & ~attributesToRemove;
}
}
}
C# Code w/ TFS Checkout
Variables
[Insert Folder to Scan] : Example C:\TFS\Code\Branch\Folder
[Insert Folder To Ignore] : Example 3rdPartyCode
[Insert URI to TFS Server Collection] : Example http://tfs.company.com:8080/tfs/DefaultCollection
Code
using Microsoft.TeamFoundation.Client;
using Microsoft.TeamFoundation.VersionControl.Client;
using System;
using System.Collections.Generic;
using System.IO;
using System.Text.RegularExpressions;
namespace FixZeroWidthSpace
{
class Program
{
static void Main(string[] args)
{
var files = Directory.GetFiles(#"[Insert Folder to Scan]", "*.cs", SearchOption.AllDirectories);
var counter = 0;
var counterEdited = 0;
var totalFiles = files.Length;
var failed = new List<string>();
var found = new List<string>();
TfsTeamProjectCollection tpc = null;
Workspace ws = null;
foreach (var file in files)
{
if(counter % 10 == 0)
{
Console.WriteLine("Searched {0} or {1} files, {2} have been edited.", counter, totalFiles, counterEdited);
}
if (!file.Contains("3rdparty"))
{
string text = File.ReadAllText(file);
var regex = new Regex("[\u200B-\u200D\uFEFF]");
var newText = regex.Replace(text, "");
if (text != newText)
{
try
{
if (ws == null || tpc == null)
{
tpc = new TfsTeamProjectCollection(new Uri(#"[Insert URI to TFS Server Collection]"));
ws = FindWorkspaceByPath(tpc, file);
}
FileAttributes attributes = File.GetAttributes(file);
if ((attributes & FileAttributes.Hidden) == FileAttributes.Hidden)
{
attributes = RemoveAttribute(attributes, FileAttributes.Hidden);
File.SetAttributes(file, attributes);
}
ws.PendEdit(file);
if ((attributes & FileAttributes.ReadOnly) == FileAttributes.ReadOnly)
{
// Make the file RW
attributes = RemoveAttribute(attributes, FileAttributes.ReadOnly);
File.SetAttributes(file, attributes);
}
File.WriteAllText(file, newText);
found.Add(file);
counterEdited++;
}
catch(Exception ex)
{
failed.Add(file);
}
}
}
counter++;
}
tpc.Dispose();
File.WriteAllLines(Path.GetDirectoryName(System.Reflection.Assembly.GetEntryAssembly().Location) + "\\found.txt", found);
File.WriteAllLines(Path.GetDirectoryName(System.Reflection.Assembly.GetEntryAssembly().Location) + "\\failed.txt", failed);
Console.WriteLine("Press any key to exit...");
Console.ReadKey();
}
private static FileAttributes RemoveAttribute(FileAttributes attributes, FileAttributes attributesToRemove)
{
return attributes & ~attributesToRemove;
}
private static Workspace FindWorkspaceByPath(TfsTeamProjectCollection tfs, string workspacePath)
{
VersionControlServer versionControl = tfs.GetService<VersionControlServer>();
WorkspaceInfo workspaceInfo = Workstation.Current.GetLocalWorkspaceInfo(workspacePath);
if (workspaceInfo != null)
{
return versionControl.GetWorkspace(workspaceInfo);
}
// No Workspace found using method 1, try to query all workspaces the user has on this machine.
Workspace[] workspaces = versionControl.QueryWorkspaces(null, Environment.UserName, Environment.MachineName);
foreach (Workspace w in workspaces)
{
foreach (WorkingFolder f in w.Folders)
{
if (f.LocalItem.Equals(workspacePath))
{
return w;
}
}
}
throw new Exception(String.Format("TFS Workspace cannot be determined for {0}.", workspacePath));
}
}
}
I recently found this happening in one of my solutions. It is nothing present in my code.
If I do a solution clean (right click on the solution -> Clean Solution) the problem goes away.

Dynamics CRM Solution Import via SDK is not working

I have the below code that imports a solution into CRM Dynamics.
The code executes successfully and the import job data returns a result of success. How ever when I look for the solution in Settings->Solutions it is not there. Can anyone suggest a fix?
private void ImportSolution(string solutionPath)
{
byte[] fileBytes = File.ReadAllBytes(solutionPath);
var request = new ImportSolutionRequest()
{
CustomizationFile = fileBytes,
ImportJobId = Guid.NewGuid()
};
var response = _settings.DestinationSourceOrgService.Execute(request);
var improtJob = new ImportJob(_settings);
var importJobResult = improtJob.GetImportJob(request.ImportJobId);
var data = importJobResult.Attributes["data"].ToString();
var jobData = new ImportJobData(data);
var filePath = $#"{this._settings.SolutionExportDirectory}\Logs\";
var fileName = $#"{filePath}{jobData.SolutionName}.xml";
Directory.CreateDirectory(filePath);
File.WriteAllText(fileName, data);
PrintResult(jobData.Result, jobData.SolutionName);
}
public class ImportJob
{
private readonly ConfigurationSettings _settings;
public ImportJob(ConfigurationSettings settings)
{
_settings = settings;
}
public Entity GetImportJob(Guid importJobId)
{
var query = new QueryExpression
{
EntityName = "importjob",
ColumnSet = new ColumnSet("importjobid", "data", "solutionname"),
Criteria = new FilterExpression()
};
var result = _settings.DestinationSourceOrgService.Retrieve("importjob", importJobId, new ColumnSet("importjobid", "data", "solutionname", "progress"));
return result;
}
}
Thre ImportSolutionResponse does not contain any information as per the screen shot below.

Multiple owin listeners with their own set of controllers, with Autofac for DI

I am trying to use multiple in-process owin listeners. Each should have a distinct set of controllers, where they may have the same route handled by a different controller. For instance
localhost:1234/api/app/test should resolve to ControllerA
localhost:5678/api/app/test should resolve to ControllerB
controller a, in owin host 1, has route attribute
[Route("api/app/test")]
controller b, in owin host 2, has route attribute
[Route("api/app/{*path}")]
and is used to forward requests to the other owin host.
We are using Autofac for dependency injection. Routes are configured through attribute routing.
autofac requires a line such as
builder.RegisterApiControllers(typeof(ControllerA).Assembly)
Our OWIN configuration contains:
var config = ConfigureWebApi();
// Configure Autofac
config.DependencyResolver = new AutofacWebApiDependencyResolver(container);
app.UseAutofacMiddleware(container);
app.UseAutofacWebApi(config);
app.UseWebApi(config);
However when starting two listeners, I need to include both assemblies for controller resolving. This leads to a 'duplicate route' exception:
Multiple controller types were found that match the URL. This can
happen if attribute routes on multiple controllers match the requested
URL.\r\n\r\nThe request has found the following matching controller
types:
\r\nLib1.Controllers.ControllerA\r\nLib2.Controllers.ControllerB"
When running the OWIN listeners in separate processes, there are no issues.
I have also tried to use multiple DI containers, one for each OWIN listener, but that conflicts with Web Api 2 as it requires GlobalConfiguration.Configuration.DependencyResolver to be set. Which conflicts with the concept of multiple DI containers.
Can someone guide me how to configure such a setup?
Use the OWIN environment and customize the HttpControllerSelector
Using the OWIN pipeline you can pass information about the request to a custom HttpControllerSelector. This allows you to be selective about which controllers are used to match which routes.
Of course this is easier said than done. The inner workings of WebAPI with respect to routing are not very transparent - source code is often the best documentation in this area.
I could not get the HttpControllerSelector to fully work, so there's an ugly workaround in CustomHttpActionSelector. It may still be sufficient if all you need to do is forward requests from one host to the other.
The end result is:
GET to http://localhost:1234/api/app/test returns "HellofromAController" (directly invokes AController)
GET to http://localhost:5678/api/app/test returns "(FromBController): \"HellofromAController\"" (invokes BController, which forwards the request to AController)
See the full source on github
I left the logging code as-is in case it's useful, but it's not relevant to the solution.
So without further ado:
CustomHttpControllerSelector.cs:
Uses the port-specific OWIN env variable ApiControllersAssembly in to filter the controllers.
public sealed class CustomHttpControllerSelector : DefaultHttpControllerSelector
{
private static readonly ILog Logger;
static CustomHttpControllerSelector()
{
Logger = LogProvider.GetCurrentClassLogger();
}
public CustomHttpControllerSelector(HttpConfiguration configuration) : base(configuration)
{
}
public override HttpControllerDescriptor SelectController(HttpRequestMessage request)
{
var apiControllerAssembly = request.GetOwinEnvironment()["ApiControllersAssembly"].ToString();
Logger.Debug($"{nameof(CustomHttpControllerSelector)}: {{{nameof(apiControllerAssembly)}: {apiControllerAssembly}}}");
var routeData = request.GetRouteData();
var routeCollectionRoute = routeData.Route as IReadOnlyCollection<IHttpRoute>;
var newRoutes = new List<IHttpRoute>();
var newRouteCollectionRoute = new RouteCollectionRoute();
foreach (var route in routeCollectionRoute)
{
var filteredDataTokens = FilterDataTokens(route, apiControllerAssembly);
if (filteredDataTokens.Count == 2)
{
var newRoute = new HttpRoute(route.RouteTemplate, (HttpRouteValueDictionary)route.Defaults, (HttpRouteValueDictionary)route.Constraints, filteredDataTokens);
newRoutes.Add(newRoute);
}
}
var newRouteDataValues = new HttpRouteValueDictionary();
foreach (var routeDataKvp in routeData.Values)
{
var newRouteDataCollection = new List<IHttpRouteData>();
var routeDataCollection = routeDataKvp.Value as IEnumerable<IHttpRouteData>;
if (routeDataCollection != null)
{
foreach (var innerRouteData in routeDataCollection)
{
var filteredDataTokens = FilterDataTokens(innerRouteData.Route, apiControllerAssembly);
if (filteredDataTokens.Count == 2)
{
var newInnerRoute = new HttpRoute(innerRouteData.Route.RouteTemplate, (HttpRouteValueDictionary)innerRouteData.Route.Defaults, (HttpRouteValueDictionary)innerRouteData.Route.Constraints, filteredDataTokens);
var newInnerRouteData = new HttpRouteData(newInnerRoute, (HttpRouteValueDictionary)innerRouteData.Values);
newRouteDataCollection.Add(newInnerRouteData);
}
}
newRouteDataValues.Add(routeDataKvp.Key, newRouteDataCollection);
}
else
{
newRouteDataValues.Add(routeDataKvp.Key, routeDataKvp.Value);
}
HttpRouteData newRouteData;
if (newRoutes.Count > 1)
{
newRouteCollectionRoute.EnsureInitialized(() => newRoutes);
newRouteData = new HttpRouteData(newRouteCollectionRoute, newRouteDataValues);
}
else
{
newRouteData = new HttpRouteData(newRoutes[0], newRouteDataValues);
}
request.SetRouteData(newRouteData);
}
var controllerDescriptor = base.SelectController(request);
return controllerDescriptor;
}
private static HttpRouteValueDictionary FilterDataTokens(IHttpRoute route, string apiControllerAssembly)
{
var newDataTokens = new HttpRouteValueDictionary();
foreach (var dataToken in route.DataTokens)
{
var actionDescriptors = dataToken.Value as IEnumerable<HttpActionDescriptor>;
if (actionDescriptors != null)
{
var newActionDescriptors = new List<HttpActionDescriptor>();
foreach (var actionDescriptor in actionDescriptors)
{
if (actionDescriptor.ControllerDescriptor.ControllerType.Assembly.FullName == apiControllerAssembly)
{
newActionDescriptors.Add(actionDescriptor);
}
}
if (newActionDescriptors.Count > 0)
{
newDataTokens.Add(dataToken.Key, newActionDescriptors.ToArray());
}
}
else
{
newDataTokens.Add(dataToken.Key, dataToken.Value);
}
}
return newDataTokens;
}
}
CustomHttpActionSelector.cs:
You shouldn't need a CustomHttpActionSelector, this only exists to work around an issue with the ActionDescriptors for BController. It works as long as BController has only one method, otherwise you'll need to implement some route-specific logic.
public sealed class CustomHttpActionSelector : ApiControllerActionSelector
{
private static readonly ILog Logger;
static CustomHttpActionSelector()
{
Logger = LogProvider.GetCurrentClassLogger();
}
public override HttpActionDescriptor SelectAction(HttpControllerContext controllerContext)
{
try
{
var actionDescriptor = base.SelectAction(controllerContext);
return actionDescriptor;
}
catch (Exception ex)
{
Logger.WarnException(ex.Message, ex);
IDictionary<string, object> dataTokens;
var route = controllerContext.Request.GetRouteData().Route;
var routeCollectionRoute = route as IReadOnlyCollection<IHttpRoute>;
if (routeCollectionRoute != null)
{
dataTokens = routeCollectionRoute
.Select(r => r.DataTokens)
.SelectMany(dt => dt)
.ToDictionary(kvp => kvp.Key, kvp => kvp.Value);
}
else
{
dataTokens = route.DataTokens;
}
var actionDescriptors = dataTokens
.Select(dt => dt.Value)
.Where(dt => dt is IEnumerable<HttpActionDescriptor>)
.Cast<IEnumerable<HttpActionDescriptor>>()
.SelectMany(r => r)
.ToList();
return actionDescriptors.FirstOrDefault();
}
}
}
Program.cs:
internal class Program
{
private static readonly ILog Logger;
static Program()
{
Log.Logger = new LoggerConfiguration()
.WriteTo
.LiterateConsole()
.MinimumLevel.Is(LogEventLevel.Verbose)
.CreateLogger();
Logger = LogProvider.GetCurrentClassLogger();
}
internal static void Main(string[] args)
{
var builder = new ContainerBuilder();
builder.RegisterModule(new LogRequestModule());
builder.RegisterApiControllers(typeof(AController).Assembly);
builder.RegisterApiControllers(typeof(BController).Assembly);
var container = builder.Build();
var config = GetHttpConfig();
config.DependencyResolver = new AutofacWebApiDependencyResolver(container);
var options = new StartOptions();
options.Urls.Add("http://localhost:1234");
options.Urls.Add("http://localhost:5678");
var listener = WebApp.Start(options, app =>
{
app.Use((ctx, next) =>
{
if (ctx.Request.LocalPort.HasValue)
{
var port = ctx.Request.LocalPort.Value;
string apiControllersAssemblyName = null;
if (port == 1234)
{
apiControllersAssemblyName = typeof(AController).Assembly.FullName;
}
else if (port == 5678)
{
apiControllersAssemblyName = typeof(BController).Assembly.FullName;
}
ctx.Set("ApiControllersAssembly", apiControllersAssemblyName);
Logger.Info($"{nameof(WebApp)}: Port = {port}, ApiControllersAssembly = {apiControllersAssemblyName}");
}
return next();
});
app.UseAutofacMiddleware(container);
app.UseAutofacWebApi(config);
app.UseWebApi(config);
});
Logger.Info(#"Press [Enter] to exit");
Console.ReadLine();
listener.Dispose(); ;
}
private static HttpConfiguration GetHttpConfig()
{
var config = new HttpConfiguration();
config.MapHttpAttributeRoutes();
config.IncludeErrorDetailPolicy = IncludeErrorDetailPolicy.Always;
config.Services.Add(typeof(IExceptionLogger), new LogProviderExceptionLogger());
config.Formatters.Remove(config.Formatters.XmlFormatter);
config.Services.Replace(typeof(IHttpControllerSelector), new CustomHttpControllerSelector(config));
config.Services.Replace(typeof(IHttpActionSelector), new CustomHttpActionSelector());
var traceSource = new TraceSource("LibLog") { Switch = { Level = SourceLevels.All } };
traceSource.Listeners.Add(new LibLogTraceListener());
var diag = config.EnableSystemDiagnosticsTracing();
diag.IsVerbose = false;
diag.TraceSource = traceSource;
return config;
}
}
LibA\Controllers\AController.cs:
[RoutePrefix("api/app")]
public class AController : ApiController
{
private static readonly ILog Logger;
static AController()
{
Logger = LogProvider.GetCurrentClassLogger();
Logger.Debug($"{nameof(AController)}: Static Constructor");
}
public AController()
{
Logger.Debug($"{nameof(AController)}: Constructor");
}
[HttpGet, Route("test")]
public async Task<IHttpActionResult> Get()
{
Logger.Debug($"{nameof(AController)}: Get()");
return Ok($"Hello from {nameof(AController)}");
}
}
LibB\Controllers\BController.cs:
[RoutePrefix("api/app")]
public class BController : ApiController
{
private static readonly ILog Logger;
static BController()
{
Logger = LogProvider.GetCurrentClassLogger();
Logger.Debug($"{nameof(BController)}: Static Constructor");
}
public BController()
{
Logger.Debug($"{nameof(BController)}: Constructor");
}
[HttpGet, Route("{*path}")]
public async Task<IHttpActionResult> Get([FromUri] string path)
{
if (path == null)
{
path = Request.RequestUri.PathAndQuery.Split(new[] {"api/app/"}, StringSplitOptions.RemoveEmptyEntries)[1];
}
Logger.Debug($"{nameof(BController)}: Get({path})");
using (var client = new HttpClient {BaseAddress = new Uri("http://localhost:1234/api/app/")})
{
var result = await client.GetAsync(path);
var content = await result.Content.ReadAsStringAsync();
return Ok($"(From {nameof(BController)}): {content}");
}
}
}
I might have another go at it when I have more time.
Let me know if you make any progress!

Resources