Azure Mobile Service Lookupasync load navigation properties - azure

I have a Place data_object in the service side which contains a navigation property Roads:
public class Place : EntityData
{
...
public List<Road> Roads { get; set; }
}
And now on the client side, I want to get a Place object using its id, but the navigation property Roads just won't load. Is there any parameter or attribute I can add to make it work?
My code for it:
var roadList = await App.MobileService.GetTable<Place>()
.LookupAsync(placeId);

Since loading navigation properties in EF requires a JOIN operation in the database (which is expensive), by default they are not loaded, as you noticed. If you want them to be loaded, you need to request that from the client, by sending the $expand=<propertyName> query string parameter.
There are two ways of implementing this: in the server and in the client. If you want to do that in the server, you can implement an action filter which will modify the client request and add that query string parameter. You can do that by using the filter below:
[AttributeUsage(AttributeTargets.Method, AllowMultiple = true)]
class ExpandPropertyAttribute : ActionFilterAttribute
{
string propertyName;
public ExpandPropertyAttribute(string propertyName)
{
this.propertyName = propertyName;
}
public override void OnActionExecuting(HttpActionContext actionContext)
{
base.OnActionExecuting(actionContext);
var uriBuilder = new UriBuilder(actionContext.Request.RequestUri);
var queryParams = uriBuilder.Query.TrimStart('?').Split(new[] { '&' }, StringSplitOptions.RemoveEmptyEntries).ToList();
int expandIndex = -1;
for (var i = 0; i < queryParams.Count; i++)
{
if (queryParams[i].StartsWith("$expand", StringComparison.Ordinal))
{
expandIndex = i;
break;
}
}
if (expandIndex < 0)
{
queryParams.Add("$expand=" + this.propertyName);
}
else
{
queryParams[expandIndex] = queryParams[expandIndex] + "," + propertyName;
}
uriBuilder.Query = string.Join("&", queryParams);
actionContext.Request.RequestUri = uriBuilder.Uri;
}
}
And then you can decorate your method with that attribute:
[ExpandProperty("Roads")]
public SingleItem<Place> GetPlace(string id) {
return base.Lookup(id);
}
Another way to implement this is to change the client-side code to send that header. Currently the overload of LookupAsync (and all other CRUD operations) that takes additional query string parameters cannot be used to add the $expand parameter (or any other $-* parameter), so you need to use a handler for that. For example, this is one such a handler:
class MyExpandPropertyHandler : DelegatingHandler
{
string tableName
string propertyName;
public MyExpandPropertyHandler(string tableName, string propertyName)
{
this.tableName = tableName;
this.propertyName = propertyName;
}
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
if (request.Method.Method == HttpMethod.Get.Method &&
request.RequestUri.PathAndQuery.StartsWith("/tables/" + tableName, StringComparison.OrdinalIgnoreCase))
{
UriBuilder builder = new UriBuilder(request.RequestUri);
string query = builder.Query;
if (!query.Contains("$expand"))
{
if (string.IsNullOrEmpty(query))
{
query = "";
}
else
{
query = query + "&";
}
query = query + "$expand=" + propertyName;
builder.Query = query.TrimStart('?');
request.RequestUri = builder.Uri;
}
}
return await base.SendAsync(request, cancellationToken);
return result;
}
}
And you'd use the handler by creating a new instance of MobileServiceClient:
var expandedClient = new MobileServiceClient(
App.MobileService.ApplicationUrl,
App.MobileService.ApplicationKey,
new MyExpandPropertyHandler("Place", "Roads"));
var roadList = await App.MobileService.GetTable<Place>()
.LookupAsync(placeId);

Related

Can you expose azure table storage IQueryable ( table.CreateQuery() ) as POCO?

I have an large application with 30 plus projects that currently uses an IRepository and POCO entities. I would like to know if there is a way to use table storage without having to implement ITableEntity. I don't want to have to import the azure storage nugget packages into every project and change all my entities to use ITableEntity.
Entity Adapater
I am aware that it is possible to create an entity adapter (such as that below) which works quite well when reading or writing an individual entity. But I have not been able to get this to work when attempting to expose IQueryable via table.CreateQuery().
public class AzureEntity
{
public Guid Id { get; set; }
public string PartitionKey { get; set; }
public string RowKey { get; set; }
public DateTimeOffset Timestamp { get; set; }
public string ETag { get; set; }
}
internal class AzureStorageEntityAdapter<T> : ITableEntity where T : AzureEntity, new()
{
#region Properties
/// <summary>
/// Gets or sets the entity's partition key
/// </summary>
public string PartitionKey
{
get { return InnerObject.PartitionKey; }
set { InnerObject.PartitionKey = value; }
}
/// <summary>
/// Gets or sets the entity's row key.
/// </summary>
public string RowKey
{
get { return InnerObject.RowKey; }
set { InnerObject.RowKey = value; }
}
/// <summary>
/// Gets or sets the entity's Timestamp.
/// </summary>
public DateTimeOffset Timestamp
{
get { return InnerObject.Timestamp; }
set { InnerObject.Timestamp = value; }
}
/// <summary>
/// Gets or sets the entity's current ETag.
/// Set this value to '*' in order to blindly overwrite an entity as part of an update operation.
/// </summary>
public string ETag
{
get { return InnerObject.ETag; }
set { InnerObject.ETag = value; }
}
/// <summary>
/// Place holder for the original entity
/// </summary>
public T InnerObject { get; set; }
#endregion
#region Ctor
public AzureStorageEntityAdapter()
{
// If you would like to work with objects that do not have a default Ctor you can use (T)Activator.CreateInstance(typeof(T));
this.InnerObject = new T();
}
public AzureStorageEntityAdapter(T innerObject)
{
this.InnerObject = innerObject;
}
#endregion
#region Methods
public virtual void ReadEntity(IDictionary<string, EntityProperty> properties, OperationContext operationContext)
{
TableEntity.ReadUserObject(this.InnerObject, properties, operationContext);
}
public virtual IDictionary<string, EntityProperty> WriteEntity(OperationContext operationContext)
{
return TableEntity.WriteUserObject(this.InnerObject, operationContext);
}
#endregion
}
I would like to be able to do something like this...
public class TableStorageRepository : IRepository
{
// snip...
public IQueryable<T> FindAll<T>() where T : class, new()
{
CloudTable table = GetCloudTable<T>();
return table.CreateQuery<AzureStorageEntityAdapter<T>>();
}
// snip...
}
The problem here is the CreateQuery creates an
IQueryable<AzureStorageEntityApater<T>>.
I can't see how to get an IQueryable of all the 'InnerObjects'.
Does anybody know if it is possible to expose IQueryable by some means without exposting ITableEntity?
This may or may not be what you want, but might give you some ideas.
I created a base repository and used a separate repository for each entity which is responsible for passing the correct CloudTable, partition/rowkey/property expressions and resolvers in. I base it around DynamicTableEntity (which allows for some advanced stuff like dynamic properties in my entities (like small collections)).
public class BaseRepository
{
protected async Task<DynamicTableEntity> GetAsync(CloudTable table, Expression<Func<DynamicTableEntity, bool>> filter)
{
var query = table.CreateQuery<DynamicTableEntity>().Where(filter).AsTableQuery();
var segment = await query.ExecuteSegmentedAsync(null);
return segment.Results.FirstOrDefault();
}
protected async Task<T> GetAsync<T>(CloudTable table, Expression<Func<DynamicTableEntity, bool>> filter, EntityResolver<T> resolver)
{
var query = table.CreateQuery<DynamicTableEntity>().Where(filter).Resolve(resolver);
var segment = await query.ExecuteSegmentedAsync(null);
return segment.Results.FirstOrDefault();
}
protected async Task<IEnumerable<DynamicTableEntity>> GetAllAsync(CloudTable table, Expression<Func<DynamicTableEntity, bool>> filter, int take = 1000)
{
if (take > 10000) take = 10000;
if (take < 1) take = 1;
var query = table.CreateQuery<DynamicTableEntity>().Where(filter).Take(take).AsTableQuery();
var token = new TableContinuationToken();
var results = new List<DynamicTableEntity>();
while (token != null)
{
var segment = await query.ExecuteSegmentedAsync(token);
results.AddRange(segment.Results);
token = segment.ContinuationToken;
}
return results;
}
protected async Task<IEnumerable<T>> GetAllAsync<T>(CloudTable table, Expression<Func<DynamicTableEntity, bool>> filter, EntityResolver<T> resolver, int take = 1000)
{
if (take > 10000) take = 10000;
if (take < 1) take = 1;
var query = table.CreateQuery<DynamicTableEntity>().Where(filter).Take(take).Resolve(resolver);
var token = new TableContinuationToken();
var results = new List<T>();
while (token != null)
{
var segment = await query.ExecuteSegmentedAsync(token);
results.AddRange(segment.Results);
token = segment.ContinuationToken;
}
return results;
}
protected async Task<int> InsertAsync(CloudTable table, DynamicTableEntity entity)
{
try
{
var result = await table.ExecuteAsync(TableOperation.Insert(entity));
return result.HttpStatusCode;
}
catch (StorageException ex)
{
return ex.RequestInformation.HttpStatusCode;
}
catch (Exception ex)
{
return 500;
}
}
protected async Task<int> ReplaceAsync(CloudTable table, DynamicTableEntity entity)
{
try
{
var result = await table.ExecuteAsync(TableOperation.Replace(entity));
return result.HttpStatusCode;
}
catch (StorageException ex)
{
return ex.RequestInformation.HttpStatusCode;
}
catch (Exception ex)
{
return 500;
}
}
protected async Task<int> DeleteAsync(CloudTable table, DynamicTableEntity entity)
{
try
{
var result = await table.ExecuteAsync(TableOperation.Delete(entity));
return result.HttpStatusCode;
}
catch (StorageException ex)
{
return ex.RequestInformation.HttpStatusCode;
}
catch (Exception ex)
{
return 500;
}
}
protected async Task<int> MergeAsync(CloudTable table, DynamicTableEntity entity)
{
try
{
var result = await table.ExecuteAsync(TableOperation.Merge(entity));
return result.HttpStatusCode;
}
catch (StorageException ex)
{
return ex.RequestInformation.HttpStatusCode;
}
catch (Exception ex)
{
return 500;
}
}
}
Example of a class inheriting from it (you'll have to use your imagination to fill in the blanks - let me know if you want to see a proper implementation)
// method
public Task<IEnumerable<T>> GetAllAsync<T>(string pk1, string pk2, EntityResolver<T> resolver, int take = 1000, Expression<Func<DynamicTableEntity, bool>> filterExpr = null)
{
var keysExpr = x => x.PartitionKey.Equals(string.Format("{0}_{1}", pk1, pk2);
var queryExpr = filterExpr != null ? keysExpr.AndAlso(filterExpr) : keysExpr;
return base.GetAllAsync<T>(CloudTableSelector.GetTable(), queryExpr, resolver, take);
}
// call
var products = await ProductRepo.GetAllAsync<ProductOwnerViewDto>(orgType, orgId, ProductOwnerViewDto.GetResolver(), take, x => x.RowKey.CompareTo(fromId) > 0);
It's a bit raw and a pain to wrap all your entities in separate repos, but I couldn't find a way to let me query the table in such a manner whilst letting me get different projections out (multiple resolvers per table).
I find solutions based on ITableEntity limiting (which is fine until you need dynamic properties then you're screwed).

Azure notification hub tags not creating nor updating - to target specific user

Hi I am working on web api as back-end service where I am using Azure notification hub. I need to notify logged in user according to conditional business logic, in short target specific user. I extract code from this article. Everything works fine but tags is not creating nor updating. I need help. Here is my code snippet
// It returns registrationId
public async Task<OperationResult<string>> GetRegistrationIdAsync(string handle)
{
........
}
// actual device registration and tag update
public async Task<OperationResult<RegistrationOutput>> RegisterDeviceAsync(string userName, string registrationId, Platforms platform, string handle)
{
...........
registration.Tags.Add(string.Format("username : {0}", userName)); // THIS TAG IS NOT CREATING
await _hub.CreateOrUpdateRegistrationAsync(registration);
...........
}
// Send notification - target specific user
public async Task<OperationResult<bool>> Send(Platforms platform, string userName, INotificationMessage message)
{
...........
}
Just after submitting this question I tried updating tags from VS notification explorer. There I found that tags does not allowed blank spaces and my tags format in api call has spaces. These spaces are the main culprit. API call silently ignore these invalid tag formats
Here is complete working implementation. Modify according to your need
public class MyAzureNotificationHubManager
{
private Microsoft.ServiceBus.Notifications.NotificationHubClient _hub;
public MyAzureNotificationHubManager()
{
_hub = MyAzureNotificationClient.Instance.Hub;
}
private const string TAGFORMAT = "username:{0}";
public async Task<string> GetRegistrationIdAsync(string handle)
{
if (string.IsNullOrEmpty(handle))
throw new ArgumentNullException("handle could not be empty or null");
// This is requied - to make uniform handle format, otherwise could have some issue.
handle = handle.ToUpper();
string newRegistrationId = null;
// make sure there are no existing registrations for this push handle (used for iOS and Android)
var registrations = await _hub.GetRegistrationsByChannelAsync(handle, 100);
foreach (RegistrationDescription registration in registrations)
{
if (newRegistrationId == null)
{
newRegistrationId = registration.RegistrationId;
}
else
{
await _hub.DeleteRegistrationAsync(registration);
}
}
if (newRegistrationId == null)
newRegistrationId = await _hub.CreateRegistrationIdAsync();
return newRegistrationId;
}
public async Task UnRegisterDeviceAsync(string handle)
{
if (string.IsNullOrEmpty(handle))
throw new ArgumentNullException("handle could not be empty or null");
// This is requied - to make uniform handle format, otherwise could have some issue.
handle = handle.ToUpper();
// remove all registration by that handle
var registrations = await _hub.GetRegistrationsByChannelAsync(handle, 100);
foreach (RegistrationDescription registration in registrations)
{
await _hub.DeleteRegistrationAsync(registration);
}
}
public async Task RegisterDeviceAsync(string userName, string registrationId, Platforms platform, string handle)
{
if (string.IsNullOrEmpty(handle))
throw new ArgumentNullException("handle could not be empty or null");
// This is requied - to make uniform handle format, otherwise could have some issue.
handle = handle.ToUpper();
RegistrationDescription registration = null;
switch (platform)
{
case Platforms.MPNS:
registration = new MpnsRegistrationDescription(handle);
break;
case Platforms.WNS:
registration = new WindowsRegistrationDescription(handle);
break;
case Platforms.APNS:
registration = new AppleRegistrationDescription(handle);
break;
case Platforms.GCM:
registration = new GcmRegistrationDescription(handle);
break;
default:
throw new ArgumentException("Invalid device platform. It should be one of 'mpns', 'wns', 'apns' or 'gcm'");
}
registration.RegistrationId = registrationId;
// add check if user is allowed to add these tags
registration.Tags = new HashSet<string>();
registration.Tags.Add(string.Format(TAGFORMAT, userName));
// collect final registration
var result = await _hub.CreateOrUpdateRegistrationAsync(registration);
var output = new RegistrationOutput()
{
Platform = platform,
Handle = handle,
ExpirationTime = result.ExpirationTime,
RegistrationId = result.RegistrationId
};
if (result.Tags != null)
{
output.Tags = result.Tags.ToList();
}
}
public async Task<bool> Send(Platforms platform, string receiverUserName, INotificationMessage message)
{
string[] tags = new[] { string.Format(TAGFORMAT, receiverUserName) };
NotificationOutcome outcome = null;
switch (platform)
{
// Windows 8.1 / Windows Phone 8.1
case Platforms.WNS:
outcome = await _hub.SendWindowsNativeNotificationAsync(message.GetWindowsMessage(), tags);
break;
case Platforms.APNS:
outcome = await _hub.SendAppleNativeNotificationAsync(message.GetAppleMessage(), tags);
break;
case Platforms.GCM:
outcome = await _hub.SendGcmNativeNotificationAsync(message.GetAndroidMessage(), tags);
break;
}
if (outcome != null)
{
if (!((outcome.State == NotificationOutcomeState.Abandoned) || (outcome.State == NotificationOutcomeState.Unknown)))
{
return true;
}
}
return false;
}
}
public class MyAzureNotificationClient
{
// Lock synchronization object
private static object syncLock = new object();
private static MyAzureNotificationClient _instance { get; set; }
// Singleton inistance
public static MyAzureNotificationClient Instance
{
get
{
if (_instance == null)
{
lock (syncLock)
{
if (_instance == null)
{
_instance = new MyAzureNotificationClient();
}
}
}
return _instance;
}
}
public NotificationHubClient Hub { get; private set; }
private MyAzureNotificationClient()
{
Hub = Microsoft.ServiceBus.Notifications.NotificationHubClient.CreateClientFromConnectionString("<full access notification connection string>", "<notification hub name>");
}
}
public interface INotificationMessage
{
string GetAppleMessage();
string GetAndroidMessage();
string GetWindowsMessage();
}
public class SampleMessage : INotificationMessage
{
public string Message { get; set; }
public string GetAndroidMessage()
{
var notif = JsonObject.Create()
.AddProperty("data", data =>
{
data.AddProperty("message", this.Message);
});
return notif.ToJson();
}
public string GetAppleMessage()
{
// Refer - https://github.com/paultyng/FluentJson.NET
var alert = JsonObject.Create()
.AddProperty("aps", aps =>
{
aps.AddProperty("alert", this.Message ?? "Your information");
});
return alert.ToJson();
}
public string GetWindowsMessage()
{
// Refer - http://improve.dk/xmldocument-fluent-interface/
var msg = new XmlObject()
.XmlDeclaration()
.Node("toast").Within()
.Node("visual").Within()
.Node("binding").Attribute("template", "ToastText01").Within()
.Node("text").InnerText("Message here");
return msg.GetOuterXml();
}
}

memberNames in ValidationResult not working as expected

I am performing some model validation via a model validator attached to the model at class level. If I find an error I need to be able to attach that error to the relevant field in the view so that it can be shown clearly to the user.
However simply passing in memberNames to the ValidationResult doesn't do anything. Instead what I have found is that I need to re-validate in the controller in order to then populate the ModelState object.
Here is the code:
public class CompletedMilestoneInCorrectOrderAttribute : ValidationAttribute
{
private const string DefaultErrorMessage = "Milestones cannot be completed out of sequence";
public CompletedMilestoneInCorrectOrderAttribute()
: base(DefaultErrorMessage)
{
}
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
var model = (RevisionEditViewModel)validationContext.ObjectInstance;
var previousCompleted = true;
var loop = 0;
var members = new List<string>();
foreach (var rm in model.RevisionMilestones)
{
if (rm.Completed && !previousCompleted)
{
members.Add("revisionMilestones[" + loop + "].ExpectedCompletionDate");
members.Add("revisionMilestones[" + loop + "].Completed");
}
if (!rm.NotApplicable)
{
previousCompleted = rm.Completed;
}
loop++;
}
if (members.Any())
{
return new ValidationResult(DefaultErrorMessage, members);
}
return null;
}
}
And in the controller
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Edit(RevisionEditViewModel model)
{
//without this code the error never gets attached to the correct field in the view
var validationContext = new ValidationContext(model, null, null);
var validationResults = new List<ValidationResult>();
Validator.TryValidateObject(model, validationContext, validationResults);
foreach (var validationResult in validationResults)
{
foreach (var memberName in validationResult.MemberNames)
{
ModelState.AddModelError(memberName, validationResult.ErrorMessage);
}
}
if (ModelState.IsValid)
{
*snip*
}
else
{
*snip*
}
}
Anyone know what is going on? How can I correct it so the messy code in the controller is no longer needed?
Cheers Mike

create sort on list for web API controller

I am writing my first web API controller so I am a bit of a noob in this area. I am trying to retrieve a list of data through a static class called CustomerDataSource:
public static class CustomerDataSource
{
public static List<Customer> customerData
{
get
{
Customer customer1 = new Customer() { name = "Bert", address = "London" };
Customer customer2 = new Customer() { name = "Jon", address = "New York" };
List<Customer> listCustomers = new List<Customer>();
listCustomers.Add(customer1);
listCustomers.Add(customer2);
return listCustomers;
}
}
}
public class Customer
{
public string name { get; set; }
public string address { get; set; }
}
I am a bit stuck with my ApiController because I am trying to sort the list either on 'name' or 'address' but using a string called 'field' does not compile. What would be a good implementation for a WebAPI controller GETmethod which provides for sorting on one of the Customer properties ?
public class ValuesController : ApiController
{
// GET api/values
public List<Customer> Get(string field)
{
var list = CustomerDataSource.customerData.OrderBy(field);
}
}
Create an extension method like below, then you can use it anywhere within the same namespace in your project.
public static class extensionmethods
{
public static IQueryable<T> OrderByPropertyName<T>(this IQueryable<T> q, string SortField, bool Ascending)
{
var param = Expression.Parameter(typeof(T), "p");
var prop = Expression.Property(param, SortField);
var exp = Expression.Lambda(prop, param);
string method = Ascending ? "OrderBy" : "OrderByDescending";
Type[] types = new Type[] { q.ElementType, exp.Body.Type };
var rs = Expression.Call(typeof(Queryable), method, types, q.Expression, exp);
return q.Provider.CreateQuery<T>(rs);
}
}
Then you can use it like:
public List<Customer> Get(string PropertyName)
{
var list = CustomerDataSource.customerData.AsQueryable().OrderByPropertyName("PropertyName",true).ToList();
}
Note:
Because the extension method uses IQueryable and returns IQuerybale, so you need to convert your List to IQueryable. Also you can order the list by ascending and descending order, just pass the boolean type value to the second parameter. The default is ascending.
You need to use a lambda expression.
if (field == "name")
var list = CustomerDataSource.customerData.OrderBy(d => d.name);
else if (field == "address")
var list = CustomerDataSource.customerData.OrderBy(d => d.address);

Neo4jClient is not returning properties for a node in .Return

I am trying to use Neo4jClient (and am new to C#) to build and retrieve data from Neo4j. First, I build the items and relations to a search:
NodeReference<Search> searchNode = client.Create(searches[i]);
itmNode = client.Create(items[j], new IRelationshipAllowingParticipantNode<Item>[0],
new[]
{
new IndexEntry("Item")
{
{"Type", items[j].Type },
{"ItemDescription", items[j].ItemDescription },
{"ItemNumber", items[j].ItemNumber }
}
});
client.CreateRelationship(itmNode, new SearchedFor(searchNode, 1));
Then, I am testing the retrieval of the node back from Neo4j:
var results = client.Cypher.Start("n", itemDict[firstitem])
.Match("n-[r]->()<-[r2]-other")
.Return<Node<Item>>("other")
.Results;
var node6 = ((IRawGraphClient)client).ExecuteGetCypherResults<Node<Item>>(new Neo4jClient.Cypher.CypherQuery("start n=node(6) return n;", null,Neo4jClient.Cypher.CypherResultMode.Set)).Select(un => un.Data);
"results" returns the 2 nodes that are related to node(6). "node6" is other code I found that I thought would return node 6. Both of these return the nodes, but the properties returned are all blank. I can see the properties in the Neo4j Monitoring Tool, but not when they are returned using Neo4jClient. Am I missing something in how I am setting up the nodes, or on how I am retrieving the data?
My object return shows Data.ItemDescription="", Data.ItemNumber=0, Reference=Node 5
Adding the "Select(un => un.Data)" after .Results did not work like I saw in other examples like this
Please let me know if you need more information.
Neo4jClient version 1.0.0.579
Neo4j version 1.8.2
Here is the item class:
public class Item
{
private string _name;
private string _desc;
private long _id;
public Item(string name, string desc, long id)
{
_name = name;
_desc = desc;
_id = id;
}
public Item()
{
_name = "";
_desc = "";
_id = 0;
}
public long ItemNumber
{
get
{
return _id;
}
}
public string ItemDescription
{
get
{
return _desc;
}
}
public string Type
{
get
{
return "Item";
}
}
}
Making it work
The issue is that your Item class has no setters exposed. There's no way for us to set those properties , so we ignore them.
You can delete half your code, and it'll work. :)
public class Item
{
public long ItemNumber { get; set; }
public string ItemDescription { get; set; }
public string Type { get ; set; }
}
Making it better
Replace this:
NodeReference<Search> searchNode = client.Create(searches[i]);
itmNode = client.Create(items[j], new IRelationshipAllowingParticipantNode<Item>[0],
new[]
{
new IndexEntry("Item")
{
{"Type", items[j].Type },
{"ItemDescription", items[j].ItemDescription },
{"ItemNumber", items[j].ItemNumber }
}
});
client.CreateRelationship(itmNode, new SearchedFor(searchNode, 1));
with this:
var searchNode = client.Create(searches[i]);
var itemNode = client.Create(
items[j],
new[] { new SearchedFor(searchNode, 1) }
new[]
{
new IndexEntry("Item")
{
{"Type", items[j].Type },
{"ItemDescription", items[j].ItemDescription },
{"ItemNumber", items[j].ItemNumber }
}
});
That will create that second node, and the relationship, in a single call.

Resources