diff --git a/AutoNumber-Old/AutoNumber.csproj b/AutoNumber-Old/AutoNumber.csproj new file mode 100644 index 0000000..43a0d8e --- /dev/null +++ b/AutoNumber-Old/AutoNumber.csproj @@ -0,0 +1,83 @@ + + + + Debug + AnyCPU + 8.0.30703 + 2.0 + {10E2B428-F093-400A-8F7F-38AA8BCBD434} + {4C25E9B5-9FA6-436c-8E19-B395D2A65FAF};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} + Library + Properties + Celedon + CeledonPartners.AutoNumber + v4.0 + 512 + + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + true + + + AutoNumberStrongKey.snk + + + + False + ..\..\..\..\..\CRM 2013 SP1 R1 SDK\SDK\Bin\Microsoft.Crm.Sdk.Proxy.dll + + + False + ..\..\..\..\..\CRM 2013 SP1 R1 SDK\SDK\Bin\Microsoft.Xrm.Sdk.dll + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/AutoNumber-Old/AutoNumberPluginConfig.cs b/AutoNumber-Old/AutoNumberPluginConfig.cs new file mode 100644 index 0000000..8052e93 --- /dev/null +++ b/AutoNumber-Old/AutoNumberPluginConfig.cs @@ -0,0 +1,17 @@ +using System; +using System.Linq; +using System.Collections.Generic; +using System.Runtime.Serialization; + +namespace Celedon +{ + [DataContract] + public class AutoNumberPluginConfig + { + [DataMember] + public string EntityName; + + [DataMember] + public string EventName; + } +} diff --git a/AutoNumber-Old/CeledonExtensions.cs b/AutoNumber-Old/CeledonExtensions.cs new file mode 100644 index 0000000..b93e24d --- /dev/null +++ b/AutoNumber-Old/CeledonExtensions.cs @@ -0,0 +1,154 @@ +// Author: Matt Barnes (matt.barnes@celedonpartners.com) +/*The MIT License (MIT) + +Copyright (c) 2015 Celedon Partners + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +// Collection of useful extension methods + +using System; +using System.IO; +using System.Text; +using System.Runtime.Serialization; +using System.Runtime.Serialization.Json; + +using Microsoft.Xrm.Sdk; +using Microsoft.Xrm.Sdk.Query; + +namespace Celedon +{ + // Helpful extentions + // Use these and add to these to make our plugins a little shorter/cleaner/leaner + public static class Extensions + { + // Used to get values from context inputparameters and outputparameters, of a specific type rather than handling the Object type in our code. + public static bool TryGetValue(this ParameterCollection parameterCollection, string key, out T value) + { + object valueObj; + if (parameterCollection.TryGetValue(key, out valueObj)) + { + try + { + value = (T)valueObj; + return true; + } + catch (InvalidCastException) { } // Key exists, but cast failed. Let this fall through to the default return value. + } + + value = default(T); + return false; + } + + // NotNull because Plugins attached to custom Actions recieve ALL input parameters, even if they were not included in the original Action context + public static bool TryGetValueNotNull(this ParameterCollection parameterCollection, string key, out T value) + { + object valueObj; + if (parameterCollection.TryGetValue(key, out valueObj)) + { + if (valueObj != null) + { + try + { + value = (T)valueObj; + return true; + } + catch (InvalidCastException) { } // Key exists, but cast failed. Let this fall through to the default return value. + } + } + + value = default(T); + return false; + } + + // NotNull because Plugins attached to custom Actions recieve ALL input parameters, even if they were not included in the original Action context + public static bool ContainsNotNull(this ParameterCollection parameterCollection, string key) + { + return parameterCollection.Contains(key) && parameterCollection[key] != null; + } + + // Parse JSON string to object - CRM Online compatible + public static T ParseJSON(this string jsonString, bool useSimpleDictionaryFormat = true) + { + try + { + DataContractJsonSerializer JsonDeserializer = new DataContractJsonSerializer(typeof(T)); + using (MemoryStream stream = new MemoryStream(Encoding.UTF8.GetBytes(jsonString))) + { + return (T)JsonDeserializer.ReadObject(stream); + } + } + catch + { + throw new InvalidDataContractException("JSON string is invalid, or could not be serialized to the specified type."); + } + } + + // Try Parse JSON string to object - CRM Online compatible + public static bool TryParseJSON(this string jsonString, out T obj, bool useSimpleDictionaryFormat = true) + { + try + { + obj = jsonString.ParseJSON(useSimpleDictionaryFormat); + return true; + } + catch + { + obj = default(T); + return false; + } + } + + // Convert object to JSON string - CRM Online compatible + public static string ToJSON(this object obj, bool useSimpleDictionaryFormat = true) + { + DataContractJsonSerializer JsonSerializer = new DataContractJsonSerializer(obj.GetType()); + using (MemoryStream stream = new MemoryStream()) + { + JsonSerializer.WriteObject(stream, obj); + return Encoding.UTF8.GetString(stream.ToArray()); + } + } + + // A slightly easier way to retreive all columns + public static Entity Retrieve(this IOrganizationService service, string entityName, Guid entityId, bool allColumns) + { + return service.Retrieve(entityName, entityId, new ColumnSet(allColumns)); + } + + // Easily convert Guid to EntityReference + public static EntityReference ToEntityReference(this Guid id, string entityType) + { + return new EntityReference(entityType, id); + } + + // Easily convert integer to OptionSetValue + public static OptionSetValue ToOptionSetValue(this int value) + { + return new OptionSetValue(value); + } + + // This is how the OOB GetService method should have been... + public static T GetService(this IServiceProvider serviceProvider) + { + return (T)serviceProvider.GetService(typeof(T)); + } + } +} diff --git a/AutoNumber-Old/CeledonPlugin.cs b/AutoNumber-Old/CeledonPlugin.cs new file mode 100644 index 0000000..f3b9ca3 --- /dev/null +++ b/AutoNumber-Old/CeledonPlugin.cs @@ -0,0 +1,409 @@ +// Author: Matt Barnes (matt.barnes@celedonpartners.com) +/*The MIT License (MIT) + +Copyright (c) 2015 Celedon Partners + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +// Implements the Plugin Workflow Activity. + +using System; +using System.Linq; +using System.ServiceModel; +using System.Globalization; +using System.Collections.ObjectModel; +using Microsoft.Xrm.Sdk; +using Microsoft.Xrm.Sdk.Client; +using Microsoft.Xrm.Sdk.Query; + +namespace Celedon +{ + /// + /// Base class for all Plugins. + /// + /// Modified by Matt Barnes - "I've made a few special modifications myself" + /// + /// - Added the method RegisterEvent(), to simplify each child plugin constructor + /// - Removed the base class constructor with the child class argument (it wasn't needed) + /// - LocalPluginContext class now includes an OrganizationServiceContext object, so it doesn't need to be initialized within each plugin + /// - LocalPluginContext class now implements IDisposable, so the OrganizationServiceContext object gets properly disposed when it is done + /// - Added the Execution Stage constants + /// - Added GetInputParameters() method for early binding of InputParameters + /// - Added GetOutputParameters() method for early binding of OutputParameters + /// - Added PreImage and PostImage - returns the first available image (assumes there is only one) if you have multiple, then retrieve them normally + /// + /// + public class CeledonPlugin : IPlugin + { + public const int PREVALIDATION = 10; + public const int PREOPERATION = 20; + public const int POSTOPERATION = 40; + + public const string CREATEMESSAGE = "Create"; + public const string RETRIEVEMESSAGE = "Retrieve"; + public const string UPDATEMESSAGE = "Update"; + public const string DELETEMESSAGE = "Delete"; + public const string RETRIEVEMULTIPLEMESSAGE = "RetrieveMultiple"; + public const string ASSOCIATEMESSAGE = "Associate"; + public const string DISASSOCIATEMESSAGE = "Disassociate"; + public const string SETSTATEMESSAGE = "SetState"; + + protected class LocalPluginContext : IDisposable + { + internal IServiceProvider ServiceProvider + { + get; + + private set; + } + + internal IOrganizationService OrganizationService + { + get; + + private set; + } + + internal OrganizationServiceContext OrganizationDataContext + { + get; + + private set; + } + + internal IPluginExecutionContext PluginExecutionContext + { + get; + + private set; + } + + internal ITracingService TracingService + { + get; + + private set; + } + + private LocalPluginContext() { } + + internal LocalPluginContext(IServiceProvider serviceProvider) + { + if (serviceProvider == null) + { + throw new ArgumentNullException("serviceProvider"); + } + + // Obtain the execution context service from the service provider. + this.PluginExecutionContext = (IPluginExecutionContext)serviceProvider.GetService(typeof(IPluginExecutionContext)); + + // Obtain the tracing service from the service provider. + this.TracingService = (ITracingService)serviceProvider.GetService(typeof(ITracingService)); + + // Obtain the Organization Service factory service from the service provider + IOrganizationServiceFactory factory = (IOrganizationServiceFactory)serviceProvider.GetService(typeof(IOrganizationServiceFactory)); + + // Use the factory to generate the Organization Service. + this.OrganizationService = factory.CreateOrganizationService(this.PluginExecutionContext.UserId); + + // Generate the Organization Data Context + this.OrganizationDataContext = new OrganizationServiceContext(this.OrganizationService); + } + + internal Entity PreImage + { + get { try { return this.PluginExecutionContext.PreEntityImages.Values.First(); } catch { throw new InvalidPluginExecutionException("Pre Image Not Found"); } } + } + + internal Entity PostImage + { + get { try { return this.PluginExecutionContext.PostEntityImages.Values.First(); } catch { throw new InvalidPluginExecutionException("Post Image Not Found"); } } + } + + internal T GetInputParameters() where T : class, ICrmRequest + { + switch (this.PluginExecutionContext.MessageName) + { + case CREATEMESSAGE: + return new CreateInputParameters() { Target = this.PluginExecutionContext.InputParameters["Target"] as Entity } as T; + case UPDATEMESSAGE: + return new UpdateInputParameters() { Target = this.PluginExecutionContext.InputParameters["Target"] as Entity } as T; + case DELETEMESSAGE: + return new DeleteInputParameters() { Target = this.PluginExecutionContext.InputParameters["Target"] as EntityReference } as T; + case RETRIEVEMESSAGE: + return new RetrieveInputParameters() { Target = this.PluginExecutionContext.InputParameters["Target"] as EntityReference, ColumnSet = this.PluginExecutionContext.InputParameters["ColumnSet"] as ColumnSet, RelatedEntitiesQuery = this.PluginExecutionContext.InputParameters["RelatedEntitiesQuery"] as RelationshipQueryCollection } as T; + case RETRIEVEMULTIPLEMESSAGE: + return new RetrieveMultipleInputParameters() { Query = this.PluginExecutionContext.InputParameters["Query"] as QueryBase } as T; + case ASSOCIATEMESSAGE: + return new AssociateInputParameters() { Target = this.PluginExecutionContext.InputParameters["Target"] as EntityReference, Relationship = this.PluginExecutionContext.InputParameters["Relationship"] as Relationship, RelatedEntities = this.PluginExecutionContext.InputParameters["RelatedEntities"] as EntityReferenceCollection } as T; + case DISASSOCIATEMESSAGE: + return new DisassociateInputParameters() { Target = this.PluginExecutionContext.InputParameters["Target"] as EntityReference, Relationship = this.PluginExecutionContext.InputParameters["Relationship"] as Relationship, RelatedEntities = this.PluginExecutionContext.InputParameters["RelatedEntities"] as EntityReferenceCollection } as T; + case SETSTATEMESSAGE: + return new SetStateInputParameters() { EntityMoniker = this.PluginExecutionContext.InputParameters["EntityMoniker"] as EntityReference, State = this.PluginExecutionContext.InputParameters["State"] as OptionSetValue, Status = this.PluginExecutionContext.InputParameters["Status"] as OptionSetValue } as T; + default: + return default(T); + } + } + + internal T GetOutputParameters() where T : class, ICrmResponse + { + if (this.PluginExecutionContext.Stage < POSTOPERATION) + { + throw new InvalidOperationException("OutputParameters only exist during Post-Operation stage."); + } + + switch (this.PluginExecutionContext.MessageName) + { + case CREATEMESSAGE: + return new CreateOutputParameters() { Id = (Guid)this.PluginExecutionContext.OutputParameters["Id"] } as T; + case RETRIEVEMESSAGE: + return new RetrieveOutputParameters() { Entity = this.PluginExecutionContext.OutputParameters["Entity"] as Entity } as T; + case RETRIEVEMULTIPLEMESSAGE: + return new RetrieveMultipleOutputParameters() { EntityCollection = this.PluginExecutionContext.OutputParameters["BusinessEntityCollection"] as EntityCollection } as T; + default: + return default(T); + } + } + + internal void Trace(string message) + { + if (string.IsNullOrWhiteSpace(message) || this.TracingService == null) + { + return; + } + + if (this.PluginExecutionContext == null) + { + this.TracingService.Trace(message); + } + else + { + this.TracingService.Trace("{0} : (Correlation Id: {1}, Initiating User: {2})", message, this.PluginExecutionContext.CorrelationId, this.PluginExecutionContext.InitiatingUserId); + } + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + if (OrganizationDataContext != null) + { + OrganizationDataContext.Dispose(); + OrganizationDataContext = null; + } + } + } + } + + private Collection>> registeredEvents; + + /// + /// Gets the List of events that the plug-in should fire for. Each List + /// Item is a containing the Pipeline Stage, Message and (optionally) the Primary Entity. + /// In addition, the fourth parameter provide the delegate to invoke on a matching registration. + /// + protected Collection>> RegisteredEvents + { + get + { + if (this.registeredEvents == null) + { + this.registeredEvents = new Collection>>(); + } + + return this.registeredEvents; + } + } + + protected void RegisterEvent(int Stage, string EventName, string EntityName, Action ExecuteMethod) + { + this.RegisteredEvents.Add(new Tuple>(Stage, EventName, EntityName, ExecuteMethod)); + } + + protected delegate void TraceDelegate(string message); + protected TraceDelegate Trace; + + /// + /// Gets or sets the name of the child class. + /// + /// The name of the child class. + protected string ChildClassName + { + get { return this.GetType().Name; } + + //private set; + } + + /// + /// Initializes a new instance of the class. + /// + /// The of the derived class. + //internal Plugin(Type childClassName) + //{ + // this.ChildClassName = childClassName.ToString(); + //} + + /// + /// Executes the plug-in. + /// + /// The service provider. + /// + /// For improved performance, Microsoft Dynamics CRM caches plug-in instances. + /// The plug-in's Execute method should be written to be stateless as the constructor + /// is not called for every invocation of the plug-in. Also, multiple system threads + /// could execute the plug-in at the same time. All per invocation state information + /// is stored in the context. This means that you should not use global variables in plug-ins. + /// + public void Execute(IServiceProvider serviceProvider) + { + if (serviceProvider == null) + { + throw new ArgumentNullException("serviceProvider"); + } + + // Construct the Local plug-in context. + using (LocalPluginContext localcontext = new LocalPluginContext(serviceProvider)) + { + localcontext.Trace(String.Format(CultureInfo.InvariantCulture, "Entered {0}.Execute()", this.ChildClassName)); + + try + { + Trace = m => localcontext.Trace(m); + // Iterate over all of the expected registered events to ensure that the plugin + // has been invoked by an expected event + // For any given plug-in event at an instance in time, we would expect at most 1 result to match. + Action entityAction = + (from a in this.RegisteredEvents + where ( + a.Item1 == localcontext.PluginExecutionContext.Stage && + a.Item2 == localcontext.PluginExecutionContext.MessageName && + (String.IsNullOrWhiteSpace(a.Item3) ? true : a.Item3 == localcontext.PluginExecutionContext.PrimaryEntityName) + ) + select a.Item4).FirstOrDefault(); + + if (entityAction != null) + { + localcontext.Trace(String.Format(CultureInfo.InvariantCulture, "{0} is firing for Entity: {1}, Message: {2}", this.ChildClassName, localcontext.PluginExecutionContext.PrimaryEntityName, localcontext.PluginExecutionContext.MessageName)); + + entityAction.Invoke(localcontext); + + // now exit - if the derived plug-in has incorrectly registered overlapping event registrations, + // guard against multiple executions. + return; + } + } + catch (FaultException e) + { + localcontext.Trace(String.Format(CultureInfo.InvariantCulture, "Exception: {0}", e.ToString())); + + // Handle the exception. + throw; + } + finally + { + localcontext.Trace(String.Format(CultureInfo.InvariantCulture, "Exiting {0}.Execute()", this.ChildClassName)); + } + } + } + + internal interface ICrmRequest { } + + internal class CreateInputParameters : ICrmRequest + { + internal Entity Target; + } + + internal class UpdateInputParameters : ICrmRequest + { + internal Entity Target; + } + + internal class DeleteInputParameters : ICrmRequest + { + internal EntityReference Target; + } + + internal class RetrieveInputParameters : ICrmRequest + { + internal EntityReference Target; + internal ColumnSet ColumnSet; + internal RelationshipQueryCollection RelatedEntitiesQuery; + } + + internal class RetrieveMultipleInputParameters : ICrmRequest + { + internal QueryBase Query; + } + + internal class AssociateInputParameters : ICrmRequest + { + internal EntityReference Target; + internal Relationship Relationship; + internal EntityReferenceCollection RelatedEntities; + } + + internal class DisassociateInputParameters : ICrmRequest + { + internal EntityReference Target; + internal Relationship Relationship; + internal EntityReferenceCollection RelatedEntities; + } + + internal class SetStateInputParameters : ICrmRequest + { + internal EntityReference EntityMoniker; + internal OptionSetValue State; + internal OptionSetValue Status; + } + + internal interface ICrmResponse { } + + internal class CreateOutputParameters : ICrmResponse + { + internal Guid Id; + } + + internal class UpdateOutputParameters : ICrmResponse { } + + internal class DeleteOutputParameters : ICrmResponse { } + + internal class RetrieveOutputParameters : ICrmResponse + { + internal Entity Entity; + } + + internal class RetrieveMultipleOutputParameters : ICrmResponse + { + internal EntityCollection EntityCollection; + } + + internal class AssociateOutputParameters : ICrmResponse { } + + internal class DisassociateOutputParameters : ICrmResponse { } + + internal class SetStateOutputParameters : ICrmResponse { } + } +} \ No newline at end of file diff --git a/AutoNumber-Old/CreateAutoNumber.cs b/AutoNumber-Old/CreateAutoNumber.cs new file mode 100644 index 0000000..84ac2fe --- /dev/null +++ b/AutoNumber-Old/CreateAutoNumber.cs @@ -0,0 +1,119 @@ +// Author: Matt Barnes (matt.barnes@celedonpartners.com) +/*The MIT License (MIT) + +Copyright (c) 2015 Celedon Partners + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +// Generates a plugin step for an entity, when a new autonumber record is created + +using System; +using System.Linq; +using System.Collections.Generic; +using Microsoft.Xrm.Sdk; + +namespace Celedon +{ + public class CreateAutoNumber : CeledonPlugin + { + // + // This plugin is executed when a new AutoNumber record is created. It generates the plugin steps on the entity type to create each number + // + // Registration Details: + // Message: Create + // Primary Entity: cel_autonumber + // User Context: SYSTEM + // Event Pipeline: Post + // Mode: Async + // Config: none + // + + internal const string PLUGIN_NAME = "CeledonPartners.AutoNumber.{0}"; + + public CreateAutoNumber() + { + RegisterEvent(POSTOPERATION, CREATEMESSAGE, "cel_autonumber", Execute); + } + + protected void Execute(LocalPluginContext Context) + { + Trace("Get Target record"); + Entity Target = Context.GetInputParameters().Target; + string pluginName = String.Format(PLUGIN_NAME, Target.GetAttributeValue("cel_entityname")); + + if (Target.GetAttributeValue("cel_triggerevent").Value == 1) + { + pluginName += " Update"; + } + + Trace("Check for existing plugin step"); + if (Context.OrganizationDataContext.CreateQuery("sdkmessageprocessingstep").Where(s => s.GetAttributeValue("name").Equals(pluginName)).ToList().Any()) + { + return; // Step already exists, nothing to do here. + } + + Trace("Build the configuration"); + AutoNumberPluginConfig config = new AutoNumberPluginConfig() + { + EntityName = Target.GetAttributeValue("cel_entityname"), + EventName = Target.GetAttributeValue("cel_triggerevent").Value == 1 ? "Update" : "Create" + }; + + Trace("Get the Id of this plugin"); + Guid PluginTypeId = Context.OrganizationDataContext.CreateQuery("plugintype") + .Where(s => s.GetAttributeValue("name").Equals("Celedon.GetNextAutoNumber")) + .Select(s => s.GetAttributeValue("plugintypeid")) + .First(); + + Trace("Get the message id from this org"); + Guid messageId = Context.OrganizationDataContext.CreateQuery("sdkmessage") + .Where(s => s.GetAttributeValue("name").Equals(config.EventName)) + .Select(s => s.GetAttributeValue("sdkmessageid")) + .First(); + + Trace("Get the filterId for for the specific entity from this org"); + Guid filterId = Context.OrganizationDataContext.CreateQuery("sdkmessagefilter") + .Where(s => s.GetAttributeValue("primaryobjecttypecode").Equals(config.EntityName) + && s.GetAttributeValue("sdkmessageid").Id.Equals(messageId)) + .Select(s => s.GetAttributeValue("sdkmessagefilterid")) + .First(); + + Trace("Build new plugin step"); + Entity newPluginStep = new Entity("sdkmessageprocessingstep") + { + Attributes = new AttributeCollection() + { + { "name", pluginName }, + { "description", pluginName }, + { "plugintypeid", PluginTypeId.ToEntityReference("plugintype") }, // This plugin type + { "sdkmessageid", messageId.ToEntityReference("sdkmessage") }, // Create or Update Message + { "configuration", config.ToJSON() }, // EntityName and RegisteredEvent in the UnsecureConfig + { "stage", PREOPERATION.ToOptionSetValue() }, // Execution Stage: Pre-Operation + { "rank", 1 }, + { "impersonatinguserid", Context.PluginExecutionContext.UserId.ToEntityReference("systemuser") }, // Run as SYSTEM user. Assumes we are currently running as the SYSTEM user + { "sdkmessagefilterid", filterId.ToEntityReference("sdkmessagefilter") }, + } + }; + + Trace("Create new plugin step"); + Guid pluginStepId = Context.OrganizationService.Create(newPluginStep); + } + } +} diff --git a/AutoNumber-Old/DeleteAutoNumber.cs b/AutoNumber-Old/DeleteAutoNumber.cs new file mode 100644 index 0000000..e0c5a7a --- /dev/null +++ b/AutoNumber-Old/DeleteAutoNumber.cs @@ -0,0 +1,94 @@ +// Author: Matt Barnes (matt.barnes@celedonpartners.com) +/*The MIT License (MIT) + +Copyright (c) 2015 Celedon Partners + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +// Removes the plugin step from an entity, if there are no registered autonumber records + +using System; +using System.Linq; +using System.Collections.Generic; + +using Microsoft.Xrm.Sdk; + +namespace Celedon +{ + public class DeleteAutoNumber : CeledonPlugin + { + // + // This plugin is executed when an AutoNumber record is deleted, it will remove the plugin steps from the associated entity + // + // Registration details: + // Message: Delete + // Primary Entity: cel_autonumber + // User Context: SYSTEM + // Event Pipeline: Post + // Mode: Async + // Config: none + // + // PreImage: + // Name: PreImage + // Alias: PreImage + // Attributes: cel_entityname, cel_attributename + // + public DeleteAutoNumber() + { + //this.RegisteredEvents.Add(new Tuple>(POSTOPERATION, DELETEMESSAGE, "entityname", new Action(Execute))); + RegisterEvent(POSTOPERATION, DELETEMESSAGE, "cel_autonumber", Execute); + } + + protected void Execute(LocalPluginContext Context) + { + int triggerEvent = Context.PreImage.Contains("cel_triggerevent") && Context.PreImage.GetAttributeValue("cel_triggerevent").Value == 1 ? 1 : 0; + + var remainingAutoNumberList = Context.OrganizationDataContext.CreateQuery("cel_autonumber") + .Where(s => s.GetAttributeValue("cel_entityname").Equals(Context.PreImage.GetAttributeValue("cel_entityname"))) + .Select(s => new { Id = s.GetAttributeValue("cel_autonumberid"), TriggerEvent = s.Contains("cel_triggerevent") ? s.GetAttributeValue("cel_triggerevent").Value : 0 }) + .ToList(); + + if (remainingAutoNumberList.Any(s => s.TriggerEvent == triggerEvent )) // If there are still other autonumber records on this entity, then do nothing. + { + return; + } + + // Find and remove the registerd plugin + string pluginName = String.Format(CreateAutoNumber.PLUGIN_NAME, Context.PreImage.GetAttributeValue("cel_entityname")); + if (Context.PreImage.Contains("cel_triggerevent") && Context.PreImage.GetAttributeValue("cel_triggerevent").Value == 1) + { + pluginName += " Update"; + } + + var pluginStepList = Context.OrganizationDataContext.CreateQuery("sdkmessageprocessingstep") + .Where(s => s.GetAttributeValue("name").Equals(pluginName)) + .Select(s => s.GetAttributeValue("sdkmessageprocessingstepid")) + .ToList(); + + if (!pluginStepList.Any()) // Plugin is already deleted, nothing to do here. + { + return; + } + + // Delete plugin step + Context.OrganizationService.Delete("sdkmessageprocessingstep", pluginStepList.First()); + } + } +} diff --git a/AutoNumber-Old/GetNextAutoNumber.cs b/AutoNumber-Old/GetNextAutoNumber.cs new file mode 100644 index 0000000..7705fb2 --- /dev/null +++ b/AutoNumber-Old/GetNextAutoNumber.cs @@ -0,0 +1,154 @@ +// Author: Matt Barnes (matt.barnes@celedonpartners.com) +/*The MIT License (MIT) + +Copyright (c) 2015 Celedon Partners + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +// Gets the next available number and adds it to the Target + +using System; +using System.Linq; +using Microsoft.Xrm.Sdk; +using Microsoft.Xrm.Sdk.Query; + +namespace Celedon +{ + public class getNextAutoNumber : CeledonPlugin + { + // + // This is the main plugin that creates the numbers and adds them to new records + // This plugin is not registered by default. It is registered and unregistered dynamically by the CreateAutoNumber and DeleteAutoNumber plugins respectively + // + + private AutoNumberPluginConfig config; + + public getNextAutoNumber(string pluginConfig, string secureConfig) + { + // Need to support older version + if (pluginConfig.TryParseJSON(out config)) + { + RegisterEvent(PREOPERATION, config.EventName, config.EntityName, Execute); + } + else + { + RegisterEvent(PREOPERATION, CREATEMESSAGE, pluginConfig, Execute); + } + + + } + + protected void Execute(LocalPluginContext context) + { + #region Get the list of autonumber records applicable to the Target entity type + int triggerEvent = context.PluginExecutionContext.MessageName == "Update" ? 1 : 0; + var autoNumberIdList = context.OrganizationDataContext.CreateQuery("cel_autonumber") + .Where(a => a.GetAttributeValue("cel_entityname").Equals(context.PluginExecutionContext.PrimaryEntityName) && a.GetAttributeValue("statecode").Value == 0 && a.GetAttributeValue("cel_triggerevent").Value == triggerEvent) + .OrderBy(a => a.GetAttributeValue("cel_autonumberid")) // Insure they are ordered, to prevent deadlocks + .Select(a => a.GetAttributeValue("cel_autonumberid")); + #endregion + + #region This loop locks the autonumber record(s) so only THIS transaction can read/write it + foreach (Guid autoNumberId in autoNumberIdList) + { + Entity lockingUpdate = new Entity("cel_autonumber"); + lockingUpdate.Id = autoNumberId; + lockingUpdate["cel_preview"] = "555"; // Use the preview field as our "dummy" field - so we don't need a dedicated "dummy" + + context.OrganizationService.Update(lockingUpdate); + } + #endregion + + #region This loop populates the Target record, and updates the autonumber record(s) + Entity Target = context.PluginExecutionContext.InputParameters["Target"] as Entity; + + foreach (Guid autoNumberId in autoNumberIdList) + { + Entity autoNumber = context.OrganizationService.Retrieve("cel_autonumber", autoNumberId, true); + string targetAttribute = autoNumber.GetAttributeValue("cel_attributename"); + + #region Check conditions that prevent creating an autonumber + if (context.PluginExecutionContext.MessageName == "Update" && !Target.Contains(autoNumber.GetAttributeValue("cel_triggerattribute"))) + { + continue; // Continue, if this is an Update event and the Target does not contain the trigger value + } + else if ((autoNumber.Contains("cel_conditionaloptionset") && (!Target.Contains(autoNumber.GetAttributeValue("cel_conditionaloptionset")) || Target.GetAttributeValue(autoNumber.GetAttributeValue("cel_conditionaloptionset")).Value != autoNumber.GetAttributeValue("cel_conditionalvalue")))) + { + continue; // Continue, if this is a conditional optionset + } + else if (Target.Contains(targetAttribute) && !String.IsNullOrWhiteSpace(Target.GetAttributeValue(targetAttribute))) + { + continue; // Continue, so we don't overwrite an existing value + } + #endregion + + #region Create the AutoNumber + int numDigits = autoNumber.GetAttributeValue("cel_digits"); + + // Generate number and insert into Target Record + Target[targetAttribute] = String.Format("{0}{1}{2}", ReplaceParameters(autoNumber.GetAttributeValue("cel_prefix"), Target, context.OrganizationService), + numDigits == 0 ? "" : autoNumber.GetAttributeValue("cel_nextnumber").ToString("D" + numDigits), + ReplaceParameters(autoNumber.GetAttributeValue("cel_suffix"), Target, context.OrganizationService)); + + // Increment next number in db + Entity updatedAutoNumber = new Entity("cel_autonumber"); + updatedAutoNumber.Id = autoNumber.Id; + updatedAutoNumber["cel_nextnumber"] = autoNumber.GetAttributeValue("cel_nextnumber") + 1; + updatedAutoNumber["cel_preview"] = Target[targetAttribute]; // fix the preview + + context.OrganizationService.Update(updatedAutoNumber); + #endregion + } + #endregion + } + + #region Process Runtime Parameters, if any + private string ReplaceParameters(string text, Entity Target, IOrganizationService Service) + { + if (String.IsNullOrWhiteSpace(text)) + { + return ""; + } + + foreach (RuntimeParameter param in RuntimeParameter.GetParametersFromString(text)) + { + if (!param.IsParentParameter()) + { + text = text.Replace(param.ParameterText, param.GetParameterValue(Target)); + } + else + { + if (Target.Contains(param.ParentLookupName)) + { + var parentRecord = Service.Retrieve(Target.GetAttributeValue(param.ParentLookupName).LogicalName, Target.GetAttributeValue(param.ParentLookupName).Id, new ColumnSet(param.AttributeName)); + text = text.Replace(param.ParameterText, param.GetParameterValue(parentRecord)); + } + else // Target record has no parent, so use default value + { + text = text.Replace(param.ParameterText, param.DefaultValue); + } + } + } + + return text; + } + #endregion + } +} diff --git a/AutoNumber-Old/Properties/AssemblyInfo.cs b/AutoNumber-Old/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..c58809e --- /dev/null +++ b/AutoNumber-Old/Properties/AssemblyInfo.cs @@ -0,0 +1,38 @@ +using System; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("AutoNumber")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("AutoNumber")] +[assembly: AssemblyCopyright("Copyright © 2015")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] +[assembly: CLSCompliant(true)] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("4dc971d5-7361-46ff-9df8-f6571ce6fc4c")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/AutoNumber-Old/RuntimeParameter.cs b/AutoNumber-Old/RuntimeParameter.cs new file mode 100644 index 0000000..df313ec --- /dev/null +++ b/AutoNumber-Old/RuntimeParameter.cs @@ -0,0 +1,436 @@ +// Author: Matt Barnes (matt.barnes@celedonpartners.com) +/*The MIT License (MIT) + +Copyright (c) 2015 Celedon Partners + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +// Class for storing and processing Runtime Parameters in the Autonumber configuration + +using System; +using System.Linq; +using System.Collections.Generic; +using System.Text.RegularExpressions; +using Microsoft.Xrm.Sdk; + +namespace Celedon +{ + public class RuntimeParameter + { + public string ParameterText { get; private set; } + public string AttributeName { get; private set; } + public string ParentLookupName { get; private set; } + public string DefaultValue { get; private set; } + public string StringFormatter { get; private set; } + public ConditionalFormatter Conditional { get; private set; } + + private RuntimeParameter() : this("", "", "", "", "", new ConditionalFormatter()) { } + public RuntimeParameter(string paramText, string attributeText, string parentLookup, string defaultValue, string stringFormat, ConditionalFormatter condition) + { + ParameterText = paramText; + AttributeName = attributeText; + ParentLookupName = parentLookup; + DefaultValue = defaultValue; + StringFormatter = stringFormat; + Conditional = condition; + } + + public static RuntimeParameter Parse(string input) + { + RuntimeParameter rp = new RuntimeParameter(); + rp.ParameterText = input; + rp.AttributeName = input.Trim('{', '}'); + + if (rp.AttributeName.Contains(':')) + { + string[] paramList = rp.AttributeName.Split(':'); + rp.AttributeName = paramList[0]; + + if (rp.AttributeName == "rand") + { + rp.StringFormatter = paramList[1]; + } + else + { + if (paramList[1].Contains('?')) + { + rp.Conditional = ConditionalFormatter.Parse(paramList[1]); + } + else if (paramList.Length > 2) + { + rp.StringFormatter = paramList[1]; + rp.Conditional = ConditionalFormatter.Parse(paramList[2]); + } + else + { + rp.StringFormatter = paramList[1]; + } + } + } + + if (rp.AttributeName.Contains('|')) + { + rp.DefaultValue = rp.AttributeName.Split('|')[1]; + rp.AttributeName = rp.AttributeName.Split('|')[0]; + } + + if (rp.AttributeName.Contains('.')) + { + rp.ParentLookupName = rp.AttributeName.Split('.')[0]; + rp.AttributeName = rp.AttributeName.Split('.')[1]; + } + + return rp; + } + + public static IEnumerable GetParametersFromString(string text) + { + foreach (string p in Regex.Matches(text, @"{(.*?)}").OfType().Select(m => m.Groups[0].Value).Distinct()) + { + yield return Parse(p); + } + } + + public string GetParameterValue(Entity Target) + { + if (Target.Contains(AttributeName)) + { + if (Target[AttributeName] is EntityReference) + { + // Lookup condition is based on GUID + return Conditional.HasCondition ? Conditional.GetResult(Target.GetAttributeValue(AttributeName).Id) : Target.GetAttributeValue(AttributeName).Name; + } + else if (Target[AttributeName] is OptionSetValue) + { + // Conditional OptionSetValue is based on the integer value + return Conditional.HasCondition ? Conditional.GetResult(Target.GetAttributeValue(AttributeName).Value.ToString()) : Target.FormattedValues[AttributeName]; + } + else if (Target[AttributeName] is bool) + { + // Note: Boolean values ignore the match value, they just use the attribute value itself as the condition + return Conditional.HasCondition ? Conditional.GetResult(Target.GetAttributeValue(AttributeName)) : Target.FormattedValues[AttributeName]; + } + else if (Target[AttributeName] is DateTime) + { + // If there is a format AND a condition, apply formatting first, then evaluate condition as a string + // If there is a condition without any format, evaluate condition as DateTime + return String.IsNullOrEmpty(StringFormatter) ? Conditional.GetResult(Target.GetAttributeValue(AttributeName)) : Conditional.GetResult(Target.GetAttributeValue(AttributeName).ToString(StringFormatter)); + } + else if (Target[AttributeName] is Money) + { + return Conditional.HasCondition ? Conditional.GetResult(Target.GetAttributeValue(AttributeName).Value) : Target.GetAttributeValue(AttributeName).Value.ToString(StringFormatter); + } + else if (Target[AttributeName] is int) + { + return Conditional.HasCondition ? Conditional.GetResult(Target.GetAttributeValue(AttributeName)) : Target.GetAttributeValue(AttributeName).ToString(StringFormatter); + } + else if (Target[AttributeName] is decimal) + { + return Conditional.HasCondition ? Conditional.GetResult(Target.GetAttributeValue(AttributeName)) : Target.GetAttributeValue(AttributeName).ToString(StringFormatter); + } + else if (Target[AttributeName] is double) + { + return Conditional.HasCondition ? Conditional.GetResult(Target.GetAttributeValue(AttributeName)) : Target.GetAttributeValue(AttributeName).ToString(StringFormatter); + } + else if (Target[AttributeName] is string) + { + return Conditional.GetResult(Target[AttributeName].ToString()); + } + } + else if (AttributeName.Equals("rand")) + { + string length = ""; + string stringStyle = "upper"; + int stringLength = 5; // Seems like reasonable default + + if (StringFormatter.Contains('?')) + { + length = StringFormatter.Split('?')[0]; + stringStyle = StringFormatter.Split('?')[1].ToLower(); + } + else + { + length = StringFormatter; + } + + if (!Int32.TryParse(length, out stringLength)) + { + stringLength = 5; + } + + string stringValues = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; + if (stringStyle == "mix") + { + stringValues = stringValues + stringValues.ToLower(); + } + else if (stringStyle == "lower") + { + stringValues = stringValues.ToLower(); + } + + Random rnd = new Random(); + return String.Join("", Enumerable.Range(0, stringLength).Select(n => stringValues[rnd.Next(stringValues.Length)])); + } + + return DefaultValue; + } + + public bool IsParentParameter() + { + return !String.IsNullOrEmpty(ParentLookupName); + } + + public bool IsRandomParameter() + { + return AttributeName.Equals("rand"); + } + + public class ConditionalFormatter + { + private char Operator; + private ConditionalFormatter FalseCondition = null; + public string MatchValue { get; private set; } + public string TrueValue { get; private set; } + public string FalseValue { get; private set; } + + public ConditionalFormatter() : this("", "", "") { } + public ConditionalFormatter(string matchValue, string trueValue, string falseValue) + { + Operator = matchValue.StartsWith(">") || matchValue.StartsWith("<") ? matchValue[0] : '='; + MatchValue = matchValue.TrimStart('>', '<'); + TrueValue = trueValue; + FalseValue = falseValue; + + if (falseValue.Contains('?')) // if any nested conditions + { + string[] condition1 = falseValue.Split(new char[] { '?' }, 2); + string[] condition2 = condition1[1].Split(new char[] { '|' }, 2); + FalseCondition = new ConditionalFormatter(condition1[0], condition2[0], condition2[1]); + } + } + + public static ConditionalFormatter Parse(string conditionalString) + { + string[] condition1 = conditionalString.Split(new char[] { '?' }, 2); + string[] condition2 = condition1[1].Split(new char[] { '|' }, 2); + + return new ConditionalFormatter(condition1[0], condition2[0], condition2[1]); + } + + public bool HasCondition + { + get { return !String.IsNullOrEmpty(MatchValue); } + } + + public bool IsRecursive + { + get { return FalseCondition != null; } + } + + public string GetResult(string inputText) + { + if (!this.HasCondition) + { + return inputText; + } + + if (this.IsRecursive) + { + return inputText == MatchValue ? TrueValue : FalseCondition.GetResult(inputText); + } + else + { + return inputText == MatchValue ? TrueValue : FalseValue; + } + } + + public string GetResult(Guid inputGuid) + { + if (!this.HasCondition) + { + return inputGuid.ToString(); + } + + if (this.IsRecursive) + { + return CompareGuid(inputGuid) ? TrueValue : FalseCondition.GetResult(inputGuid); + } + else + { + return CompareGuid(inputGuid) ? TrueValue : FalseValue; + } + } + + public string GetResult(int inputInt) + { + if (!this.HasCondition) + { + return inputInt.ToString(); + } + + if (this.IsRecursive) + { + return CompareNumeric(inputInt) ? TrueValue : FalseCondition.GetResult(inputInt); + } + else + { + return CompareNumeric(inputInt) ? TrueValue : FalseValue; + } + } + + public string GetResult(double inputDouble) + { + if (!this.HasCondition) + { + return inputDouble.ToString(); + } + + if (this.IsRecursive) + { + return CompareNumeric((decimal)inputDouble) ? TrueValue : FalseCondition.GetResult(inputDouble); + } + else + { + return CompareNumeric((decimal)inputDouble) ? TrueValue : FalseValue; + } + } + + public string GetResult(decimal inputDecimal) + { + if (!this.HasCondition) + { + return inputDecimal.ToString(); + } + + if (this.IsRecursive) + { + return CompareNumeric((decimal)inputDecimal) ? TrueValue : FalseCondition.GetResult(inputDecimal); + } + else + { + return CompareNumeric((decimal)inputDecimal) ? TrueValue : FalseValue; + } + } + + public string GetResult(DateTime inputDate) + { + if (!this.HasCondition) + { + return inputDate.ToString(); + } + + if (this.IsRecursive) + { + return CompareDateTime(inputDate) ? TrueValue : FalseCondition.GetResult(inputDate); + } + else + { + return CompareDateTime(inputDate) ? TrueValue : FalseValue; + } + } + + private bool CompareGuid(Guid id) + { + Guid matchGuid; + if (Guid.TryParse(MatchValue, out matchGuid)) + { + return id.Equals(matchGuid); + } + else + { + return id.ToString().Equals(MatchValue); + } + } + + private bool CompareDateTime(DateTime date) + { + DateTime matchDate; + if (DateTime.TryParse(MatchValue, out matchDate)) + { + switch (Operator) + { + case '>': + return date > matchDate; + case '<': + return date < matchDate; + default: + return date == matchDate; + } + } + else + { + return date.ToShortDateString() == MatchValue; + } + } + + private bool CompareNumeric(decimal number) + { + decimal matchNumber; + if (Decimal.TryParse(MatchValue, out matchNumber)) + { + switch (Operator) + { + case '>': + return number > matchNumber; + case '<': + return number < matchNumber; + default: + return number == matchNumber; + } + } + else + { + return number.ToString() == MatchValue; + } + } + + public string GetResult(bool value) + { + // Boolean only has 2 possible values, so it doesn't support recursive conditions + return value ? TrueValue : FalseValue; + } + + public override bool Equals(Object obj) + { + return obj is ConditionalFormatter && this == (ConditionalFormatter)obj; + } + public override int GetHashCode() + { + return MatchValue.GetHashCode() ^ TrueValue.GetHashCode() ^ FalseValue.GetHashCode(); + } + public static bool operator ==(ConditionalFormatter x, ConditionalFormatter y) + { + if (Object.ReferenceEquals(null, x) && Object.ReferenceEquals(null, y)) + { + return true; + } + else if (Object.ReferenceEquals(null, x) || Object.ReferenceEquals(null, y)) + { + return false; + } + return x.MatchValue == y.MatchValue && x.TrueValue == y.TrueValue && x.FalseValue == y.FalseValue && x.FalseCondition == y.FalseCondition; + } + public static bool operator !=(ConditionalFormatter x, ConditionalFormatter y) + { + return !(x == y); + } + } + } +} diff --git a/AutoNumber-Old/ValidateAutoNumber.cs b/AutoNumber-Old/ValidateAutoNumber.cs new file mode 100644 index 0000000..07939e3 --- /dev/null +++ b/AutoNumber-Old/ValidateAutoNumber.cs @@ -0,0 +1,243 @@ +// Author: Matt Barnes (matt.barnes@celedonpartners.com) +/*The MIT License (MIT) + +Copyright (c) 2015 Celedon Partners + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +// Validation on pre-create of a new AutoNumber record + +#define VALIDATEPARAMETERS // temporarily disable this, until it can be made to work +#define DUPLICATECHECK + +using System; +using System.Linq; +using System.Collections.Generic; + +using Microsoft.Xrm.Sdk; +using Microsoft.Xrm.Sdk.Messages; +using Microsoft.Xrm.Sdk.Metadata; +using System.Text.RegularExpressions; + +namespace Celedon +{ + public class ValidateAutoNumber : CeledonPlugin + { + // + // This Plugin will validate the details of a new AutoNumber record before it is created + // + // Registration Details: + // Message: Create + // Primary Entity: cel_autonumber + // User Context: SYSTEM + // Event Pipeline: PreValidation + // Mode: Sync + // Config: none + // + private LocalPluginContext Context; + private Dictionary> EntityMetadata; + private Dictionary AttributeMetadata; + + public ValidateAutoNumber(string unsecureConfig, string secureConfig) + { + EntityMetadata = new Dictionary>(); + AttributeMetadata = new Dictionary(); + + RegisterEvent(PREVALIDATION, CREATEMESSAGE, "cel_autonumber", Execute); + } + + protected void Execute(LocalPluginContext context) + { + Context = context; + + Trace("Getting Target entity"); + Entity Target = Context.GetInputParameters().Target; + Trace("Validate the Entity name"); + Trace("Get Attribute List"); + List attributeList = GetEntityMetadata(Target.GetAttributeValue("cel_entityname")); + + Trace("Validate the Attribute name"); + if (!attributeList.Select(a => a.LogicalName).Contains(Target.GetAttributeValue("cel_attributename"))) + { + throw new InvalidPluginExecutionException("Specified Attribute does not exist."); + } + + Trace("Validate the Trigger Attribute (if any)"); + if (!String.IsNullOrEmpty(Target.GetAttributeValue("cel_triggerattribute")) && !attributeList.Select(a => a.LogicalName).Contains(Target.GetAttributeValue("cel_triggerattribute"))) + { + throw new InvalidPluginExecutionException("Specified Trigger Attribute does not exist."); + } + + Trace("Validate the Attribute type"); + if (attributeList.Single(a => a.LogicalName.Equals(Target.GetAttributeValue("cel_attributename"))).AttributeType != AttributeTypeCode.String && attributeList.Single(a => a.LogicalName.Equals(Target.GetAttributeValue("cel_attributename"))).AttributeType != AttributeTypeCode.Memo) + { + throw new InvalidPluginExecutionException("Attribute must be a text field."); + } + + #region test parameters +#if VALIDATEPARAMETERS + Dictionary fields = new Dictionary() { { "cel_prefix", "Prefix" }, { "cel_suffix", "Suffix" } }; + + foreach (string field in fields.Keys) + { + if (Target.Contains(field) && Target.GetAttributeValue(field).Contains('{')) + { + if (Target.GetAttributeValue(field).Count(c => c.Equals('{')) != Target.GetAttributeValue(field).Count(c => c.Equals('}'))) + { + throw new InvalidPluginExecutionException(String.Format("Invalid parameter formatting in {0}", fields[field])); + } + + foreach (string p in Regex.Matches(Target.GetAttributeValue(field), @"{(.*?)}").OfType().Select(m => m.Groups[0].Value).Distinct()) + { + if (p.Substring(1).Contains('{')) + { + throw new InvalidPluginExecutionException(String.Format("Invalid parameter formatting in {0}", fields[field])); + } + } + + try + { + foreach (RuntimeParameter param in RuntimeParameter.GetParametersFromString(Target.GetAttributeValue(field))) + { + if (!param.IsParentParameter()) + { + if (!attributeList.Select(a => a.LogicalName).Contains(param.AttributeName)) + { + throw new InvalidPluginExecutionException(String.Format("{0} is not a valid attribute name in {1} value", param.AttributeName, fields[field])); + } + } + else + { + if (!attributeList.Select(a => a.LogicalName).Contains(param.ParentLookupName)) + { + throw new InvalidPluginExecutionException(String.Format("{0} is not a valid attribute name in {1} value", param.ParentLookupName, fields[field])); + } + + if (attributeList.Single(a => a.LogicalName.Equals(param.ParentLookupName)).AttributeType != AttributeTypeCode.Lookup && attributeList.Single(a => a.LogicalName.Equals(param.ParentLookupName)).AttributeType != AttributeTypeCode.Customer && attributeList.Single(a => a.LogicalName.Equals(param.ParentLookupName)).AttributeType != AttributeTypeCode.Owner) + { + throw new InvalidPluginExecutionException(String.Format("{0} must be a Lookup attribute type in {1} value", param.ParentLookupName, fields[field])); + } + + var parentLookupAttribute = (LookupAttributeMetadata)GetAttributeMetadata(Target.GetAttributeValue("cel_entityname"), param.ParentLookupName); + if (!parentLookupAttribute.Targets.Any(e => GetEntityMetadata(e).Select(a => a.LogicalName).Contains(param.AttributeName))) + { + throw new InvalidPluginExecutionException(String.Format("invalid attribute on {0} parent entity, in {1} value", param.ParentLookupName, fields[field])); + } + + } + } + } + catch (InvalidPluginExecutionException) + { + throw; + } + catch + { + throw new InvalidPluginExecutionException(String.Format("Failed to parse Runtime Parameters in {0} value.", fields[field])); + } + } + } +#endif + #endregion + + if (Target.Contains("cel_conditionaloptionset")) + { + Trace("Validate Conditional OptionSet"); + if (!attributeList.Select(a => a.LogicalName).Contains(Target.GetAttributeValue("cel_conditionaloptionset"))) + { + throw new InvalidPluginExecutionException("Specified Conditional OptionSet does not exist"); + } + + if (attributeList.Single(a => a.LogicalName.Equals(Target.GetAttributeValue("cel_conditionaloptionset"))).AttributeType != AttributeTypeCode.Picklist) + { + throw new InvalidPluginExecutionException("Conditional Attribute must be an OptionSet"); + } + + Trace("Validate Conditional Value"); + PicklistAttributeMetadata optionSetMetadata = (PicklistAttributeMetadata)GetAttributeMetadata(Target.GetAttributeValue("cel_entityname"), Target.GetAttributeValue("cel_conditionaloptionset"));//attributeResponse.AttributeMetadata; + if (!optionSetMetadata.OptionSet.Options.Select(o => o.Value).Contains(Target.GetAttributeValue("cel_conditionalvalue"))) + { + throw new InvalidPluginExecutionException("Conditional Value does not exist in OptionSet"); + } + } + + #region Duplicate Check +#if DUPLICATECHECK + Trace("Validate there are no duplicates"); + // TODO: Fix this. duplicate detection works when all fields contain data, but fails when some fields are empty + var autoNumberList = Context.OrganizationDataContext.CreateQuery("cel_autonumber") + .Where(a => a.GetAttributeValue("cel_entityname").Equals(Target.GetAttributeValue("cel_entityname")) && a.GetAttributeValue("cel_attributename").Equals(Target.GetAttributeValue("cel_attributename"))) + .Select(a => new { Id = a.GetAttributeValue("cel_autonumberid"), ConditionalOption = a.GetAttributeValue("cel_conditionaloptionset"), ConditionalValue = a.GetAttributeValue("cel_conditionalvalue") }) + .ToList(); + + + if (!Target.Contains("cel_conditionaloptionset") && autoNumberList.Any()) + { + throw new InvalidPluginExecutionException("Duplicate AutoNumber record exists."); + } + else if (autoNumberList.Where(a => a.ConditionalOption.Equals(Target.GetAttributeValue("cel_conditionaloptionset")) && a.ConditionalValue.Equals(Target.GetAttributeValue("cel_conditionalvalue"))).Any()) + { + throw new InvalidPluginExecutionException("Duplicate AutoNumber record exists."); + } +#endif + #endregion + + Trace("Insert the autoNumber Name attribute"); + Target["cel_name"] = String.Format("AutoNumber for {0}, {1}", Target.GetAttributeValue("cel_entityname"), Target.GetAttributeValue("cel_attributename")); + } + + private AttributeMetadata GetAttributeMetadata(string entityName, string attributeName) + { + string attributeKey = entityName + attributeName; + if (!AttributeMetadata.ContainsKey(attributeKey)) + { + try + { + RetrieveAttributeResponse attributeResponse = (RetrieveAttributeResponse)Context.OrganizationService.Execute(new RetrieveAttributeRequest() { EntityLogicalName = entityName, LogicalName = attributeName }); + AttributeMetadata.Add(attributeKey, attributeResponse.AttributeMetadata); + } + catch + { + throw new InvalidPluginExecutionException(String.Format("{1} attribute does not exist on {0} entity, or entity does not exist.", entityName, attributeName)); + } + } + + return AttributeMetadata[attributeKey]; + } + + private List GetEntityMetadata(string entityName) + { + if (!EntityMetadata.ContainsKey(entityName)) + { + try + { + RetrieveEntityResponse response = (RetrieveEntityResponse)Context.OrganizationDataContext.Execute(new RetrieveEntityRequest() { EntityFilters = EntityFilters.Attributes, LogicalName = entityName }); + EntityMetadata.Add(entityName, response.EntityMetadata.Attributes.ToList()); // Keep the list of Attributes + } + catch + { + throw new InvalidPluginExecutionException(String.Format("{0} Entity does not exist.", entityName)); + } + } + + return EntityMetadata[entityName].ToList(); + } + } +} diff --git a/AutoNumber.Tests/App.config b/AutoNumber.Tests/App.config new file mode 100644 index 0000000..a8ebc20 --- /dev/null +++ b/AutoNumber.Tests/App.config @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/AutoNumber.Tests/AutoNumber.Tests.csproj b/AutoNumber.Tests/AutoNumber.Tests.csproj new file mode 100644 index 0000000..25c1bd7 --- /dev/null +++ b/AutoNumber.Tests/AutoNumber.Tests.csproj @@ -0,0 +1,193 @@ + + + + Debug + AnyCPU + {CBE6269C-2B9B-460C-9AE1-89DA739B30A8} + Library + Properties + AutoNumber.Tests + AutoNumber.Tests + v4.5 + 512 + {3AC096D0-A1C2-E12C-1390-A8335801FDAB};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} + 10.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + $(ProgramFiles)\Common Files\microsoft shared\VSTT\$(VisualStudioVersion)\UITestExtensionPackages + False + UnitTest + + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + false + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + false + + + + ..\packages\Microsoft.CrmSdk.Extensions.6.0.4\lib\net40\AntiXSSLibrary.dll + True + + + ..\packages\Castle.Core.4.1.0\lib\net45\Castle.Core.dll + True + + + ..\packages\Microsoft.CrmSdk.CoreAssemblies.6.1.2\lib\net45\Microsoft.Crm.Sdk.Proxy.dll + True + + + ..\packages\Microsoft.IdentityModel.6.1.7600.16394\lib\net35\Microsoft.IdentityModel.dll + True + + + ..\packages\WindowsAzure.ServiceBus.2.1.2.0\lib\net40-full\Microsoft.ServiceBus.dll + True + + + ..\packages\Microsoft.WindowsAzure.ConfigurationManager.2.0.0.0\lib\net40\Microsoft.WindowsAzure.Configuration.dll + True + + + ..\packages\Microsoft.CrmSdk.Extensions.6.0.4\lib\net40\Microsoft.Xrm.Client.dll + True + + + ..\packages\Microsoft.CrmSdk.Extensions.6.0.4\lib\net40\Microsoft.Xrm.Client.CodeGeneration.dll + True + + + ..\packages\Microsoft.CrmSdk.Extensions.6.0.4\lib\net40\Microsoft.Xrm.Portal.dll + True + + + ..\packages\Microsoft.CrmSdk.Extensions.6.0.4\lib\net40\Microsoft.Xrm.Portal.Files.dll + True + + + ..\packages\Microsoft.CrmSdk.CoreAssemblies.6.1.2\lib\net45\Microsoft.Xrm.Sdk.dll + True + + + ..\packages\Microsoft.CrmSdk.Deployment.6.1.1\lib\net45\Microsoft.Xrm.Sdk.Deployment.dll + True + + + ..\packages\Moq.4.7.63\lib\net45\Moq.dll + True + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + True + True + Settings.settings + + + + + + + + SettingsSingleFileGenerator + Settings.Designer.cs + + + + + {56e63e6d-35e9-48b5-a549-1321610f197a} + AutoNumber + + + + + + + $projectId$ + $projectName$ + + + + + + + + + False + + + False + + + False + + + False + + + + + + + + \ No newline at end of file diff --git a/AutoNumber.Tests/MoqExtensions.cs b/AutoNumber.Tests/MoqExtensions.cs new file mode 100644 index 0000000..1e86583 --- /dev/null +++ b/AutoNumber.Tests/MoqExtensions.cs @@ -0,0 +1,30 @@ +using Moq.Language.Flow; +using System; +using System.Collections; +using System.Diagnostics; + +namespace AutoNumber.Tests +{ + public static class MoqExtensions + { + public static void ReturnsInOrder(this ISetup setup, + params object[] results) where T : class + { + var queue = new Queue(results); + setup.Returns(() => + { + var result = queue.Dequeue(); + if (result is Exception) + { + throw result as Exception; + } + return (TResult)result; + }); + } + + public static void WriteTrace(string s, object[] o) + { + Debug.WriteLine(s); + } + } +} diff --git a/AutoNumber.Tests/Properties/AssemblyInfo.cs b/AutoNumber.Tests/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..78ec067 --- /dev/null +++ b/AutoNumber.Tests/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("AutoNumber.Tests")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("AutoNumber.Tests")] +[assembly: AssemblyCopyright("Copyright © 2017")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("ce5c64cf-6dcb-4fad-9158-2eeeb1f0c6fe")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/AutoNumber.Tests/Properties/Settings.Designer.cs b/AutoNumber.Tests/Properties/Settings.Designer.cs new file mode 100644 index 0000000..deb5f81 --- /dev/null +++ b/AutoNumber.Tests/Properties/Settings.Designer.cs @@ -0,0 +1,44 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace AutoNumber.Tests.Properties { + + + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "14.0.0.0")] + internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase { + + private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings()))); + + public static Settings Default { + get { + return defaultInstance; + } + } + + [global::System.Configuration.ApplicationScopedSettingAttribute()] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Configuration.DefaultSettingValueAttribute("PLUGIN")] + public string CRMProjectType { + get { + return ((string)(this["CRMProjectType"])); + } + } + + [global::System.Configuration.ApplicationScopedSettingAttribute()] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Configuration.DefaultSettingValueAttribute("UNIT")] + public string CRMTestType { + get { + return ((string)(this["CRMTestType"])); + } + } + } +} diff --git a/AutoNumber.Tests/Properties/Settings.settings b/AutoNumber.Tests/Properties/Settings.settings new file mode 100644 index 0000000..f838cc8 --- /dev/null +++ b/AutoNumber.Tests/Properties/Settings.settings @@ -0,0 +1,12 @@ + + + + + + PLUGIN + + + UNIT + + + \ No newline at end of file diff --git a/AutoNumber.Tests/UnitTests.cs b/AutoNumber.Tests/UnitTests.cs new file mode 100644 index 0000000..5093d74 --- /dev/null +++ b/AutoNumber.Tests/UnitTests.cs @@ -0,0 +1,211 @@ +using System; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Microsoft.Xrm.Sdk; + +namespace Celedon +{ + [TestClass] + public class AutoNumberUnitTest + { + [TestMethod] + public void RuntimeParameterParseTest1() + { + RuntimeParameter rp = RuntimeParameter.Parse("{attributeName}"); + + // Stuff that should be populated + Assert.AreEqual(rp.AttributeName, "attributeName"); + + // Stuff that should not be populated + Assert.AreEqual(rp.Conditional, new RuntimeParameter.ConditionalFormatter()); + Assert.AreEqual(rp.DefaultValue, String.Empty); + Assert.AreEqual(rp.ParentLookupName, String.Empty); + Assert.AreEqual(rp.StringFormatter, String.Empty); + } + + [TestMethod] + public void RuntimeParameterParseTest2() + { + RuntimeParameter rp = RuntimeParameter.Parse("{parentLookup.parentAttribute}"); + + // Stuff that should be populated + Assert.AreEqual(rp.AttributeName, "parentAttribute"); + Assert.AreEqual(rp.ParentLookupName, "parentLookup"); + + // Stuff that should not be populated + Assert.AreEqual(rp.Conditional, new RuntimeParameter.ConditionalFormatter()); + Assert.AreEqual(rp.DefaultValue, String.Empty); + Assert.AreEqual(rp.StringFormatter, String.Empty); + } + [TestMethod] + public void RuntimeParameterParseTest3() + { + RuntimeParameter rp = RuntimeParameter.Parse("{attributeName|defaultValue}"); + + // Stuff that should be populated + Assert.AreEqual(rp.AttributeName, "attributeName"); + Assert.AreEqual(rp.DefaultValue, "defaultValue"); + + // Stuff that should not be populated + Assert.AreEqual(rp.Conditional, new RuntimeParameter.ConditionalFormatter()); + Assert.AreEqual(rp.ParentLookupName, String.Empty); + Assert.AreEqual(rp.StringFormatter, String.Empty); + } + [TestMethod] + public void RuntimeParameterParseTest4() + { + RuntimeParameter rp = RuntimeParameter.Parse("{attributeName:formatString}"); + + // Stuff that should be populated + Assert.AreEqual(rp.AttributeName, "attributeName"); + Assert.AreEqual(rp.StringFormatter, "formatString"); + + // Stuff that should not be populated + Assert.AreEqual(rp.Conditional, new RuntimeParameter.ConditionalFormatter()); + Assert.AreEqual(rp.DefaultValue, String.Empty); + Assert.AreEqual(rp.ParentLookupName, String.Empty); + } + [TestMethod] + public void RuntimeParameterParseTest5() + { + RuntimeParameter rp = RuntimeParameter.Parse("{attributeName:matchValue?trueValue|falseValue}"); + + // Stuff that should be populated + Assert.AreEqual(rp.AttributeName, "attributeName"); + Assert.AreEqual(rp.Conditional.MatchValue, "matchValue"); + + // Stuff that should not be populated + Assert.AreEqual(rp.DefaultValue, String.Empty); + Assert.AreEqual(rp.ParentLookupName, String.Empty); + Assert.AreEqual(rp.StringFormatter, String.Empty); + + // Conditional test cases + Assert.AreEqual(rp.Conditional.GetResult("matchValue"), "trueValue"); + Assert.AreEqual(rp.Conditional.GetResult("other"), "falseValue"); + } + [TestMethod] + public void RuntimeParameterParseTest6() + { + RuntimeParameter rp = RuntimeParameter.Parse("{attributeName:match1?result1|match2?result2|match3?result3|elseResult}"); + + // Stuff that should be populated + Assert.AreEqual(rp.AttributeName, "attributeName"); + + // Stuff that should not be populated + Assert.AreEqual(rp.DefaultValue, String.Empty); + Assert.AreEqual(rp.ParentLookupName, String.Empty); + Assert.AreEqual(rp.StringFormatter, String.Empty); + + // Conditional test cases + Assert.AreEqual(rp.Conditional.GetResult("match1"), "result1"); + Assert.AreEqual(rp.Conditional.GetResult("match2"), "result2"); + Assert.AreEqual(rp.Conditional.GetResult("match3"), "result3"); + Assert.AreEqual(rp.Conditional.GetResult("other"), "elseResult"); + } + [TestMethod] + public void RuntimeParameterParseTest7() + { + RuntimeParameter rp = RuntimeParameter.Parse("{attributeName:formatString:matchValue?trueValue|falseValue}"); + + // Stuff that should be populated + Assert.AreEqual(rp.AttributeName, "attributeName"); + Assert.AreEqual(rp.StringFormatter, "formatString"); + + // Stuff that should not be populated + Assert.AreEqual(rp.DefaultValue, String.Empty); + Assert.AreEqual(rp.ParentLookupName, String.Empty); + + // Conditional test cases + Assert.AreEqual(rp.Conditional.GetResult("matchValue"), "trueValue"); + Assert.AreEqual(rp.Conditional.GetResult("other"), "falseValue"); + + } + [TestMethod] + public void RuntimeParameterParseTest8() + { + RuntimeParameter rp = RuntimeParameter.Parse("{parentLookup.parentAttribute:formatString:matchValue?trueValue|falseValue}"); + + // Stuff that should be populated + Assert.AreEqual(rp.AttributeName, "parentAttribute"); + Assert.AreEqual(rp.ParentLookupName, "parentLookup"); + Assert.AreEqual(rp.StringFormatter, "formatString"); + + // Stuff that should not be populated + Assert.AreEqual(rp.DefaultValue, String.Empty); + + // Conditional test cases + Assert.AreEqual(rp.Conditional.GetResult("matchValue"), "trueValue"); + Assert.AreEqual(rp.Conditional.GetResult("other"), "falseValue"); + } + [TestMethod] + public void RuntimeParameterParseTest9() + { + RuntimeParameter rp = RuntimeParameter.Parse("{attributeName|defaultValue:formatString:matchValue?trueValue|falseValue}"); + + // Stuff that should be populated + Assert.AreEqual(rp.AttributeName, "attributeName"); + Assert.AreEqual(rp.DefaultValue, "defaultValue"); + Assert.AreEqual(rp.StringFormatter, "formatString"); + + // Stuff that should not be populated + Assert.AreEqual(rp.ParentLookupName, String.Empty); + + // Conditional test cases + Assert.AreEqual(rp.Conditional.GetResult("matchValue"), "trueValue"); + Assert.AreEqual(rp.Conditional.GetResult("other"), "falseValue"); + } + [TestMethod] + public void RuntimeParameterParseTest10() + { + RuntimeParameter rp = RuntimeParameter.Parse("{attributeName:>100?trueValue|falseValue}"); + + // Stuff that should be populated + Assert.AreEqual(rp.AttributeName, "attributeName"); + + // Stuff that should not be populated + Assert.AreEqual(rp.DefaultValue, String.Empty); + Assert.AreEqual(rp.ParentLookupName, String.Empty); + Assert.AreEqual(rp.StringFormatter, String.Empty); + + // Conditional test cases + Assert.AreEqual(rp.Conditional.GetResult(101), "trueValue"); + Assert.AreEqual(rp.Conditional.GetResult(99), "falseValue"); + } + [TestMethod] + public void RuntimeParameterParseTest11() + { + RuntimeParameter rp = RuntimeParameter.Parse("{attributeName:<2015-1-1?trueValue|falseValue}"); + + // Stuff that should be populated + Assert.AreEqual(rp.AttributeName, "attributeName"); + + // Stuff that should not be populated + Assert.AreEqual(rp.DefaultValue, String.Empty); + Assert.AreEqual(rp.ParentLookupName, String.Empty); + Assert.AreEqual(rp.StringFormatter, String.Empty); + + // Conditional test cases + Assert.AreEqual(rp.Conditional.GetResult(new DateTime(2014,1,1)), "trueValue"); + Assert.AreEqual(rp.Conditional.GetResult(new DateTime(2016,1,1)), "falseValue"); + } + [TestMethod] + public void RuntimeParameterParseTest12() + { + RuntimeParameter rp = RuntimeParameter.Parse("{attributeName:yyyy:2015?trueValue|falseValue}"); + + // Stuff that should be populated + Assert.AreEqual(rp.AttributeName, "attributeName"); + Assert.AreEqual(rp.StringFormatter, "yyyy"); + + // Stuff that should not be populated + Assert.AreEqual(rp.DefaultValue, String.Empty); + Assert.AreEqual(rp.ParentLookupName, String.Empty); + + // Conditional test cases + Entity test = new Entity(); + test["attributeName"] = new DateTime(2015,1,1); + Assert.AreEqual(rp.GetParameterValue(test), "trueValue"); + test["attributeName"] = new DateTime(2016, 1, 1); + Assert.AreEqual(rp.GetParameterValue(test), "falseValue"); + } + } +} diff --git a/AutoNumber.Tests/packages.config b/AutoNumber.Tests/packages.config new file mode 100644 index 0000000..de38e99 --- /dev/null +++ b/AutoNumber.Tests/packages.config @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/AutoNumber.WebResources/AutoNumber.WebResources.csproj b/AutoNumber.WebResources/AutoNumber.WebResources.csproj new file mode 100644 index 0000000..74a8c32 --- /dev/null +++ b/AutoNumber.WebResources/AutoNumber.WebResources.csproj @@ -0,0 +1,105 @@ + + + + + Debug + AnyCPU + + + 2.0 + {68670149-40FE-4EA9-A624-572B2C595F69} + {349c5851-65df-11da-9384-00065b846f21};{fae04ec0-301f-11d3-bf4b-00c04f79efbc} + Library + Properties + AutoNumber.WebResources + AutoNumber.WebResources + v4.5 + true + + enabled + enabled + false + + + + true + full + false + bin\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\ + TRACE + prompt + 4 + + + + + + + + + + + + + True + True + Settings.settings + + + + + + + + + SettingsSingleFileGenerator + Settings.Designer.cs + + + Web.config + + + Web.config + + + + 10.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + + + + + + + + + True + True + 18176 + / + http://localhost:18176/ + False + False + + + False + + + + + + \ No newline at end of file diff --git a/AutoNumber.WebResources/Properties/AssemblyInfo.cs b/AutoNumber.WebResources/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..1bb5990 --- /dev/null +++ b/AutoNumber.WebResources/Properties/AssemblyInfo.cs @@ -0,0 +1,35 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("AutoNumber.WebResources")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("AutoNumber.WebResources")] +[assembly: AssemblyCopyright("Copyright © 2017")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("7865dd68-63fc-444e-97e8-5bf9c2199dd2")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Revision and Build Numbers +// by using the '*' as shown below: +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/AutoNumber.WebResources/Properties/Settings.Designer.cs b/AutoNumber.WebResources/Properties/Settings.Designer.cs new file mode 100644 index 0000000..f165871 --- /dev/null +++ b/AutoNumber.WebResources/Properties/Settings.Designer.cs @@ -0,0 +1,41 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.0 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace Properties +{ + + + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "12.0.0.0")] + internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase + { + + private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings()))); + + public static Settings Default + { + get + { + return defaultInstance; + } + } + + [global::System.Configuration.ApplicationScopedSettingAttribute()] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Configuration.DefaultSettingValueAttribute("WEBRESOURCE")] + public string CRMProjectType + { + get + { + return ((string)(this["CRMProjectType"])); + } + } + } +} diff --git a/AutoNumber.WebResources/Properties/Settings.settings b/AutoNumber.WebResources/Properties/Settings.settings new file mode 100644 index 0000000..39f2a3e --- /dev/null +++ b/AutoNumber.WebResources/Properties/Settings.settings @@ -0,0 +1,9 @@ + + + + + + WEBRESOURCE + + + \ No newline at end of file diff --git a/AutoNumber.WebResources/Web.Debug.config b/AutoNumber.WebResources/Web.Debug.config new file mode 100644 index 0000000..2e302f9 --- /dev/null +++ b/AutoNumber.WebResources/Web.Debug.config @@ -0,0 +1,30 @@ + + + + + + + + + + \ No newline at end of file diff --git a/AutoNumber.WebResources/Web.Release.config b/AutoNumber.WebResources/Web.Release.config new file mode 100644 index 0000000..c358444 --- /dev/null +++ b/AutoNumber.WebResources/Web.Release.config @@ -0,0 +1,31 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/AutoNumber.WebResources/Web.config b/AutoNumber.WebResources/Web.config new file mode 100644 index 0000000..bfb640d --- /dev/null +++ b/AutoNumber.WebResources/Web.config @@ -0,0 +1,11 @@ + + + + + + + + \ No newline at end of file diff --git a/WebResources/cel_autonumber.js b/AutoNumber.WebResources/scripts/cel_autonumber.js similarity index 100% rename from WebResources/cel_autonumber.js rename to AutoNumber.WebResources/scripts/cel_autonumber.js diff --git a/AutoNumber.sln b/AutoNumber.sln new file mode 100644 index 0000000..82cfa95 --- /dev/null +++ b/AutoNumber.sln @@ -0,0 +1,34 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 14 +VisualStudioVersion = 14.0.25420.1 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AutoNumber", "AutoNumber\AutoNumber.csproj", "{56E63E6D-35E9-48B5-A549-1321610F197A}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AutoNumber.Tests", "AutoNumber.Tests\AutoNumber.Tests.csproj", "{CBE6269C-2B9B-460C-9AE1-89DA739B30A8}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AutoNumber.WebResources", "AutoNumber.WebResources\AutoNumber.WebResources.csproj", "{68670149-40FE-4EA9-A624-572B2C595F69}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {56E63E6D-35E9-48B5-A549-1321610F197A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {56E63E6D-35E9-48B5-A549-1321610F197A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {56E63E6D-35E9-48B5-A549-1321610F197A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {56E63E6D-35E9-48B5-A549-1321610F197A}.Release|Any CPU.Build.0 = Release|Any CPU + {CBE6269C-2B9B-460C-9AE1-89DA739B30A8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CBE6269C-2B9B-460C-9AE1-89DA739B30A8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CBE6269C-2B9B-460C-9AE1-89DA739B30A8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CBE6269C-2B9B-460C-9AE1-89DA739B30A8}.Release|Any CPU.Build.0 = Release|Any CPU + {68670149-40FE-4EA9-A624-572B2C595F69}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {68670149-40FE-4EA9-A624-572B2C595F69}.Debug|Any CPU.Build.0 = Debug|Any CPU + {68670149-40FE-4EA9-A624-572B2C595F69}.Release|Any CPU.ActiveCfg = Release|Any CPU + {68670149-40FE-4EA9-A624-572B2C595F69}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection +EndGlobal diff --git a/AutoNumber/AutoNumber.csproj b/AutoNumber/AutoNumber.csproj index 43a0d8e..93181d0 100644 --- a/AutoNumber/AutoNumber.csproj +++ b/AutoNumber/AutoNumber.csproj @@ -1,17 +1,15 @@  - + + Debug AnyCPU - 8.0.30703 - 2.0 - {10E2B428-F093-400A-8F7F-38AA8BCBD434} - {4C25E9B5-9FA6-436c-8E19-B395D2A65FAF};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} + {56E63E6D-35E9-48B5-A549-1321610F197A} Library Properties - Celedon - CeledonPartners.AutoNumber - v4.0 + AutoNumber + AutoNumber + v4.5 512 @@ -23,6 +21,7 @@ DEBUG;TRACE prompt 4 + false pdbonly @@ -31,47 +30,70 @@ TRACE prompt 4 + false true - AutoNumberStrongKey.snk + + - False - ..\..\..\..\..\CRM 2013 SP1 R1 SDK\SDK\Bin\Microsoft.Crm.Sdk.Proxy.dll + ..\packages\Microsoft.CrmSdk.CoreAssemblies.6.1.2\lib\net45\Microsoft.Crm.Sdk.Proxy.dll + True + + + ..\packages\Microsoft.IdentityModel.6.1.7600.16394\lib\net35\Microsoft.IdentityModel.dll + True - False - ..\..\..\..\..\CRM 2013 SP1 R1 SDK\SDK\Bin\Microsoft.Xrm.Sdk.dll + ..\packages\Microsoft.CrmSdk.CoreAssemblies.6.1.2\lib\net45\Microsoft.Xrm.Sdk.dll + True - - - + + + - + + + + + + + + + - - + + + True + True + Settings.settings + - - - + + Designer + + + SettingsSingleFileGenerator + Settings.Designer.cs + +