diff --git a/SysML2.NET.Tests/PSM/DTO/DataMapperTestFixture.cs b/SysML2.NET.Tests/PSM/DTO/DataMapperTestFixture.cs
new file mode 100644
index 000000000..9137050e3
--- /dev/null
+++ b/SysML2.NET.Tests/PSM/DTO/DataMapperTestFixture.cs
@@ -0,0 +1,67 @@
+// ------------------------------------------------------------------------------------------------
+//
+//
+// Copyright (C) 2022-2026 Starion Group S.A.
+// SPDX-License-Identifier: Apache-2.0
+//
+//
+// ------------------------------------------------------------------------------------------------
+
+namespace SysML2.NET.Tests.PSM.DTO
+{
+ using System;
+
+ using NUnit.Framework;
+
+ using SysML2.NET.Common;
+ using SysML2.NET.Core.DTO.Root.Namespaces;
+ using SysML2.NET.PSM.DTO;
+
+ [TestFixture]
+ public class DataMapperTestFixture
+ {
+ [Test]
+ public void VerifyToPim()
+ {
+ Assert.That(DataMapper.ToPim(null), Is.Null);
+ Assert.That(() => DataMapper.ToPim(new UnmodelledDataRequest()), Throws.TypeOf());
+
+ var element = new Namespace { Id = Guid.NewGuid() };
+ var externalData = new ExternalDataRequest { Id = Guid.NewGuid() };
+ var externalRelationship = new ExternalRelationshipRequest { Id = Guid.NewGuid(), Language = "the language" };
+ var projectUsage = new ProjectUsageRequest { Id = Guid.NewGuid(), UsedProject = Guid.NewGuid() };
+
+ using (Assert.EnterMultipleScope())
+ {
+ Assert.That(DataMapper.ToPim(element), Is.SameAs(element));
+ Assert.That(DataMapper.ToPim(externalData), Is.TypeOf().With.Property("Id").EqualTo(externalData.Id));
+ Assert.That(DataMapper.ToPim(externalRelationship), Is.TypeOf().With.Property("Language").EqualTo("the language"));
+ Assert.That(DataMapper.ToPim(projectUsage), Is.TypeOf().With.Property("Id").EqualTo(projectUsage.Id));
+ }
+ }
+
+ [Test]
+ public void VerifyToRequest()
+ {
+ Assert.That(DataMapper.ToRequest(null), Is.Null);
+ Assert.That(() => DataMapper.ToRequest(new ExternalData { Id = Guid.NewGuid() }), Throws.TypeOf());
+
+ var element = new Namespace { Id = Guid.NewGuid() };
+ var externalData = new SysML2.NET.PIM.DTO.ExternalData { Id = Guid.NewGuid() };
+ var externalRelationship = new SysML2.NET.PIM.DTO.ExternalRelationship { Id = Guid.NewGuid(), Language = "the language" };
+ var projectUsage = new SysML2.NET.PIM.DTO.ProjectUsage { Id = Guid.NewGuid() };
+
+ using (Assert.EnterMultipleScope())
+ {
+ Assert.That(DataMapper.ToRequest(element), Is.SameAs(element));
+ Assert.That(DataMapper.ToRequest(externalData), Is.TypeOf().With.Property("Id").EqualTo(externalData.Id));
+ Assert.That(DataMapper.ToRequest(externalRelationship), Is.TypeOf().With.Property("Language").EqualTo("the language"));
+ Assert.That(DataMapper.ToRequest(projectUsage), Is.TypeOf().With.Property("Id").EqualTo(projectUsage.Id));
+ }
+ }
+
+ private sealed class UnmodelledDataRequest : IDataRequest
+ {
+ }
+ }
+}
diff --git a/SysML2.NET.Tests/PSM/DTO/PimMappingTestFixture.cs b/SysML2.NET.Tests/PSM/DTO/PimMappingTestFixture.cs
index d1a3aebe9..d60fdc6ce 100644
--- a/SysML2.NET.Tests/PSM/DTO/PimMappingTestFixture.cs
+++ b/SysML2.NET.Tests/PSM/DTO/PimMappingTestFixture.cs
@@ -445,6 +445,130 @@ public void VerifyDataIdentityRoundTrip()
}
}
+ [Test]
+ public void VerifyDataVersionRequestRoundTrip()
+ {
+ Assert.That(() => new DataVersionRequest().FromPim(null), Throws.TypeOf());
+
+ var record = new PIM.DTO.DataVersion
+ {
+ Id = Guid.NewGuid(),
+ Alias = [.. TwoAliases],
+ Description = "the description",
+ Name = "the version",
+ Commit = Guid.NewGuid()
+ };
+
+ var empty = RoundTrip(record);
+
+ using (Assert.EnterMultipleScope())
+ {
+ Assert.That(empty.Identity, Is.Null);
+ Assert.That(empty.Payload, Is.Null);
+ Assert.That(empty.Id, Is.EqualTo(Guid.Empty));
+ Assert.That(empty.Commit, Is.EqualTo(Guid.Empty));
+ }
+
+ var identity = new PIM.DTO.DataIdentity { Id = Guid.NewGuid(), Name = "the identity" };
+ var payload = new PIM.DTO.ExternalData { Id = Guid.NewGuid(), ResourceIdentifier = "https://example.org/the-resource" };
+ record.Identity = identity;
+ record.Payload = payload;
+
+ var result = RoundTrip(record);
+
+ using (Assert.EnterMultipleScope())
+ {
+ Assert.That(result.Alias, Is.EqualTo(TwoAliases));
+ Assert.That(result.Description, Is.EqualTo(record.Description));
+ Assert.That(result.Name, Is.EqualTo(record.Name));
+ Assert.That(result.Identity.Id, Is.EqualTo(identity.Id));
+ Assert.That(result.Payload, Is.TypeOf());
+ Assert.That(result.Payload.Id, Is.EqualTo(payload.Id));
+ Assert.That(((PIM.DTO.ExternalData)result.Payload).ResourceIdentifier, Is.EqualTo(payload.ResourceIdentifier));
+ }
+ }
+
+ [Test]
+ public void VerifyCommitRequestRoundTrip()
+ {
+ var commitRequest = new CommitRequest();
+
+ using (Assert.EnterMultipleScope())
+ {
+ Assert.That(() => commitRequest.FromPim(null), Throws.TypeOf());
+ Assert.That(() => commitRequest.FromPim(new PIM.DTO.Commit(), null), Throws.TypeOf());
+ Assert.That(() => commitRequest.FromPim(null, []), Throws.TypeOf());
+ }
+
+ var record = new PIM.DTO.Commit
+ {
+ Id = Guid.NewGuid(),
+ Alias = [.. TwoAliases],
+ Description = "the description",
+ Name = "the commit",
+ Change = [Guid.NewGuid()]
+ };
+
+ commitRequest.FromPim(record);
+
+ using (Assert.EnterMultipleScope())
+ {
+ Assert.That(commitRequest.Name, Is.EqualTo(record.Name));
+ Assert.That(commitRequest.Change, Has.Count.EqualTo(0));
+ Assert.That(() => commitRequest.FromPim(record, []), Throws.TypeOf());
+ }
+
+ var (emptyCommit, emptyChange) = new CommitRequest().ToPimWithChange();
+
+ using (Assert.EnterMultipleScope())
+ {
+ Assert.That(emptyCommit.Change, Has.Count.EqualTo(0));
+ Assert.That(emptyChange, Has.Count.EqualTo(0));
+ }
+
+ var request = new CommitRequest
+ {
+ Alias = [.. TwoAliases],
+ Description = "the description",
+ Name = "the commit",
+ Change =
+ [
+ new DataVersionRequest { Name = "the first version", Identity = new DataIdentityRequest { Id = Guid.NewGuid() } },
+ new DataVersionRequest { Name = "the second version", Payload = new ExternalDataRequest { Id = Guid.NewGuid() } }
+ ]
+ };
+
+ var (commit, change) = request.ToPimWithChange();
+
+ using (Assert.EnterMultipleScope())
+ {
+ Assert.That(commit.Id, Is.Not.EqualTo(Guid.Empty));
+ Assert.That(commit.Alias, Is.EqualTo(TwoAliases));
+ Assert.That(commit.Name, Is.EqualTo(request.Name));
+ Assert.That(change, Has.Count.EqualTo(2));
+ Assert.That(commit.Change, Is.EqualTo(change.Select(version => version.Id)));
+ Assert.That(change.Select(version => version.Commit), Is.All.EqualTo(commit.Id));
+ Assert.That(change[0].Name, Is.EqualTo("the first version"));
+ Assert.That(change[0].Identity.Id, Is.EqualTo(request.Change[0].Identity.Id));
+ Assert.That(change[1].Payload, Is.TypeOf());
+ Assert.That(request.ToPim().Change, Has.Count.EqualTo(2));
+ }
+
+ var restored = new CommitRequest();
+ restored.FromPim(commit, [.. change.Reverse()]);
+
+ using (Assert.EnterMultipleScope())
+ {
+ Assert.That(restored.Alias, Is.EqualTo(TwoAliases));
+ Assert.That(restored.Description, Is.EqualTo(request.Description));
+ Assert.That(restored.Name, Is.EqualTo(request.Name));
+ Assert.That(restored.Change, Has.Count.EqualTo(2));
+ Assert.That(restored.Change[0].Name, Is.EqualTo("the first version"));
+ Assert.That(restored.Change[0].Identity.Id, Is.EqualTo(request.Change[0].Identity.Id));
+ Assert.That(restored.Change[1].Payload, Is.TypeOf());
+ }
+ }
+
[Test]
public void VerifyContractCoverage()
{
@@ -463,8 +587,8 @@ public void VerifyContractCoverage()
using (Assert.EnterMultipleScope())
{
- Assert.That(mappable, Has.Count.EqualTo(23));
- Assert.That(unmapped, Is.EquivalentTo(["Error", "CommitRequest", "DataVersionRequest", "MergeOutcome"]));
+ Assert.That(mappable, Has.Count.EqualTo(25));
+ Assert.That(unmapped, Is.EquivalentTo(["Error", "MergeOutcome"]));
Assert.That(mappable, Does.Contain("PrimitiveConstraint"));
}
}
diff --git a/SysML2.NET/PSM/DTO/DataMapper.cs b/SysML2.NET/PSM/DTO/DataMapper.cs
new file mode 100644
index 000000000..57418ee84
--- /dev/null
+++ b/SysML2.NET/PSM/DTO/DataMapper.cs
@@ -0,0 +1,63 @@
+// ------------------------------------------------------------------------------------------------
+//
+//
+// Copyright (C) 2022-2026 Starion Group S.A.
+// SPDX-License-Identifier: Apache-2.0
+//
+//
+// ------------------------------------------------------------------------------------------------
+
+namespace SysML2.NET.PSM.DTO
+{
+ using System;
+
+ using SysML2.NET.Common;
+
+ ///
+ /// Maps the payload of a data version between the Platform Independent Model and the REST/HTTP PSM request
+ ///
+ ///
+ /// The payload alternatives are reached through and ,
+ /// which carry no Platform Independent Model counterpart of their own, so the dispatch lives here.
+ ///
+ public static class DataMapper
+ {
+ ///
+ /// Maps a REST/HTTP PSM payload request onto its Platform Independent Model counterpart
+ ///
+ /// The payload to map.
+ /// The Platform Independent Model payload, or null when the payload is null.
+ /// Thrown when the payload is of an alternative the Platform Independent Model does not model.
+ public static IData ToPim(IDataRequest request)
+ {
+ return request switch
+ {
+ null => null,
+ ExternalDataRequest externalData => externalData.ToPim(),
+ ExternalRelationshipRequest externalRelationship => externalRelationship.ToPim(),
+ ProjectUsageRequest projectUsage => projectUsage.ToPim(),
+ IData element => element,
+ _ => throw new NotSupportedException($"A {request.GetType().Name} has no Platform Independent Model counterpart.")
+ };
+ }
+
+ ///
+ /// Maps a Platform Independent Model payload onto its REST/HTTP PSM request alternative
+ ///
+ /// The payload to map.
+ /// The request payload, or null when the payload is null.
+ /// Thrown when the payload is of a type the REST/HTTP PSM request does not model.
+ public static IDataRequest ToRequest(IData data)
+ {
+ return data switch
+ {
+ null => null,
+ PIM.DTO.ExternalData externalData => PimMapper.Map(externalData),
+ PIM.DTO.ExternalRelationship externalRelationship => PimMapper.Map(externalRelationship),
+ PIM.DTO.ProjectUsage projectUsage => PimMapper.Map(projectUsage),
+ IDataRequest element => element,
+ _ => throw new NotSupportedException($"A {data.GetType().Name} has no REST/HTTP PSM request counterpart.")
+ };
+ }
+ }
+}
diff --git a/SysML2.NET/PSM/DTO/IRequestOfTRecord.cs b/SysML2.NET/PSM/DTO/IRequestOfTRecord.cs
index db9a160a5..98f9d2b63 100644
--- a/SysML2.NET/PSM/DTO/IRequestOfTRecord.cs
+++ b/SysML2.NET/PSM/DTO/IRequestOfTRecord.cs
@@ -15,9 +15,6 @@ namespace SysML2.NET.PSM.DTO
///
/// The Platform Independent Model type that this request model corresponds to.
///
- ///
- /// Requests whose payload the Platform Independent Model cannot carry implement the non generic only.
- ///
public interface IRequest : IRequest, IPimMappable
{
}
diff --git a/SysML2.NET/PSM/DTO/Mapping/CommitRequest.Mapping.cs b/SysML2.NET/PSM/DTO/Mapping/CommitRequest.Mapping.cs
new file mode 100644
index 000000000..6320245f6
--- /dev/null
+++ b/SysML2.NET/PSM/DTO/Mapping/CommitRequest.Mapping.cs
@@ -0,0 +1,108 @@
+// ------------------------------------------------------------------------------------------------
+//
+//
+// Copyright (C) 2022-2026 Starion Group S.A.
+// SPDX-License-Identifier: Apache-2.0
+//
+//
+// ------------------------------------------------------------------------------------------------
+
+namespace SysML2.NET.PSM.DTO
+{
+ using System;
+ using System.Collections.Generic;
+ using System.Linq;
+
+ ///
+ /// Declares the Platform Independent Model counterpart of the Commit request model
+ ///
+ ///
+ /// The request embeds its change while the Platform Independent Model commit references it, so the mapping yields
+ /// the commit together with its data versions, bound by identifiers generated at mapping time.
+ ///
+ public partial class CommitRequest : IRequest
+ {
+ ///
+ /// Converts this model into its Platform Independent Model record
+ ///
+ /// The commit, whose change references data versions that only returns.
+ /// Thrown when a change payload has no Platform Independent Model counterpart.
+ public PIM.DTO.Commit ToPim()
+ {
+ return this.ToPimWithChange().Commit;
+ }
+
+ ///
+ /// Converts this model into its Platform Independent Model commit and the data versions that commit changes
+ ///
+ /// The commit, and its change in the order of the request.
+ /// Thrown when a change payload has no Platform Independent Model counterpart.
+ public (PIM.DTO.Commit Commit, IReadOnlyList Change) ToPimWithChange()
+ {
+ var commit = new PIM.DTO.Commit
+ {
+ Id = Guid.NewGuid(),
+ Alias = [.. this.Alias ?? []],
+ Description = this.Description,
+ Name = this.Name
+ };
+
+ var change = (this.Change ?? []).Select(request =>
+ {
+ var version = request.ToPim();
+ version.Id = Guid.NewGuid();
+ version.Commit = commit.Id;
+ return version;
+ }).ToList();
+
+ commit.Change = [.. change.Select(version => version.Id)];
+
+ return (commit, change);
+ }
+
+ ///
+ /// Populates this model from its Platform Independent Model record, leaving the change empty
+ ///
+ /// The record to read from.
+ /// Thrown when the record is null.
+ public void FromPim(PIM.DTO.Commit record)
+ {
+ if (record == null)
+ {
+ throw new ArgumentNullException(nameof(record));
+ }
+
+ this.Alias = [.. record.Alias ?? []];
+ this.Description = record.Description;
+ this.Name = record.Name;
+ this.Change = [];
+ }
+
+ ///
+ /// Populates this model from its Platform Independent Model record and the data versions its change references
+ ///
+ /// The record to read from.
+ /// The data versions that resolve the record's change identifiers.
+ /// Thrown when the record or the data versions are null.
+ /// Thrown when a change identifier resolves to no data version, or two data versions share an identifier.
+ /// Thrown when a change payload has no REST/HTTP PSM request counterpart.
+ public void FromPim(PIM.DTO.Commit record, IEnumerable change)
+ {
+ if (change == null)
+ {
+ throw new ArgumentNullException(nameof(change));
+ }
+
+ this.FromPim(record);
+
+ var versions = change.ToDictionary(version => version.Id);
+
+ this.Change =
+ [
+ .. (record.Change ?? []).Select(identifier => versions.TryGetValue(identifier, out var version)
+ ? PimMapper.Map(version)
+ : throw new ArgumentException($"The change {identifier} resolves to no data version.", nameof(change)))
+ ];
+ }
+ }
+}
diff --git a/SysML2.NET/PSM/DTO/Mapping/DataVersionRequest.Mapping.cs b/SysML2.NET/PSM/DTO/Mapping/DataVersionRequest.Mapping.cs
new file mode 100644
index 000000000..be5054af2
--- /dev/null
+++ b/SysML2.NET/PSM/DTO/Mapping/DataVersionRequest.Mapping.cs
@@ -0,0 +1,59 @@
+// ------------------------------------------------------------------------------------------------
+//
+//
+// Copyright (C) 2022-2026 Starion Group S.A.
+// SPDX-License-Identifier: Apache-2.0
+//
+//
+// ------------------------------------------------------------------------------------------------
+
+namespace SysML2.NET.PSM.DTO
+{
+ using System;
+
+ ///
+ /// Declares the Platform Independent Model counterpart of the DataVersion request model
+ ///
+ ///
+ /// The request carries no identifier and no commit; supplies both when it maps its change.
+ ///
+ public partial class DataVersionRequest : IRequest
+ {
+ ///
+ /// Converts this model into its Platform Independent Model record
+ ///
+ /// The record carrying the members that the REST/HTTP PSM models.
+ /// Thrown when the payload has no Platform Independent Model counterpart.
+ public PIM.DTO.DataVersion ToPim()
+ {
+ return new PIM.DTO.DataVersion
+ {
+ Alias = [.. this.Alias ?? []],
+ Description = this.Description,
+ Identity = this.Identity?.ToPim(),
+ Name = this.Name,
+ Payload = DataMapper.ToPim(this.Payload)
+ };
+ }
+
+ ///
+ /// Populates this model from its Platform Independent Model record
+ ///
+ /// The record to read from.
+ /// Thrown when the record is null.
+ /// Thrown when the payload has no REST/HTTP PSM request counterpart.
+ public void FromPim(PIM.DTO.DataVersion record)
+ {
+ if (record == null)
+ {
+ throw new ArgumentNullException(nameof(record));
+ }
+
+ this.Alias = [.. record.Alias ?? []];
+ this.Description = record.Description;
+ this.Identity = PimMapper.Map(record.Identity);
+ this.Name = record.Name;
+ this.Payload = DataMapper.ToRequest(record.Payload);
+ }
+ }
+}