Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions src/MetadataGen/MetadataGenerator365/AssemblyInfo.fs
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,16 @@ using System.Reflection;
[assembly: AssemblyDescription("A simulation engine that can mock a specific MS CRM instance. Useful for testing and debugging business logic.")]
[assembly: AssemblyCompany("Delegate A/S")]
[assembly: AssemblyCopyright("Copyright (c) Delegate A/S 2017")]
[assembly: AssemblyVersion("1.15.3")]
[assembly: AssemblyFileVersion("1.15.3")]
[assembly: AssemblyVersion("1.15.4")]
[assembly: AssemblyFileVersion("1.15.4")]
namespace System {
internal static class AssemblyVersionInformation {
internal const System.String AssemblyTitle = "XrmMockup";
internal const System.String AssemblyProduct = "XrmMockup";
internal const System.String AssemblyDescription = "A simulation engine that can mock a specific MS CRM instance. Useful for testing and debugging business logic.";
internal const System.String AssemblyCompany = "Delegate A/S";
internal const System.String AssemblyCopyright = "Copyright (c) Delegate A/S 2017";
internal const System.String AssemblyVersion = "1.15.3";
internal const System.String AssemblyFileVersion = "1.15.3";
internal const System.String AssemblyVersion = "1.15.4";
internal const System.String AssemblyFileVersion = "1.15.4";
}
}
8 changes: 4 additions & 4 deletions src/XrmMockup365/AssemblyInfo.fs
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,16 @@ using System.Reflection;
[assembly: AssemblyDescription("A simulation engine that can mock a specific MS CRM instance. Useful for testing and debugging business logic.")]
[assembly: AssemblyCompany("Delegate A/S")]
[assembly: AssemblyCopyright("Copyright (c) Delegate A/S 2017")]
[assembly: AssemblyVersion("1.15.3")]
[assembly: AssemblyFileVersion("1.15.3")]
[assembly: AssemblyVersion("1.15.4")]
[assembly: AssemblyFileVersion("1.15.4")]
namespace System {
internal static class AssemblyVersionInformation {
internal const System.String AssemblyTitle = "XrmMockup";
internal const System.String AssemblyProduct = "XrmMockup";
internal const System.String AssemblyDescription = "A simulation engine that can mock a specific MS CRM instance. Useful for testing and debugging business logic.";
internal const System.String AssemblyCompany = "Delegate A/S";
internal const System.String AssemblyCopyright = "Copyright (c) Delegate A/S 2017";
internal const System.String AssemblyVersion = "1.15.3";
internal const System.String AssemblyFileVersion = "1.15.3";
internal const System.String AssemblyVersion = "1.15.4";
internal const System.String AssemblyFileVersion = "1.15.4";
}
}
56 changes: 47 additions & 9 deletions src/XrmMockup365/XrmMockup.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,29 +16,41 @@ namespace DG.Tools.XrmMockup {
/// </summary>
public class XrmMockup365 : XrmMockupBase {

private static readonly Dictionary<XrmMockupSettings, XrmMockup365> instances = new Dictionary<XrmMockupSettings, XrmMockup365>();
private static readonly Dictionary<XrmMockupSettings, StaticMetadataCache> metadataCache = new Dictionary<XrmMockupSettings, StaticMetadataCache>();
private static readonly object cacheLock = new object();

private XrmMockup365(XrmMockupSettings Settings, MetadataSkeleton metadata = null, List<Entity> workflows = null, List<SecurityRole> securityRoles = null) :
base(Settings, metadata, workflows, securityRoles)
{
}

private XrmMockup365(XrmMockupSettings Settings, StaticMetadataCache staticCache) :
base(Settings, staticCache)
{
}

/// <summary>
/// Gets an instance of XrmMockup365
/// Gets a new instance of XrmMockup365 with its own database
/// </summary>
/// <param name="Settings"></param>
public static XrmMockup365 GetInstance(XrmMockupSettings Settings) {
if (instances.ContainsKey(Settings)) {
return instances[Settings];
StaticMetadataCache cache;

lock (cacheLock)
{
if (!metadataCache.ContainsKey(Settings))
{
metadataCache[Settings] = Core.BuildStaticMetadataCache(Settings);
}
cache = metadataCache[Settings];
}

var instance = new XrmMockup365(Settings);
instances[Settings] = instance;
return instance;
// Always return a new instance with its own database
return new XrmMockup365(Settings, cache);
}

/// <summary>
/// Gets an instance of XrmMockup365 using the same metadata as the provided
/// Gets a new instance of XrmMockup365 using the same metadata as the provided instance
/// </summary>
/// <param name="xrmMockup">The existing instance to copy</param>
/// <param name="settings">
Expand All @@ -47,7 +59,33 @@ public static XrmMockup365 GetInstance(XrmMockupSettings Settings) {
/// </param>
public static XrmMockup365 GetInstance(XrmMockup365 xrmMockup, XrmMockupSettings settings = null)
{
return new XrmMockup365(settings ?? xrmMockup.Settings, xrmMockup.Metadata, xrmMockup.Workflows, xrmMockup.SecurityRoles);
var effectiveSettings = settings ?? xrmMockup.Settings;

// Try to use cached metadata if settings match
StaticMetadataCache cache;
lock (cacheLock)
{
if (metadataCache.ContainsKey(effectiveSettings))
{
cache = metadataCache[effectiveSettings];
}
else
{
// Create a new cache entry using the existing instance's data
cache = new StaticMetadataCache(
xrmMockup.Metadata,
xrmMockup.Workflows,
xrmMockup.SecurityRoles,
new Dictionary<string, Type>(), // Will be rebuilt if needed
xrmMockup.BaseCurrency,
0, // Will be retrieved from metadata
null // Will be rebuilt if needed
);
metadataCache[effectiveSettings] = cache;
}
}

return new XrmMockup365(effectiveSettings, cache);
}
}
}
192 changes: 187 additions & 5 deletions src/XrmMockupShared/Core.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,13 @@
using Microsoft.Xrm.Sdk.Query;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.ServiceModel;
using System.Text.Json;
using System.Text.RegularExpressions;
using WorkflowExecuter;

[assembly: InternalsVisibleTo("SharedTests")]
Expand Down Expand Up @@ -129,24 +131,204 @@ public Core(XrmMockupSettings Settings, MetadataSkeleton metadata, List<Entity>
this.FormulaFieldEvaluator = new FormulaFieldEvaluator(ServiceFactory);
}

/// <summary>
/// Creates a new instance of Core using cached static metadata
/// </summary>
/// <param name="Settings"></param>
/// <param name="staticCache"></param>
public Core(XrmMockupSettings Settings, StaticMetadataCache staticCache)
{
this.TimeOffset = new TimeSpan();
this.settings = Settings;
this.metadata = staticCache.Metadata;
this.baseCurrency = staticCache.BaseCurrency;
this.baseCurrencyPrecision = staticCache.BaseCurrencyPrecision;
this.OnlineProxy = staticCache.OnlineProxy;
this.entityTypeMap = staticCache.EntityTypeMap;

this.db = new XrmDb(staticCache.Metadata.EntityMetadata, staticCache.OnlineProxy);
this.snapshots = new Dictionary<string, Snapshot>();
this.security = new Security(this, staticCache.Metadata, staticCache.SecurityRoles, db);
this.TracingServiceFactory = Settings.TracingServiceFactory ?? new TracingServiceFactory();
this.ServiceFactory = new MockupServiceProviderAndFactory(this);

//add the additional plugin settings to the meta data
if (Settings.IPluginMetadata != null)
{
// Create a new list to avoid modifying the cached metadata
var allPlugins = new List<MetaPlugin>(staticCache.Metadata.Plugins);
allPlugins.AddRange(Settings.IPluginMetadata);
this.pluginManager = new PluginManager(Settings.BasePluginTypes, staticCache.Metadata.EntityMetadata, allPlugins);
}
else
{
this.pluginManager = new PluginManager(Settings.BasePluginTypes, staticCache.Metadata.EntityMetadata, staticCache.Metadata.Plugins);
}

this.workflowManager = new WorkflowManager(Settings.CodeActivityInstanceTypes, Settings.IncludeAllWorkflows,
staticCache.Workflows, staticCache.Metadata.EntityMetadata);
this.customApiManager = new CustomApiManager(Settings.BaseCustomApiTypes);

this.systemAttributeNames = new List<string>() { "createdon", "createdby", "modifiedon", "modifiedby" };

this.RequestHandlers = GetRequestHandlers(db);
InitializeDB();
this.security.InitializeSecurityRoles(db);
this.orgDetail = Settings.OrganizationDetail;

this.FormulaFieldEvaluator = new FormulaFieldEvaluator(ServiceFactory);
}

/// <summary>
/// Builds a static metadata cache from settings
/// </summary>
/// <param name="settings"></param>
/// <returns></returns>
public static StaticMetadataCache BuildStaticMetadataCache(XrmMockupSettings settings)
{
var metadataDirectory = settings.MetadataDirectoryPath ?? "../../Metadata/";
var metadata = Utility.GetMetadata(metadataDirectory);
var workflows = Utility.GetWorkflows(metadataDirectory);
var securityRoles = Utility.GetSecurityRoles(metadataDirectory);

var baseCurrency = metadata.BaseOrganization.GetAttributeValue<EntityReference>("basecurrencyid");
var baseCurrencyPrecision = metadata.BaseOrganization.GetAttributeValue<int>("pricingdecimalprecision");

var onlineProxy = BuildOnlineProxy(settings);
var entityTypeMap = new Dictionary<string, Type>();

// Build entity type map for proxy types if enabled
if (settings.EnableProxyTypes == true)
{
BuildEntityTypeMap(settings, entityTypeMap);
}

// Note: IPluginMetadata is handled per-instance in the Core constructor
// to avoid modifying the shared cache

return new StaticMetadataCache(metadata, workflows, securityRoles, entityTypeMap,
baseCurrency, baseCurrencyPrecision, onlineProxy);
}

private static OrganizationServiceProxy BuildOnlineProxy(XrmMockupSettings settings)
{
if (settings.OnlineEnvironment.HasValue)
{
var env = settings.OnlineEnvironment.Value;
var orgHelper = new OrganizationHelper(
new Uri(env.uri),
env.providerType,
env.username,
env.password,
env.domain);
var proxy = orgHelper.GetServiceProxy();
if (settings.EnableProxyTypes == true)
proxy.EnableProxyTypes();
return proxy;
}
return null;
}

private static void BuildEntityTypeMap(XrmMockupSettings settings, Dictionary<string, Type> entityTypeMap)
{
if (settings.Assemblies?.Any() ?? false)
{
foreach (var assembly in settings.Assemblies)
{
EnableProxyTypes(assembly, entityTypeMap);
}
}
else
{
List<string> exclude = new List<string> {
"Microsoft.Xrm.Sdk.dll",
"Microsoft.Crm.Sdk.Proxy.dll"
};

var regex = new Regex("^XrmMockup.*\\.dll$");
var assemblies = new List<Assembly>();
var addedAssemblies = new HashSet<string>();

var exeAsm = AppDomain.CurrentDomain.GetAssemblies();
assemblies.AddRange(exeAsm);
foreach (var name in exeAsm.Select(x => x.FullName))
{
addedAssemblies.Add(name);
}

string path = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
foreach (string dll in Directory.GetFiles(path, "*.dll"))
{
var asm = Assembly.LoadFrom(dll);
if (addedAssemblies.Contains(asm.FullName)) continue;

assemblies.Add(asm);
addedAssemblies.Add(asm.FullName);
}

var useableAssemblies =
assemblies
.Where(asm => asm.CustomAttributes.Any(attr => attr.AttributeType.Name.Equals("ProxyTypesAssemblyAttribute")))
.Where(asm => !exclude.Contains(asm.ManifestModule.Name) && !regex.IsMatch(asm.ManifestModule.Name))
.ToList();

if (useableAssemblies?.Count > 0)
{
foreach (var asm in useableAssemblies)
{
EnableProxyTypes(asm, entityTypeMap);
}
}
}
}

private static void EnableProxyTypes(Assembly assembly, Dictionary<string, Type> entityTypeMap)
{
foreach (var type in assembly.GetLoadableTypes())
{
if (type.CustomAttributes
.FirstOrDefault(a => a.AttributeType.Name == "EntityLogicalNameAttribute")
?.ConstructorArguments
?.FirstOrDefault()
.Value is string logicalName)
{
entityTypeMap.Add(logicalName, type);
}
}
}

/// <summary>
/// Creates a deep copy of an Entity to avoid modifying shared cached entities
/// </summary>
private Entity CloneEntity(Entity original)
{
var clone = new Entity(original.LogicalName, original.Id);
foreach (var attr in original.Attributes)
{
clone[attr.Key] = attr.Value;
}
return clone;
}

private void InitializeDB()
{
this.OrganizationId = Guid.NewGuid();
this.OrganizationName = "MockupOrganization";

// Setup currencies
// Setup currencies - create copies to avoid modifying shared cached entities
var currencies = new List<Entity>();
foreach (var entity in metadata.Currencies)
{
Utility.RemoveAttribute(entity, "createdby", "modifiedby", "organizationid", "modifiedonbehalfby",
var currencyCopy = CloneEntity(entity);
Utility.RemoveAttribute(currencyCopy, "createdby", "modifiedby", "organizationid", "modifiedonbehalfby",
"createdonbehalfby");
currencies.Add(entity);
currencies.Add(currencyCopy);
}

this.db.AddRange(currencies);

// Setup root business unit
var rootBu = metadata.RootBusinessUnit;
// Setup root business unit - create a copy to avoid modifying the shared cached entity
var rootBu = CloneEntity(metadata.RootBusinessUnit);
rootBu["name"] = "RootBusinessUnit";
rootBu.Attributes.Remove("organizationid");
this.db.Add(rootBu, false);
Expand Down
Loading