For some reason I need to save some big strings into user profiles. Because a property with type string has a limit to 400 caracters I decited to try with binary type (PropertyDataType.Binary) that allow a length of 7500. My ideea is to convert the string that I have into binary and save to property.
I create the property using the code :
context = ServerContext.GetContext(elevatedSite);
profileManager = new UserProfileManager(context);
profile = profileManager.GetUserProfile(userLoginName);
Property newProperty = profileManager.Properties.Create(false);
newProperty.Name = "aaa";
newProperty.DisplayName = "aaa";
newProperty.Type = PropertyDataType.Binary;
newProperty.Length = 7500;
newProperty.PrivacyPolicy = PrivacyPolicy.OptIn;
newProperty.DefaultPrivacy = Privacy.Organization;
profileManager.Properties.Add(newProperty);
myProperty = profile["aaa"];
profile.Commit();
The problem is that when I try to provide the value of byte[] type to the property I receive the error "Unable to cast object of type 'System.Byte' to type 'System.String'.". If I try to provide a string value I receive "Invalid Binary Value: Input must match binary byte[] data type."
Then my question is how to use this binary type ?
The code that I have :
SPUser user = elevatedWeb.CurrentUser;
ServerContext context = ServerContext.GetContext(HttpContext.Current);
UserProfileManager profileManager = new UserProfileManager(context);
UserProfile profile = GetUserProfile(elevatedSite, currentUserLoginName);
UserProfileValueCollection myProperty= profile[PropertyName];
myProperty.Value = StringToBinary(GenerateBigString());
and the functions for test :
private static string GenerateBigString()
{
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 750; i++) sb.Append("0123456789");
return sb.ToString();
}
private static byte[] StringToBinary(string theSource)
{
byte[] thebytes = new byte[7500];
thebytes = System.Text.Encoding.ASCII.GetBytes(theSource);
return thebytes;
}
Have you tried with smaller strings? Going max on the first test might hide other behaviors. When you inspect the generated string in the debugger, it fits the requirements? (7500 byte[])
For those, who are looking for answer. You must use Add method instead:
var context = ServerContext.GetContext(elevatedSite);
var profileManager = new UserProfileManager(context);
var profile = profileManager.GetUserProfile(userLoginName);
profile["MyPropertyName"].Add(StringToBinary("your cool string"));
profile.Commit();
Related
I'm using .NET Core 3 to read from device twins in an Azure IoT hub. I want to get property X and that property is, as always, both stored in the desired and reported properties. I want to get the one that's newer. This information is written in the metadata.
My question is, is this possible via just the IoT Hub Query Language or do I have to fetch from both desired and reported and check this out myself?
The Azure IoT Hub Query Language supports only the subset of the SQL statements, so the following example (device1 and twin property color) shows a workaround for missing a CASE statement:
query string to get the desired property as the lastUpdated:
querystring = $"SELECT devices.properties.desired.color FROM devices WHERE deviceId = 'device1' and devices.properties.desired.$metadata.color.$lastUpdated > devices.properties.reported.$metadata.color.$lastUpdated";
if the return value is empty, we have to make the second query to obtain a reported property such as:
querystring = $"SELECT devices.properties.reported.color FROM devices WHERE deviceId = 'device1' and devices.properties.reported.$metadata.color.$lastUpdated > devices.properties.desired.$metadata.color.$lastUpdated";
if the return value is still empty, there are missing our desired and/or reported property in the device twin or the deviceId is wrong.
The following code snippet shows an example of the above usage:
using Microsoft.Azure.Devices;
using System.Linq;
using System;
using System.Threading.Tasks;
namespace ConsoleApp3
{
class Program
{
static string connectionString = "*****";
static async Task Main(string[] args)
{
RegistryManager registryManager = RegistryManager.CreateFromConnectionString(connectionString);
string deviceId = "device1";
string propertyName = "color";
string querystring = $"SELECT devices.properties.desired.{propertyName} FROM devices WHERE deviceId = '{deviceId}' and devices.properties.desired.$metadata.{propertyName}.$lastUpdated > devices.properties.reported.$metadata.{propertyName}.$lastUpdated";
dynamic prop = null;
for (int ii = 0; ii < 2; ii++)
{
var query = registryManager.CreateQuery(querystring);
{
prop = (await query.GetNextAsJsonAsync())?.FirstOrDefault();
if (prop == null)
querystring = $"SELECT devices.properties.reported.{propertyName} FROM devices WHERE deviceId = '{deviceId}' and devices.properties.reported.$metadata.{propertyName}.$lastUpdated > devices.properties.desired.$metadata.{propertyName}.$lastUpdated";
else
break;
}
}
Console.WriteLine(prop ?? $"Not found property '{propertyName}' or device '{deviceId}'");
}
}
}
UPDATE:
In the case of multiple properties, we have to check each property individually by code in the fetched device twin entity. The following code snippet shows an example of this checking:
// multiple properties
querystring = $"SELECT devices.properties FROM devices WHERE deviceId='{deviceId}'";
var query2 = registryManager.CreateQuery(querystring);
JObject prop2 = JObject.Parse((await query2.GetNextAsJsonAsync())?.FirstOrDefault());
JToken desired = prop2.SelectToken("properties.desired");
JToken reported = prop2.SelectToken("properties.reported");
string pathLastUpdated = $"$metadata.{propertyName}.$lastUpdated";
var color = (DateTime)desired.SelectToken(pathLastUpdated) > (DateTime)reported.SelectToken(pathLastUpdated) ?
(string)desired[propertyName] : (string)reported[propertyName];
// more properties
Console.WriteLine(color);
also, you can create an extension class to simplify the code, see the following example:
public static class JObjectExtensions
{
public static T GetLastUpdated<T>(this JObject properties, string propertyName)
{
JToken desired = properties.SelectToken("properties.desired");
JToken reported = properties.SelectToken("properties.reported");
string pathLastUpdated = $"$metadata.{propertyName}.$lastUpdated";
return (DateTime)desired.SelectToken(pathLastUpdated) > (DateTime)reported.SelectToken(pathLastUpdated) ?
desired.SelectToken(propertyName).ToObject<T>() : reported.SelectToken(propertyName).ToObject<T>();
}
public static string GetLastUpdated(this JObject properties, string propertyName)
{
return GetLastUpdated<string>(properties, propertyName);
}
}
the following usage of the above extension shows how can be obtained any desired vs reported properties based on their lastUpdated timestamp:
color = prop2.GetLastUpdated(propertyName);
string color2 = prop2.GetLastUpdated("test.color");
var test = prop2.GetLastUpdated<JObject>("test");
string jsontext = prop2.GetLastUpdated<JObject>("test").ToString(Formatting.None);
var test2 = prop2.GetLastUpdated<Test>("test");
int counter = prop2.GetLastUpdated<int>("counter");
Note, that the exception is thrown in the case of property missing.
When you try and serialize a Guid that is empty (not null, but empty) the result will be omitted if you set ExcludeDefaultValues = true.
But, if you then set ExcludeDefaultValues = false it will generate the string ""
JsConfig.IncludeNullValues = false;
JsConfig.ExcludeDefaultValues = false;
var tt = new { Name="Fred", Value=Guid.Empty, Value2=Guid.NewGuid() };
var test = JSON.stringify(tt);
Console.WriteLine(test);
Gives
{"Name":"Fred","Value":"00000000000000000000000000000000","Value2":"598a6e08af224db9a08c2d0e2f6cff11"}
But we want the Guid's formatted as a Microsoft format Guid at the client end, so we add a serializer:
JsConfig.IncludeNullValues = false;
JsConfig.ExcludeDefaultValues = false;
JsConfig<Guid>.SerializeFn = guid => guid.ToString();
var tt = new { Name="Fred", Value=Guid.Empty, Value2=Guid.NewGuid() };
var test = JSON.stringify(tt);
Console.WriteLine(test);
Gives
{"Name":"Fred","Value2":"07a2d8c0-48ad-4e72-b6f3-4fec81c36a1d"}
So the presence of a SerializeFn seems to make it ignore the config settings so it's impossible to generate the empty Guid.
The same bug applies to numbers, so if (like us) you reformat all Double to three decimal places they are omitted if zero, which is wrong.
Has anyone found a workaround for this?
Stepping through the source, it appears you need to explicitly call out what Types you want to include the default value for if there is a SerializeFn for that Type. Source reference. Note the JsConfig<Guid>.IncludeDefaultValue = true; line below.
Example Source
JsConfig.Reset();
JsConfig.IncludeNullValues = false;
JsConfig.ExcludeDefaultValues = false;
JsConfig<Guid>.SerializeFn = guid => guid.ToString();
JsConfig<Guid>.IncludeDefaultValue = true;
var tt = new { Name = "Fred", Value = Guid.Empty, Value2 = Guid.NewGuid() };
var test = tt.ToJson();
Console.WriteLine(test);
Output
{"Name":"Fred","Value":"00000000-0000-0000-0000-000000000000","Value2":"b86c4a18-db07-42f8-b618-6263148219ad"}
Your problem statement: Gistlyn: Note how it does not return the default GUID in the console.
The answer proposed above: Gistlyn: Notice how it does return the default GUID in the console.
I am trying to send binary data, i.e. byte arrays, using yaml. According to the yaml documentation, Yaml Binary Type, this is supported. On the Java side I use SnakeYaml and if a value of byte[] is passed, then the yaml correctly gives !!binary.
This functionality does not seem to be supported "out of the box" in YamlDotNet. The below code snippet creates a sequence of integer values:
IDictionary<string, object> data = new Dictionary<string, object>();
const string value = ":| value: <XML> /n\n C:\\cat";
byte[] bytes = Encoding.UTF8.GetBytes(value);
data.Add(ValueKey, bytes);
// Turn the object representation into text
using (var output = new StringWriter())
{
var serializer = new Serializer();
serializer.Serialize(output, data);
return output.ToString();
}
Output like:
val:\r- 58\r- 124\r- 32\r- 118\r- 97\r- 108\r- 117\r- 101\r- 58\r- 32\r- 60\r- 88\r- 77\r- 76\r- 62\r- 32\r- 47\r- 110\r- 10\r- 32\r- 67\r- 58\r- 92\r- 99\r- 97\r- 116\r
But I would like something more like:
val: !!binary |-
OnwgdmFsdWU6IDxYTUw+IC9uCiBDOlxjYXQ=
Can anyone recommend a workaround?
The preferred way to add support for custom types is to use a custom IYamlTypeConverter. A possible implementation for the !!binary type would be:
public class ByteArrayConverter : IYamlTypeConverter
{
public bool Accepts(Type type)
{
// Return true to indicate that this converter is able to handle the byte[] type
return type == typeof(byte[]);
}
public object ReadYaml(IParser parser, Type type)
{
var scalar = (YamlDotNet.Core.Events.Scalar)parser.Current;
var bytes = Convert.FromBase64String(scalar.Value);
parser.MoveNext();
return bytes;
}
public void WriteYaml(IEmitter emitter, object value, Type type)
{
var bytes = (byte[])value;
emitter.Emit(new YamlDotNet.Core.Events.Scalar(
null,
"tag:yaml.org,2002:binary",
Convert.ToBase64String(bytes),
ScalarStyle.Plain,
false,
false
));
}
}
To use the converter in the Serializer, you simply need to register it using the following code:
var serializer = new Serializer();
serializer.RegisterTypeConverter(new ByteArrayConverter());
For the Deserializer, you also need to register the converter, but you also need to add a tag mapping to resolve the !!binary tag to the byte[] type:
var deserializer = new Deserializer();
deserializer.RegisterTagMapping("tag:yaml.org,2002:binary", typeof(byte[]));
deserializer.RegisterTypeConverter(new ByteArrayConverter());
A fully working example can be tried here
For anyone that's interested.... I fixed this by creating the string myself and adding the !!binary tag, and also doing some clean up. Below is the code.
ToYaml:
IDictionary<string, string> data = new Dictionary<string, string>();
string byteAsBase64Fromat = Convert.ToBase64String("The string to convert");
byteAsBase64Fromat = "!!binary |-\n" + byteAsBase64Fromat + "\n";
data.Add(ValueKey, byteAsBase64Fromat);
string yaml;
using (var output = new StringWriter())
{
var serializer = new Serializer();
serializer.Serialize(output, data);
yaml = output.ToString();
}
string yaml = yaml.Replace(">", "");
return yaml.Replace(Environment.NewLine + Environment.NewLine, Environment.NewLine);
And then back by:
string binaryText = ((YamlScalarNode)data.Children[new YamlScalarNode(ValueKey)]).Value
String value = Convert.FromBase64String(binaryText);
Is it possible to convert the following string to a Sharepoint API object like SPUser or SPUserValueField? (without parsing it)
"<my:Person xmlns:my=\"http://schemas.microsoft.com/office/infopath/2003/myXSD\"><my:DisplayName>devadmin</my:DisplayName><my:AccountId>GLINTT\\devadmin</my:AccountId><my:AccountType>User</my:AccountType></my:Person>"
Thanks,
David Esteves
Yes, the Microsoft.Office.Workflow.Utility assembly has Contact.ToContacts which will deserialize Person XML into an array of Contact instances.
http://msdn.microsoft.com/en-us/library/ms553588
-Oisin
Solved :)
(Just an example)
The following function retrieves the SPUser from person:
protected SPUser GetSPUserFromExtendedPropertiesDelegateTo(string xmnls_node)
{
StringBuilder oBuilder = new StringBuilder();
System.IO.StringWriter oStringWriter = new System.IO.StringWriter(oBuilder);
System.Xml.XmlTextWriter oXmlWriter = new System.Xml.XmlTextWriter(oStringWriter);
oXmlWriter.Formatting = System.Xml.Formatting.Indented;
byte[] byteArray = Encoding.ASCII.GetBytes(xmnls_node);
MemoryStream stream = new MemoryStream(byteArray);
System.IO.Stream s = (Stream)stream;
System.IO.StreamReader _xmlFile = new System.IO.StreamReader(s);
string _content = _xmlFile.ReadToEnd();
System.Xml.XmlDocument _doc = new System.Xml.XmlDocument();
_doc.LoadXml(_content);
System.Xml.XPath.XPathNavigator navigator = _doc.CreateNavigator();
System.Xml.XmlNamespaceManager manager = new System.Xml.XmlNamespaceManager(navigator.NameTable);
manager.AddNamespace("my", "http://schemas.microsoft.com/office/infopath/2003/myXSD");
System.Xml.XmlNode _node = _doc.SelectSingleNode("/my:Person/my:AccountId", manager);
if (_node != null)
{
return this.workflowProperties.Web.EnsureUser(_node.InnerText.ToString());
}
return null;
}
The situation:
I have a bunch of Terms in the Term Store and a list that uses them.
A lot of the terms have not been used yet, and are not available yet in the TaxonomyHiddenList.
If they are not there yet they don't have an ID, and I can not add them to a list item.
There is a method GetWSSIdOfTerm on Microsoft.SharePoint.Taxonomy.TaxonomyField that's supposed to return the ID of a term for a specific site.
This gives back IDs if the term has already been used and is present in the TaxonomyHiddenList, but if it's not then 0 is returned.
Is there any way to programmatically add terms to the TaxonomyHiddenList or force it happening?
Don't use
TaxonomyFieldValue tagValue = new TaxonomyFieldValue(termString);
myItem[tagsFieldName] = tagValue;"
because you will have errors when you want to crawl this item.
For setting value in a taxonomy field, you have just to use :
tagsField.SetFieldValue(myItem , myTerm);
myItem.Update();"
Regards
In case of usage
string termString = String.Concat(myTerm.GetDefaultLabel(1033),
TaxonomyField.TaxonomyGuidLabelDelimiter, myTerm.Id);
then during instantiation TaxonomyFieldValue
TaxonomyFieldValue tagValue = new TaxonomyFieldValue(termString);
exception will be thrown with message
Value does not fall within the expected range
You have additionally provide WssId to construct term string like shown below
// We don't know the WssId so default to -1
string termString = String.Concat("-1;#",myTerm.GetDefaultLabel(1033),
TaxonomyField.TaxonomyGuidLabelDelimiter, myTerm.Id);
On MSDN you can find how to create a Term and add it to TermSet. Sample is provided from TermSetItem class description. TermSet should have a method CreateTerm(name, lcid) inherited from TermSetItem. Therefore you can use it in the sample below int catch statement ie:
catch(...)
{
myTerm = termSet.CreateTerm(myTerm, 1030);
termStore.CommitAll();
}
As for assigning term to list, this code should work (i'm not sure about the name of the field "Tags", however it's easy to find out the proper internal name of the taxonomy field):
using (SPSite site = new SPSite("http://myUrl"))
{
using (SPWeb web = site.OpenWeb())
{
string tagsFieldName = "Tags";
string myListName = "MyList";
string myTermName = "myTerm";
SPListItem myItem = web.Lists[myListName].GetItemById(1);
TaxonomyField tagsField = (TaxonomyField) myList.Fields[tagsFieldName];
TaxonomySession session = new TaxonomySession(site);
TermStore termStore = session.TermStores[tagsField.SspId];
TermSet termSet = termStore.GetTermSet(tagsField.TermSetId);
Term myTerm = null;
try
{
myTerm = termSet.Terms[myTermName];
}
catch (ArgumentOutOfRangeException)
{
// ?
}
string termString = String.Concat(myTerm.GetDefaultLabel(1033),
TaxonomyField.TaxonomyGuidLabelDelimiter, myTerm.Id);
if (tagsField.AllowMultipleValues)
{
TaxonomyFieldValueCollection tagsValues = new TaxonomyFieldValueCollection(tagsField);
tagsValues.PopulateFromLabelGuidPairs(
String.Join(TaxonomyField.TaxonomyMultipleTermDelimiter.ToString(),
new[] { termString }));
myItem[tagsFieldName] = tagsValues;
}
else
{
TaxonomyFieldValue tagValue = new TaxonomyFieldValue(termString);
myItem[tagsFieldName] = tagValue;
}
myItem.Update();
}
}