I am running into a very strange issue where I cannot use the indexer of an IReadOnlyCollection when it is exposed via an interface property. Below is a code example of the problem. The second line of the script results in TypeError: Cannot read property 'SystemName' of undefined. E.g. the indexer is returning undefined. It seems like the indexer should work regardless of if I am accessing the host object directly or via an interface.
>
public interface IDataObject
{
Guid Id { get; set; }
string SystemName { get; set; }
string DisplayName { get; set; }
string Description { get; set; }
IReadOnlyCollection<SubDataObject> ChildrenReadOnly { get; }
}
public class DataObject : IDataObject
{
public DataObject()
{
Children = new List<SubDataObject>();
}
public Guid Id { get; set; }
public string SystemName { get; set; }
public string DisplayName { get; set; }
public string Description { get; set; }
public IReadOnlyCollection<SubDataObject> ChildrenReadOnly { get { return Children; } }
public Char CharTest { get; set; }
public int IntTest { get; set; }
public decimal DecimalTest { get; set; }
public byte ByteTest { get; set; }
public bool? BoolTest { get; set; }
public List<SubDataObject> Children { get; set; }
public string IntTestToString()
{
return IntTest.ToString();
}
public Func<string> DoSomething { get; set; }
}
public class SubDataObject
{
public DataObject Parent { get; set; }
public Guid Id { get; set; }
public string SystemName { get; set; }
public string DisplayName { get; set; }
public string Description { get; set; }
}
private static void ReproduceIReadOnlyCollectionIssue()
{
var dataObj = new DataObject
{
Id = Guid.NewGuid(),
SystemName = "Root Object System Name",
DisplayName = "Root Object Display Name",
Description = "Root Description",
};
dataObj.Children = new List<SubDataObject>
{
new SubDataObject
{
Parent = dataObj,
Id = Guid.NewGuid(),
SystemName = "Sub System Name 0",
DisplayName = "Sub Display Name 0",
Description = "Sub Description 0"
}
};
using (var engine = new V8ScriptEngine())
{
engine.AddHostType("Console", typeof(Console));
engine.AddHostObject("dataObj", dataObj);
engine.AddHostObject("readOnlyCollection", dataObj.ChildrenReadOnly);
const string script = @"
Console.WriteLine(readOnlyCollection[0].SystemName); //works fine
Console.WriteLine(dataObj.ChildrenReadOnly[0].SystemName); //indexer returns undefined.
";
engine.Execute(script);
}
}
Comments: Hello! This behavior is by design. Consider this line from your sample: ``` C# engine.AddHostObject("dataObj", dataObj); ``` With this setup, the expression `dataObj.ChildrenReadOnly` is of type `IReadOnlyCollection<SubDataObject>`. This type doesn't have an indexer, so applying indexing syntax to the result yields `undefined` in JavaScript and a compilation error in C#. To understand why it works in the other case, consider this line: ``` C# engine.AddHostObject("readOnlyCollection", dataObj.ChildrenReadOnly); ``` When you use `AddHostObject()`, you expose the given object's _runtime_ type, which in this case is `List<SubDataObject>` — a type that does have an indexer. Therefore `readOnlyCollection[0]` works as you expect. You may be wondering why ClearScript doesn't always expose the runtime types of managed objects. The reason has to do with C#-style method binding, which relies heavily on static typing. Most script languages don't support static typing, so ClearScript simulates it with a feature called type restriction. Type restriction is the default behavior in ClearScript, although `AddHostObject()` predates it and retains its original behavior for compatibility. A newer method, `AddRestrictedHostObject()`, serves a similar purpose but supports type restriction. In any case, you can use `ScriptMemberAttribute` to disable type restriction for an individual class member, or `ScriptEngine.DisableTypeRestriction` to turn it off globally. Keep in mind however that doing so may break certain method binding scenarios. Good luck!
>
public interface IDataObject
{
Guid Id { get; set; }
string SystemName { get; set; }
string DisplayName { get; set; }
string Description { get; set; }
IReadOnlyCollection<SubDataObject> ChildrenReadOnly { get; }
}
public class DataObject : IDataObject
{
public DataObject()
{
Children = new List<SubDataObject>();
}
public Guid Id { get; set; }
public string SystemName { get; set; }
public string DisplayName { get; set; }
public string Description { get; set; }
public IReadOnlyCollection<SubDataObject> ChildrenReadOnly { get { return Children; } }
public Char CharTest { get; set; }
public int IntTest { get; set; }
public decimal DecimalTest { get; set; }
public byte ByteTest { get; set; }
public bool? BoolTest { get; set; }
public List<SubDataObject> Children { get; set; }
public string IntTestToString()
{
return IntTest.ToString();
}
public Func<string> DoSomething { get; set; }
}
public class SubDataObject
{
public DataObject Parent { get; set; }
public Guid Id { get; set; }
public string SystemName { get; set; }
public string DisplayName { get; set; }
public string Description { get; set; }
}
private static void ReproduceIReadOnlyCollectionIssue()
{
var dataObj = new DataObject
{
Id = Guid.NewGuid(),
SystemName = "Root Object System Name",
DisplayName = "Root Object Display Name",
Description = "Root Description",
};
dataObj.Children = new List<SubDataObject>
{
new SubDataObject
{
Parent = dataObj,
Id = Guid.NewGuid(),
SystemName = "Sub System Name 0",
DisplayName = "Sub Display Name 0",
Description = "Sub Description 0"
}
};
using (var engine = new V8ScriptEngine())
{
engine.AddHostType("Console", typeof(Console));
engine.AddHostObject("dataObj", dataObj);
engine.AddHostObject("readOnlyCollection", dataObj.ChildrenReadOnly);
const string script = @"
Console.WriteLine(readOnlyCollection[0].SystemName); //works fine
Console.WriteLine(dataObj.ChildrenReadOnly[0].SystemName); //indexer returns undefined.
";
engine.Execute(script);
}
}
Comments: Hello! This behavior is by design. Consider this line from your sample: ``` C# engine.AddHostObject("dataObj", dataObj); ``` With this setup, the expression `dataObj.ChildrenReadOnly` is of type `IReadOnlyCollection<SubDataObject>`. This type doesn't have an indexer, so applying indexing syntax to the result yields `undefined` in JavaScript and a compilation error in C#. To understand why it works in the other case, consider this line: ``` C# engine.AddHostObject("readOnlyCollection", dataObj.ChildrenReadOnly); ``` When you use `AddHostObject()`, you expose the given object's _runtime_ type, which in this case is `List<SubDataObject>` — a type that does have an indexer. Therefore `readOnlyCollection[0]` works as you expect. You may be wondering why ClearScript doesn't always expose the runtime types of managed objects. The reason has to do with C#-style method binding, which relies heavily on static typing. Most script languages don't support static typing, so ClearScript simulates it with a feature called type restriction. Type restriction is the default behavior in ClearScript, although `AddHostObject()` predates it and retains its original behavior for compatibility. A newer method, `AddRestrictedHostObject()`, serves a similar purpose but supports type restriction. In any case, you can use `ScriptMemberAttribute` to disable type restriction for an individual class member, or `ScriptEngine.DisableTypeRestriction` to turn it off globally. Keep in mind however that doing so may break certain method binding scenarios. Good luck!