<a id="how-custom-functions-are-converted"></a>
<a id="custom-function-v2-rules"></a>
# How Custom Functions are converted

This page describes how TrialGrid converts a C# Custom Function from the V1 to the V2 Medidata Rave API, for those who want to know what the 'Convert to V2' button and the "Convert to V2 API" action change, and why a function may not convert. [Converting to the V2 API](custom_function.html.md#custom-function-v2-conversion) describes how to start a conversion, and how conversions are licensed.

<a id="the-approach"></a>
## The approach

TrialGrid makes only the changes V2 needs, and leaves everything else in the Source Code as it was, including its layout and comments.

1. **Rave says what to change.** TrialGrid validates the Source Code with Medidata Designer. For each V1 reference, Medidata Rave reports the line and column it is on, the V1 type or member, and usually the V2 replacement. Medidata Rave has compiled the code, so it knows the type of every object, which the Source Code alone often doesn't show.
2. **|appname| makes the change.** Where V2 keeps the same shape, the change is the name Medidata Rave suggests. Where the shape changes, for example an argument moves or a method becomes a property, a rule below makes it.
3. **Rave checks the result.** The converted Source Code is validated again. Medidata Rave reports some V1 references only once those around them have changed, so if any remain the conversion is repeated, up to five times. A compile error from an ID that V2 gives as a reference, rather than a number, is repaired the same way: see below.
4. **The result is checked before it is saved.** It must compile with no errors and no API Compatibility warnings, TrialGrid's own analysis must find no V1 left, and the function must use the same strings, call the same Custom Functions and perform the same actions as before. The arguments of each query, comment and sticky action are compared too.

If any step fails, the function is left unchanged and flagged with the reason. A function too long for Medidata Rave once converted is saved all the same, flagged for a person to shorten: see [What isn't converted](#what-isn-t-converted).

<a id="functions-that-call-each-other"></a>
## Functions that call each other

Custom Functions linked by *PerformCustomFunction*, as caller or callee, directly or through others, are converted as a group. A function that calls another by a name worked out when the code runs is treated as linked to every function its strings name.

A V2 function can call a V1 one, so a function that can't be converted doesn't stop the rest of its group. It stays V1, and so do the functions it calls, and the functions they call in turn, so that none of them returns a V2 list to a V1 function. A V2 list can't stand in for a V1 collection such as `DataPoints`. A function that calls one staying V1 is converted, unless it may pass that function a list. Only a literal, a calculation, the text from a string method such as `ToString()`, or a variable declared as a type that can't hold a list, is taken not to be one. A variable declared `var` or `object`, or a value read from another object, may be a list. A function that casts what one staying V1 returns to a list is held back too, whether it casts the call itself or a variable it keeps the result in. A V1 function's `DataPoints` can be read as an `IReadOnlyList<IDataPoint>`, but no other V1 list has been shown to be a V2 one. Each function held back this way is flagged with the name of the function that held it.

A group is saved only if none of its functions changed while it was being converted, including those that stay V1, since they decide which functions are held back.

A call keeps its arguments: `CustomFunction.PerformCustomFunction(name, version, args)` becomes `Rave.PerformCustomFunction(name, version, args)`, and the called function reads its arguments as before.

<a id="names-and-types"></a>
## Names and types

- **Types.** Medidata Rave reports a V1 type once, so every use of it is changed: declarations, casts, generic arguments, arrays, `typeof`, `as` and `is`. A V1 type Medidata Rave doesn't report at all, as it may not in a cast, is changed to what Medidata Rave has suggested for it in other functions. A type written with its namespace, such as `Medidata.Utilities.KeyValueCollection`, is replaced whole. V1 types from `Medidata.Core.Objects`, `Medidata.Utilities` and other Medidata namespaces are all recognised.
- **The running check.** `ActionFunctionParams afp = (ActionFunctionParams)ThisObject;` becomes `IActionFunctionParams afp = Rave.Context.ActionFunctionParams;`, and a member read through it, such as the action data point, becomes the matching `Rave.Context` property.
- **Renamed members** take their V2 name, for example `DataPoints` on a record becomes `AllDataPoints` and `FindByFieldOID` becomes `FindByFieldOid`.
- **Static helpers moving to Rave**, such as `Number.IsValidInteger`, become their `Rave.*` form with the same arguments.
- **Methods that become properties**, such as `dp.StandardValue()`, lose their brackets.
- **IDs become references.** `x.ID` becomes `x.Ref` where Medidata Rave suggests it, but only where the ID is just passed on, so that a reference serves as the number did: to a call whose V2 form takes a reference, to one of the function's own methods, into a variable that is itself only passed on, or compared for equality with another ID. A number declared from one, such as `int v = subject.CRFVersionID;`, is declared with `var` so it can hold the reference, and a variable, field, parameter or method's result declared `int` that carries one is declared as the reference type, which Medidata Rave names when it compiles the converted code.
- **IDs meaning no object.** An ID compared with 0, meaning "no object", such as `dp.Variable.DataDictionaryID > 0`, becomes its reference compared with the reference type's default, `dp.Variable.DataDictionaryRef != default(DataDictionaryRef)`. That default is what V2 gives when there is no object, as 0 is in V1. Other numbers, such as -1, are flagged: some IDs have them as real values, so they don't mean "no object". A variable carrying a reference that is given 0 is given the default instead, and so is the 0 in `subject != null ? subject.CRFVersionID : 0`. An ID compared with `null` is still compared with `null`. That comparison always gave the same answer, since neither a number nor a reference can be null.
- **A field's CRF version.** V1 code often reads it as `dp.Field.CRFVersion.ID`. Medidata Rave says that ID, a library version's, has no V2 equivalent, but it is the field's `CRFVersionID`, so it becomes `dp.Field.CrfVersionRef` and is converted as any other ID.
- **IDs used any other way.** A function that does anything else with the ID is flagged. Examples are adding it to text, calling `ToString()` on it and reading the result, storing or passing it as an object, or returning it as the function's result. A reference has no number, and its text isn't the old number.
- **Members reached through** `?.`. Medidata Rave gives no API Compatibility warning for a V1 member read with `?.`, only a compile error once the code around it is V2. Where V2 has the member under another capitalisation, as `FindByFolderOID` is `FindByFolderOid`, it is renamed on the line the error gives.
- **Documented replacements.** Where Medidata documents a V2 replacement that Medidata Rave's warnings don't yet suggest, it is used: reading a user's `Active` becomes `Enabled`.
- **Freeze status.** V2 has no freeze display status. `dp.FreezeDisplayStatus == FreezeDisplayStatusEnum.Frozen` becomes `dp.IsFrozenRaw`, and comparing with `Unfrozen`, or with `!=`, becomes `!dp.IsFrozenRaw`. V2's `LockStatus` isn't used: a data point frozen and then locked is still frozen to V1, and its `LockStatus` is `Locked`. Any other use of the V1 values, or of `FreezeDisplayStatus` itself, such as comparing it with a number or using it as text, is flagged.

<a id="calls-whose-arguments-change"></a>
## Calls whose arguments change

- **Arguments follow their names.** Where the V2 method takes the same arguments in a different order, they are matched by name, so `CustomFunction.PerformQueryAction(text, group, ..., dp, ...)` becomes `Rave.Marking.PerformQueryAction(dp, text, group, ...)`. Where the names differ but the number is the same, they keep their positions.
- **A static call that becomes a method of its argument.** `CustomFunction.FetchAllDataPointsForOIDPath(field, form, folder, subject)` becomes `subject.GetDataPointsByOidPath(field, form, folder)`. V2 returns inactive data points too, so where V1 passed `activeOnly` as `true` the result is filtered to the active ones; `false` is simply dropped.
- **One method of several.** `Audits.Load(x)` becomes `x.FetchAudits()`, the method Medidata Rave names on whichever type `x` is.
- **Lookups by coded data.** V2's list of dictionary entries has no `FindByCodedData`, and V1's ignores case, so `entries.FindByCodedData(code)` becomes `entries.FirstOrDefault(entry => string.Equals(entry.CodedData, code, StringComparison.OrdinalIgnoreCase))`. The lookup works the code out again for each entry, so a code that changes something or calls a method other than a string method's, such as `prefix + ++i`, is flagged.
- **A page's container.** V1's `page.Parent` is the page's folder, or its subject for a page at the subject's level. V2 gives every page a `Subject`, and only a page in a folder an `Instance`, so `page.Parent is Instance` becomes `page.Instance != null` and `page.Parent is Subject` becomes `page.Instance == null`. Any other use of `Parent` is flagged.
- **Doing or undoing.** A method taking a Boolean that V2 splits in two, such as `dp.Freeze(true)` and `dp.Freeze(false)`, becomes `dp.Freeze()` or `dp.UnFreeze()`; likewise `Lock` and `Unlock`. A variable chooses between them when the code runs.
- **Arguments V1 left out.** V2 allows `dp.Verify(isVerified)` only with all three of its arguments written, so it becomes `dp.Verify(isVerified, false, false)`.
- **Arguments V2 doesn't need.** V2 knows who is calling, so a V1 method's interaction argument is dropped: `Users.LoadAllUsers(interaction)` becomes `Rave.Configuration.FetchAllUsers()`.
- **Arguments passed with out or ref.** A variable passed as `out` or `ref` is declared with the type the V2 method's parameter has, which must match it exactly.

<a id="creating-objects"></a>
## Creating objects

- **Data pages and folders.** `new DataPage(instance, form, matrix)`, and `instance.AddCRF(form, matrix)`, become `instance.AddDataPage(form)`, which adds the page to the instance itself. Likewise `new Instance(parent, folder, matrix)` becomes `parent.AddInstance(folder)`. The subject's V1 overloads add to the container they are given: `subject.AddCRF(parent, form, matrix)` becomes `parent.AddDataPage(form)`, and `subject.AddInstance(parent, folder, matrix)` becomes `parent.AddInstance(folder)`. The V1 `DataPages.Add(page)` or `Instances.Add(instance)` straight after is removed, since V2 has already added it, and so is one guarded only by `if (page != null)`. V1's matrix isn't passed, since V2's `AddDataPage(form)` and `AddInstance(folder)` take none. V1 ignores the matrix it is given, placing the page or folder as V2's methods do, whether it is passed a subject matrix's ID, a matrix's, a folder's, a CRF version's or 0. The matrix is left out wherever reading it changes nothing. A function that works it out with a method call is flagged, since leaving it out would skip the call.
- **Key value pairs.** `list.Add(new KeyValue(key, value))` becomes `list.Add(key, value)`. A variable that holds a pair only so it can be added goes: `pair = new KeyValue(key, value); list.Add(pair);` becomes `list.Add(key, value);`, and so does setting `pair.Key` and `pair.Value` on the lines before the `Add`. In the second form the pair must be created anew for each `Add`: V1 adds one pair whose values change as the same object each time, so the list ends up holding the last values more than once, and such a function is left for a person. V2 can't create a pair anywhere else. A pair kept in an `ArrayList` and only ever read back with `ToString()`, which gives its value, is replaced by the text: `items.Add(new KeyValue(text, text))` becomes `items.Add(text)`. This needs the key and the value to be the same, and every `ArrayList` in the function to stay in variables and methods declared `ArrayList`. A pair the function keeps for itself becomes .NET's `KeyValuePair<string, string>`, which has the same `Key` and `Value`: `items.Add(new KeyValue(form, field))` and `KeyValue pair = (KeyValue)item;` become the same with `KeyValuePair<string, string>`. This needs every item read from an `ArrayList` to be cast back to a pair, and every pair to be read only for its `Key` and `Value`, since a `KeyValuePair` is never null and its text isn't V1's. Any other pair kept as an object is flagged.
- **Collections.** A V1 collection, such as `DataPoints`, becomes a read-only list in V2, as V2's methods return, even where Medidata Rave suggests a `List`. Where the function adds to, removes from or sorts one, it is declared as a `List` instead, holding a copy of what it is given. Variables declared together all become `List` when any of them is changed. `new DataPoints()` becomes `new List<IDataPoint>()`. A V2 method returning a sequence rather than a list, such as `FilterDataPagesByFormOid`, has `.ToList()` added so its result can still be counted and indexed. A V1 collection was also an `ICollection`, which a V2 list isn't, so a V2 list passed where one is taken, as in `new ArrayList(dps)`, is passed as a copy: `new ArrayList(dps.ToList())`. A data dictionary's `EntriesCount` becomes the `Count` of its `Entries`.
- **Factories.** A V1 constructor that V2 replaces with a factory, such as `new KeyValueCollection()`, becomes the factory call, here `Rave.Edc.CreateKeyValueCollection()`.

<a id="statements"></a>
## Statements

- **Setting Active.** For a data point, record, data page or folder, V2 has no setter: `x.Active = true;` becomes `x.Activate(null);`, `false` becomes `x.InActivate(null);`, and a variable chooses between them when the code runs. Reading `Active` is unchanged.
- **The latest entered value.** A common helper keeps a data point's entered values in a `SortedList` keyed by audit ID, so that the last is the latest. V2's audits have no ID, and Medidata advise ordering by creation time. The audits are therefore ordered newest first by `AuditTime`, and each is keyed by its place counted from the end, so the newest still sorts last. This needs the list to be only added to, counted and read by index, and the audits to be loaded once and walked by index from the start; otherwise the function is flagged.
- **Static helper methods.** A function's own static helper methods could call V1's static methods, but they can't reach `Rave`. If any of them uses it after converting, none of the function's helper methods stays static.

<a id="what-isn-t-converted"></a>
## What isn't converted

A function is flagged rather than converted when it uses:

- something Medidata Rave says has no V2 equivalent, such as `LibraryVersion.ID` or setting a user's `Active`;
- an endpoint Medidata Rave 2025.2.0 removed, or database access, which needs replacing with a stored procedure or another approach;
- a V1 member or method whose V2 replacement has a different shape that no rule above covers;
- `subject.GetDataPointByFieldName(name)`. It finds the data points of every form with a field of that draft field name, where V2's `GetDataPointsByField` takes one field, found by its OID;
- sorting a `KeyValueCollection`. V2's collection can't be sorted, read back or listed. V1's `Sort()` orders the pairs by value, ignoring case, so adding them in that order gives the same result.

It is also flagged, without converting, when its source is already longer than the 8,000 characters Medidata Rave takes.

V2's names are longer than V1's, so a function can go over the limit once converted. It is then laid out as the editor's Auto-indent button does, if that brings it under. If it is still too long, it is saved converted for a person to shorten, provided no V1 is left and it keeps its strings, calls and actions. Designer checks a copy without comments or layout when that fits. The flag is **Too long** if Designer found nothing in it, **Too long, with problems** if Designer found problems, which the flag lists, and **Too long, unchecked** if even the copy is too long to check. A **Too long** flag goes once the function fits, though only comments or layout were removed to make it fit. The others stay until the code itself changes, since the problems are still in it or it hasn't been checked.

The flag gives the reason, and its detail lists each reference with its line.

<a id="what-has-been-checked-in-a-study"></a>
## What has been checked in a study

The conversion targets the latest Medidata Rave, currently 2026.3.0, and converted code is checked against it. On an older Medidata Rave it can behave differently, so test converted Custom Functions on your own Medidata Rave before you use them. On 2026.1.0, for example, reading the `DataDictionaryRef` of a variable with no data dictionary throws an error, where the latest Medidata Rave gives the default. A function that compared `DataDictionaryID` with 0 to skip such variables, and was converted as described under "IDs meaning no object", then throws on exactly those variables. `Rave.Architect.FetchFieldByOid` likewise throws on 2026.1.0 for an OID that no field has, where the latest gives null.

Some rules can't be shown correct by compiling, so these have been checked on the latest Medidata Rave, by running a V1 and a V2 Custom Function side by side in a Medidata Rave study and comparing what each does:

- `AddDataPage(form)` and `AddInstance(folder)` add the new page or folder where V1's constructors did, at the subject's level or inside a folder, and return it;
- `IsFrozenRaw` gives the answer V1's freeze status comparison did for a data point that is frozen, unfrozen, locked, or frozen and then locked;
- a page added to a folder is treated the same whichever subject matrix V1 passed: in a folder from a merged matrix, pages added with another object's subject matrix, with the folder's own and with V2's `AddDataPage(form)` all stay, active, when the matrix is unmerged;
- `GetDataPointsByOidPath` returns what V1's `FetchAllDataPointsForOIDPath` did, inactive data points included, and filtering it to the active ones matches V1's `activeOnly`;
- setting a record's `Active` and calling `InActivate(null)` or `Activate(null)` do the same, and an inactive record still holds its fields;
- two references to the same data point are equal and two to different data points are not, as their IDs were;
- `Verify` written with its two further arguments `false` verifies and unverifies a data point as V1's one-argument call does, including for a user whose role has no permission to verify: both verify the data point;
- `Freeze()`, `UnFreeze()`, `Lock()` and `Unlock()` do what V1's `Freeze` and `Lock` did with `true` and `false`;
- `FetchAudits()` returns the audits `Audits.Load` did, in the same order, newest first;
- `FilterDataPagesByFormOid(...).ToList()` holds the pages V1's `FilterDataPagesByFormOID` did, in the same order;
- a Custom Function called through `Rave.PerformCustomFunction` receives the arguments it was passed, as it did in V1;
- a V2 Custom Function can call a V1 one, and read a `DataPoints` it returns as an `IReadOnlyList<IDataPoint>`, but a V2 list it passes isn't a V1 collection;
- V1's `AddCRF`, `AddInstance`, `new DataPage` and `new Instance` place the new page or folder as `AddDataPage(form)` and `AddInstance(folder)` do, whether they are passed a subject matrix's ID, a matrix's, a folder's, a CRF version's or 0;
- ordering a data point's audits newest first by `AuditTime` gives the order `FetchAudits()` does;
- a field with no data dictionary has a `DataDictionaryRef` equal to `default(DataDictionaryRef)`;
- `Rave.Configuration.FetchAllUsers()` returns the users V1's `Users.LoadAllUsers(interaction)` did;
- a field's `CRFVersion.ID` is its `CRFVersionID`, and finds the same variable as V2's `CrfVersionRef`;
- a data dictionary's `EntriesCount` is the number of its `Entries`, as V2's `Entries.Count` is;
- V1's `KeyValueCollection.Sort()` orders the pairs by value, ignoring case;
- V1's `FindByCodedData` ignores case;
- V1's `page.Parent` is an `Instance` for a page in a folder and the `Subject` for one at the subject's level, and V2's `page.Instance` is null only for the second;
- V1's `GetDataPointByFieldName` matches the draft field name on every form, which V2's field found by OID doesn't;
- a user's `Enabled` gives the same answer as V1's `Active` for an active user, and for an inactive one in Rave Classic, the only place an inactive user was compared;
- a V1 `KeyValue` read with `ToString()` gives its value.

Converted Custom Functions should be tested like any other change before they are used in production.
