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
54 changes: 54 additions & 0 deletions src/XrmMockup365/Core.cs
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ internal class Core : IXrmMockupExtension
private int baseCurrencyPrecision;
private FormulaFieldEvaluator FormulaFieldEvaluator { get; set; }
private List<string> systemAttributeNames;
internal FileBlockStore FileBlockStore { get; private set; }

/// <summary>
/// Creates a new instance of Core
Expand Down Expand Up @@ -119,6 +120,8 @@ private void InitializeCore(CoreInitializationData initData)
entityTypeMap = initData.EntityTypeMap;

db = new XrmDb(initData.Metadata.EntityMetadata, initData.OnlineProxy);
EnsureFileAttachmentMetadata();
FileBlockStore = new FileBlockStore();
snapshots = new Dictionary<string, Snapshot>();
security = new Security(this, initData.Metadata, initData.SecurityRoles, db);
TracingServiceFactory = initData.Settings.TracingServiceFactory ?? new TracingServiceFactory();
Expand Down Expand Up @@ -397,6 +400,8 @@ private void InitializeDB()
new InitializeFileBlocksUploadRequestHandler(this, db, metadata, security),
new UploadBlockRequestHandler(this, db, metadata, security),
new CommitFileBlocksUploadRequestHandler(this, db, metadata, security),
new InitializeFileBlocksDownloadRequestHandler(this, db, metadata, security),
new DownloadBlockRequestHandler(this, db, metadata, security),
new InstantiateTemplateRequestHandler(this, db, metadata, security),
new CreateMultipleRequestHandler(this, db, metadata, security),
new UpdateMultipleRequestHandler(this, db, metadata, security),
Expand Down Expand Up @@ -1352,6 +1357,7 @@ internal void ResetEnvironment()

pluginManager.ResetPlugins();
this.db = new XrmDb(metadata.EntityMetadata, GetOnlineProxy());
EnsureFileAttachmentMetadata();
this.RequestHandlers = GetRequestHandlers(db);
InitializeDB();
security.ResetEnvironment(db);
Expand Down Expand Up @@ -1426,6 +1432,54 @@ internal void AddSecurityRole(SecurityRole role)
security.AddSecurityRole(role);
}

private void EnsureFileAttachmentMetadata()
{
if (db.IsValidEntity("fileattachment"))
return;

var entityMetadata = new EntityMetadata();
SetMetadataProperty(entityMetadata, "LogicalName", "fileattachment");
SetMetadataProperty(entityMetadata, "PrimaryIdAttribute", "fileattachmentid");
SetMetadataProperty(entityMetadata, "PrimaryNameAttribute", "filename");

var attributes = new AttributeMetadata[]
{
CreateAttributeMetadata<UniqueIdentifierAttributeMetadata>("fileattachmentid", AttributeTypeCode.Uniqueidentifier),
CreateAttributeMetadata<StringAttributeMetadata>("filename", AttributeTypeCode.String),
CreateAttributeMetadata<DateTimeAttributeMetadata>("createdon", AttributeTypeCode.DateTime),
CreateAttributeMetadata<BigIntAttributeMetadata>("filesizeinbytes", AttributeTypeCode.BigInt),
CreateAttributeMetadata<StringAttributeMetadata>("mimetype", AttributeTypeCode.String),
CreateAttributeMetadata<StringAttributeMetadata>("objecttypecode", AttributeTypeCode.String),
CreateAttributeMetadata<StringAttributeMetadata>("regardingfieldname", AttributeTypeCode.String),
CreateAttributeMetadata<LookupAttributeMetadata>("objectid", AttributeTypeCode.Lookup)
};

SetMetadataProperty(entityMetadata, "Attributes", attributes);
db.RegisterEntityMetadata(entityMetadata);
}

private static void SetMetadataProperty(object metadata, string propertyName, object value)
{
var property = metadata.GetType().GetProperty(propertyName, BindingFlags.Public | BindingFlags.Instance);
if (property != null && property.CanWrite)
{
property.SetValue(metadata, value);
return;
}

var field = metadata.GetType().GetField($"_{char.ToLower(propertyName[0])}{propertyName.Substring(1)}",
BindingFlags.NonPublic | BindingFlags.Instance);
field?.SetValue(metadata, value);
}

private static T CreateAttributeMetadata<T>(string logicalName, AttributeTypeCode typeCode) where T : AttributeMetadata, new()
{
var attribute = new T();
SetMetadataProperty(attribute, "LogicalName", logicalName);
SetMetadataProperty(attribute, "AttributeType", typeCode);
return attribute;
}

public void TriggerExtension(IOrganizationService service, OrganizationRequest request, Entity currentEntity,
Entity preEntity, EntityReference userRef)
{
Expand Down
97 changes: 97 additions & 0 deletions src/XrmMockup365/Database/FileBlockStore.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
using Microsoft.Xrm.Sdk;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;

namespace DG.Tools.XrmMockup.Database
{
internal sealed class FileUploadSession
{
public Guid FileAttachmentId { get; set; }
public string FileName { get; set; }
public string MimeType { get; set; }
public EntityReference Target { get; set; }
public string FileAttributeName { get; set; }
public List<FileBlock> Blocks { get; set; } = new List<FileBlock>();
public DateTime CreatedOn { get; set; }
}

internal sealed class FileBlock
{
public string BlockId { get; set; }
public byte[] Data { get; set; }
}

internal sealed class CommittedFile
{
public Guid FileAttachmentId { get; set; }
public string FileName { get; set; }
public string MimeType { get; set; }
public long FileSize { get; set; }
public byte[] Data { get; set; }
public EntityReference Target { get; set; }
public string FileAttributeName { get; set; }
}

internal sealed class FileBlockStore
{
private readonly ConcurrentDictionary<string, FileUploadSession> pendingUploads = new ConcurrentDictionary<string, FileUploadSession>();
private readonly ConcurrentDictionary<Guid, CommittedFile> committedFiles = new ConcurrentDictionary<Guid, CommittedFile>();
private readonly ConcurrentDictionary<string, CommittedFile> downloadSessions = new ConcurrentDictionary<string, CommittedFile>();

public void StartUpload(string token, FileUploadSession session)
{
pendingUploads[token] = session;
}

public FileUploadSession GetUploadSession(string token)
{
pendingUploads.TryGetValue(token, out var session);
return session;
}

public void CommitUpload(string token, CommittedFile committedFile)
{
committedFiles[committedFile.FileAttachmentId] = committedFile;
pendingUploads.TryRemove(token, out _);
}

public CommittedFile GetCommittedFile(Guid fileAttachmentId)
{
committedFiles.TryGetValue(fileAttachmentId, out var file);
return file;
}

public void StartDownload(string token, CommittedFile committedFile)
{
downloadSessions[token] = committedFile;
}

public CommittedFile GetDownloadSession(string token)
{
downloadSessions.TryGetValue(token, out var file);
return file;
}

public CommittedFile FindCommittedFile(EntityReference target, string fileAttributeName)
{
foreach (var file in committedFiles.Values)
{
if (file.Target?.Id == target.Id &&
file.Target?.LogicalName == target.LogicalName &&
file.FileAttributeName == fileAttributeName)
{
return file;
}
}
return null;
}

public void Clear()
{
pendingUploads.Clear();
committedFiles.Clear();
downloadSessions.Clear();
}
}
}
8 changes: 8 additions & 0 deletions src/XrmMockup365/Database/XrmDb.cs
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,14 @@ internal bool IsValidEntity(string entityLogicalName)
return EntityMetadata.TryGetValue(entityLogicalName, out EntityMetadata entityMetadata);
}

internal void RegisterEntityMetadata(EntityMetadata entityMetadata)
{
if (entityMetadata is null)
throw new ArgumentNullException(nameof(entityMetadata));

EntityMetadata[entityMetadata.LogicalName] = entityMetadata;
}

internal void PrefillDBWithOnlineData(QueryExpression queryExpr)
{
if (OnlineProxy != null)
Expand Down
9 changes: 6 additions & 3 deletions src/XrmMockup365/Internal/Utility.cs
Original file line number Diff line number Diff line change
Expand Up @@ -264,17 +264,20 @@
RelationshipMetadataBase relationshipBase;
foreach (var meta in entityMetadata)
{
relationshipBase = meta.Value.ManyToManyRelationships.FirstOrDefault(rel => rel.MetadataId == metadataId);
var manyToMany = meta.Value.ManyToManyRelationships ?? Array.Empty<ManyToManyRelationshipMetadata>();
relationshipBase = manyToMany.FirstOrDefault(rel => rel.MetadataId == metadataId);
if (relationshipBase != null)
{
return relationshipBase;
}
relationshipBase = meta.Value.ManyToManyRelationships.FirstOrDefault(rel => rel.SchemaName == name);
relationshipBase = manyToMany.FirstOrDefault(rel => rel.SchemaName == name);
if (relationshipBase != null)
{
return relationshipBase;
}
var oneToManyBases = meta.Value.ManyToOneRelationships.Concat(meta.Value.OneToManyRelationships);
var manyToOne = meta.Value.ManyToOneRelationships ?? Array.Empty<OneToManyRelationshipMetadata>();
var oneToMany = meta.Value.OneToManyRelationships ?? Array.Empty<OneToManyRelationshipMetadata>();
var oneToManyBases = manyToOne.Concat(oneToMany);
relationshipBase = oneToManyBases.FirstOrDefault(rel => rel.MetadataId == metadataId);
if (relationshipBase != null)
{
Expand Down Expand Up @@ -1176,7 +1179,7 @@
}

[DataContract()]
internal enum componentstate

Check warning on line 1182 in src/XrmMockup365/Internal/Utility.cs

View workflow job for this annotation

GitHub Actions / run-ci

The type name 'componentstate' only contains lower-cased ascii characters. Such names may become reserved for the language.
{

[EnumMember()]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ internal override void CheckSecurity(OrganizationRequest orgRequest, EntityRefer

var callingUserPrivs = security.GetPrincipalPrivilege(userRef.Id);

var entityMetadata = metadata.EntityMetadata.Single(x => x.Value.ObjectTypeCode.Value == ttRow.GetAttributeValue<int>("objecttypecode"));
var entityMetadata = metadata.EntityMetadata.Single(x => x.Value.ObjectTypeCode.HasValue && x.Value.ObjectTypeCode.Value == ttRow.GetAttributeValue<int>("objecttypecode"));

var callingPrivs = callingUserPrivs[entityMetadata.Value.LogicalName];

Expand Down
55 changes: 48 additions & 7 deletions src/XrmMockup365/Requests/CommitFileBlocksUploadRequestHandler.cs
Original file line number Diff line number Diff line change
@@ -1,20 +1,61 @@
using Microsoft.Xrm.Sdk;
using Microsoft.Xrm.Sdk;
using Microsoft.Crm.Sdk.Messages;
using DG.Tools.XrmMockup.Database;
using System;
using System.Collections.Generic;
using System.Linq;
using System.ServiceModel;

namespace DG.Tools.XrmMockup
{
internal class CommitFileBlocksUploadRequestHandler : RequestHandler {
internal CommitFileBlocksUploadRequestHandler(Core core, XrmDb db, MetadataSkeleton metadata, Security security) : base(core, db, metadata, security, "CommitFileBlocksUpload") {}
internal sealed class CommitFileBlocksUploadRequestHandler : RequestHandler
{
internal CommitFileBlocksUploadRequestHandler(Core core, XrmDb db, MetadataSkeleton metadata, Security security)
: base(core, db, metadata, security, "CommitFileBlocksUpload") { }

internal override OrganizationResponse Execute(OrganizationRequest orgRequest, EntityReference userRef) {
internal override OrganizationResponse Execute(OrganizationRequest orgRequest, EntityReference userRef)
{
var request = MakeRequest<CommitFileBlocksUploadRequest>(orgRequest);

// Document store not implemented in database yet

var resp = new UploadBlockResponse();
var session = core.FileBlockStore.GetUploadSession(request.FileContinuationToken);
if (session is null)
throw new FaultException("Invalid or expired file continuation token.");

var blockDataList = new List<byte[]>();
foreach (var blockId in request.BlockList)
{
var block = session.Blocks.FirstOrDefault(b => b.BlockId == blockId);
if (block is null)
throw new FaultException($"Block with ID '{blockId}' not found in upload session.");

blockDataList.Add(block.Data);
}

var totalSize = blockDataList.Sum(b => b.Length);
var fileData = new byte[totalSize];
var offset = 0;
foreach (var blockData in blockDataList)
{
Buffer.BlockCopy(blockData, 0, fileData, offset, blockData.Length);
offset += blockData.Length;
}

var committedFile = new CommittedFile
{
FileAttachmentId = session.FileAttachmentId,
FileName = session.FileName,
MimeType = request.MimeType,
FileSize = fileData.Length,
Data = fileData,
Target = session.Target,
FileAttributeName = session.FileAttributeName
};

core.FileBlockStore.CommitUpload(request.FileContinuationToken, committedFile);

var resp = new CommitFileBlocksUploadResponse();
resp.Results["FileId"] = session.FileAttachmentId;
resp.Results["FileSizeInBytes"] = (long)fileData.Length;
return resp;
}
}
Expand Down
39 changes: 39 additions & 0 deletions src/XrmMockup365/Requests/DownloadBlockRequestHandler.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
using Microsoft.Xrm.Sdk;
using Microsoft.Crm.Sdk.Messages;
using DG.Tools.XrmMockup.Database;
using System;
using System.ServiceModel;

namespace DG.Tools.XrmMockup
{
internal sealed class DownloadBlockRequestHandler : RequestHandler
{
internal DownloadBlockRequestHandler(Core core, XrmDb db, MetadataSkeleton metadata, Security security)
: base(core, db, metadata, security, "DownloadBlock") { }

internal override OrganizationResponse Execute(OrganizationRequest orgRequest, EntityReference userRef)
{
var request = MakeRequest<DownloadBlockRequest>(orgRequest);

var committedFile = core.FileBlockStore.GetDownloadSession(request.FileContinuationToken);
if (committedFile is null)
throw new FaultException("Invalid or expired file continuation token.");

var offset = (int)request.Offset;
var blockLength = (int)request.BlockLength;

if (offset < 0 || offset >= committedFile.Data.Length)
throw new FaultException($"Invalid offset: {offset}. File size is {committedFile.Data.Length} bytes.");

var availableBytes = committedFile.Data.Length - offset;
var actualLength = Math.Min(blockLength, availableBytes);

var data = new byte[actualLength];
Buffer.BlockCopy(committedFile.Data, offset, data, 0, actualLength);

var resp = new DownloadBlockResponse();
resp.Results["Data"] = data;
return resp;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
using Microsoft.Xrm.Sdk;
using Microsoft.Crm.Sdk.Messages;
using DG.Tools.XrmMockup.Database;
using System;
using System.ServiceModel;

namespace DG.Tools.XrmMockup
{
internal sealed class InitializeFileBlocksDownloadRequestHandler : RequestHandler
{
internal InitializeFileBlocksDownloadRequestHandler(Core core, XrmDb db, MetadataSkeleton metadata, Security security)
: base(core, db, metadata, security, "InitializeFileBlocksDownload") { }

internal override OrganizationResponse Execute(OrganizationRequest orgRequest, EntityReference userRef)
{
var request = MakeRequest<InitializeFileBlocksDownloadRequest>(orgRequest);

var committedFile = core.FileBlockStore.FindCommittedFile(request.Target, request.FileAttributeName);
if (committedFile is null)
throw new FaultException($"No file attachment found for target entity '{request.Target.LogicalName}' with ID '{request.Target.Id}' and attribute '{request.FileAttributeName}'.");

var token = Guid.NewGuid().ToString();
core.FileBlockStore.StartDownload(token, committedFile);

var resp = new InitializeFileBlocksDownloadResponse();
resp.Results["FileContinuationToken"] = token;
resp.Results["FileSizeInBytes"] = committedFile.FileSize;
resp.Results["FileName"] = committedFile.FileName;
return resp;
}
}
}
Loading
Loading