Social Icons

twitter google plus linkedin rss feed

Pages

Showing posts with label The Missing Methods. Show all posts
Showing posts with label The Missing Methods. Show all posts

28.5.20

Synchronous and Asychronous ThreadSafe Blitzkrieg Caching

Update! You can download BlitzCache from nuget now


Caching is necessary
Over the years I have used a lot of caching. In fact, I consider that some things like user permissions should normally have a cache of at least one minute.

Blitzkrieg Caching
Even when a method is cached there are cases when it is called again before it has finished the first time and this results in a new request to the database, and this time much slower. This is what I call The Blitzkrieg Scenario.

The slowest the query the more probabilities you have for this to happen and the worse the impact. I have seen too many times SQL Server freeze in the struggle of replying to the exact same query while the query is already being executed...

Ideally at least in my mind a cached method should only calculate its value once per cache period. To achieve this we could use a lock... But if I am caching different calls I want more than one call to be executed at the same time, exactly one time per cache key in parallel. This is why I created the LockDictionary class.

The LockDictionary
Instead of having a lock in my cache service that will lock all the parallel calls indiscriminately I have a dictionary of locks to lock by cache key.

public static class LockDictionary
{
    private static readonly object dictionaryLock = new object();
    private static readonly Dictionary<string, object> locks = new Dictionary<string, object>();

    public static object Get(string key)
    {
        if (!locks.ContainsKey(key))
        {
            lock (dictionaryLock)
            {
                if (!locks.ContainsKey(key)) locks.Add(key, new object());
            }
        }

        return locks[key];
    }
}
With this I can very easily select what I want to lock

GetBlitzkriegLocking
Now I can check if something is cached and return it or lock that call in particular and calculate the value of the function passed as a parameter.

public class CacheService : ICacheService
{
  private readonly IMemoryCache memoryCache;

  public CacheService(IMemoryCache memoryCache)
  {
      this.memoryCache = memoryCache;
  }

  public T GetBlitzkriegLocking<T>(string cacheKey, Func<T> function, double milliseconds)
  {
      if (memoryCache.TryGetValue(cacheKey, out T result)) return result;
      lock (LockDictionary.Get(cacheKey))
      {
          if (memoryCache.TryGetValue(cacheKey, out result)) return result;

          result = function.Invoke();
          memoryCache.Set(cacheKey, result, DateTime.Now.AddMilliseconds(milliseconds));
      }

      return result;
  }
}
And how do I use it?
var completionInfo = cacheService.GetBlitzkriegLocking($"CompletionInfo-{legalEntityDto.Id}", () => GetCompletionInfoDictionary(legalEntityDto), 500));
//Look ma, I am caching this for just 500 milliseconds and it really makes a difference
I find this method extremely useful but sometimes the function I am calling needs to be awaited... And you Cannot await in the body of a lock statement. What do I do?

The SemaphoreDictionary
Semaphores do allow you to await whatever you need, in fact they themselves are awaitable. If we translate the LockDictionary class to use semaphores it looks like this:

public static class SemaphoreDictionary
{
    private static readonly object dictionaryLock = new object();
    private static Dictionary<string, SemaphoreSlim> locks = new Dictionary<string, SemaphoreSlim>();

    public static SemaphoreSlim Get(string key)
    {
        if (!locks.ContainsKey(key))
        {
            lock (dictionaryLock)
            {
                if (!locks.ContainsKey(key)) locks.Add(key, new SemaphoreSlim(1,1));
            }
        }

        return locks[key];
    }
}
And using this I can await calls while I am locking stuff.


The Awaitable GetBlitzkriegLocking

The main rule about semaphores is that you must make sure you release them or they will be locked forever. Catching the error is optional though.
public async Task<T> GetBlitzkriegLocking<T>(string cacheKey, Func<Task<T>> function, double milliseconds)
{
    if (memoryCache.TryGetValue(cacheKey, out T result)) return result;

    var semaphore = SemaphoreDictionary.Get(cacheKey);

    try
    {
        await semaphore.WaitAsync();
        if (!memoryCache.TryGetValue(cacheKey, out result))
        {
            result = await function.Invoke();
            memoryCache.Set(cacheKey, result, DateTime.Now.AddMilliseconds(milliseconds));
        }
    }
    finally
    {
        semaphore.Release();
    }

    return result;
}
And how do I use it?
await cache.GetBlitzkriegLocking($"RequestPermissions-{userSid}-{workplace}", () => RequestPermissionsAsync(userSid, workplace), 60 * 1000);
Please give these methods a try and let me know how you would improve them. I am using them every now and then and I really enjoy how they simplify the code. I hope you like them too.

No comments:

Post a Comment

22.4.19

Concurrency Errors and Value Objects in Entity Framework 2.x

We have been using value objects in our databases for a couple of months and after we passed the first hurdles with entity framework we have noticed improvements in speed and a significant decrease in the Includes wich is all what we wanted to get... but suddenly...

In one of the microservices we noticed we were getting concurrency exceptions every time we updated a value object. And it wasn't a complicated nested value object, it was a value object made of two guids and two strings.

I spent a good afternoon trying to figure out why were we getting the error and could not see anything wrong in the code... and I thought... it's entity framework again!

I asked for help to the rest of the team and they found this thread Changes on Owned Entites Properties causes a concurrency conflict on same dbContext where they propose a workaround. We tried and it didn't work but we thought it was going in the right direction so we debug it and change it a bit and voilà the rowversion column was being updated again both in SQL and in my backend!

The modified code is this:
private void ConcurrencyFix()
{
    var changedEntriesWithVos = ChangeTracker.Entries().Where(e =>
        e.State == EntityState.Unchanged
        && e.References.Any(r =>
            r.TargetEntry != null
            && (r.TargetEntry.State == EntityState.Modified || r.TargetEntry.State == EntityState.Added)
            && r.TargetEntry.Metadata.IsOwned()
            && e.Metadata.Relational().TableName == r.TargetEntry.Metadata.Relational().TableName)).ToArray();

    foreach (var entry in changedEntriesWithVos)
        entry.State = EntityState.Modified;
}

And we have placed it in our SaveChanges methods in our base context, from which all of our contexts are inheriting so we are sure this code is always being executed when we save.
public override int SaveChanges(bool acceptAllChangesOnSuccess)
{
    ConcurrencyFix();
    return base.SaveChanges(acceptAllChangesOnSuccess);
}

public override Task SaveChangesAsync(bool acceptAllChangesOnSuccess, CancellationToken cancellationToken = default)
{
    ConcurrencyFix();
    return base.SaveChangesAsync(acceptAllChangesOnSuccess, cancellationToken);
}

Have fun everyone!

No comments:

Post a Comment

23.3.19

Configuring and Updating nested value objects in Entity Framework

Wow three years since the last post... not bad!

For those of you who are curious I have finally left SharePoint and now I am sure we both are happier. But with a new platform come new issues.

I am working now in a web application with .net core and entity framework and we have started using ValueObjects. All was fun and games until last wednesday when I decided to make one value object a child of another.

We have User:
public class User: ValueObject
{
    public string ActiveDirectorySID { get; private set; }
    public string Email { get; private set; }
    public string Name { get; private set; }

    (...)
}

We have Position:
public class Position : ValueObject
{
    public Guid PositionId { get; private set; }
    public string JobTitle { get; private set; }
    public User Employee { get; private set; }
    (...)
}

And Finally we have CompanyPolicy:
public class CompanyPolicy : Entity
{
    public string Title { get; private set; }
    (...)
    public Position Owner { get; private set; }
    public Position Reviewer { get; private set; }
    public Position Validator { get; private set; }
    (...)
}

As you can see CompanyPolicy owns three Positions and each position owns an User. The first issue we need to deal with is the mappings of the CompanyPolicy object. After reading half of the internet the solution ended up being easy, the configuration code for the Positions inside a CompanyPolicy is:
public override void Configure(EntityTypeBuilder<CompanyPolicy> builder)
{
  base.Configure(builder);

  builder.Property(x => x.Title);
  (...)
  builder.OwnsOne(p => p.Owner, cb => { cb.OwnsOne(e => e.Employee); });
  builder.OwnsOne(p => p.Reviewer, cb => { cb.OwnsOne(e => e.Employee); });
  builder.OwnsOne(p => p.Validator, cb => { cb.OwnsOne(e => e.Employee); });
  (...)
}

So far so good, we add the migration an it does not fail, we update the databases and it works... we add a value and all the fields get populated... We did it?! NOPE. The values never update. No matter what you do.

We read the other half of the internet and found this thread where they show a workaround and say this is already solved in the 3.0 preview, and as we don't want to wait and we don't want to install VS2019 we decided to go for the workaround.

Based on it and after a couple of iterations we came up with this method:
public static void UpdateChildValueObjects<TEntity, TParent>(
    this BaseAllensContext context,
    TEntity rootEntity,
    Expression<Func<TParent, ValueObject>> getChildValueObject,
    params Expression<Func<TEntity,TParent>>[] getParentValueObjectArray)
        where TEntity : class where TParent:class
        {
    var values = new List<ValueObject>();

    //In the DetectChanges and when setting the values the rootEntity gets
    //the values from the database. That's why we need to save them before!
    for (int i = 0; i < getParentValueObjectArray.Length; i++)
    {
        values.Add(getChildValueObject.Compile()
          .Invoke(getParentValueObjectArray[i].Compile().Invoke(rootEntity)));
    }

    context.ChangeTracker.DetectChanges();

    for (var i = 0; i < getParentValueObjectArray.Length; i++)
    {
        context.Entry(rootEntity).Reference(getParentValueObjectArray[i])
          .TargetEntry.Reference(getChildValueObject)
          .CurrentValue = values[i];
    }

    context.Entry(rootEntity).State = EntityState.Modified;
}

Ugly anyone? Hard to read? It is not ugly, I like it. And it sits in your repository beautifully.

In the repository we have added this method:

public void Update(CompanyPolicy policy)
{
    Context.UpdateChildValueObjects(policy, x => x.Employee,
        x => x.Owner, x => x.Validator, x => x.Reviewer);
}

This is what I wanted to show :)
So with this method in the repository you are telling that you want to update the Employee ValueObject in the Owner, Validator and Reviewer ValueObjects of the Entity policy. And you might be thinking... That looks nice but, how do you use it?
Well this is the ugly part...

First after we update the CompanyPolicy entity as we would normally do we need to make sure these value objects are updated so... I call the update first on the entity and then on the repository, like this:
var owner= companyPolicyDTO.Owner != null
    ? Mapper.Map<Position>(companyPolicyDTO.Owner)
    : Position.Empty;
var reviewer = companyPolicyDTO.Reviewer != null
    ? Mapper.Map<Position>(companyPolicyDTO.Reviewer)
    : Position.Empty;
var validator = companyPolicyDTO.Validator != null
    ? Mapper.Map<Position>(companyPolicyDTO.Validator)
    : Position.Empty;

companyPolicy.Update(companyPolicyDTO.Title, owner, reviewer, validator);
companyPolicyRepository.Update(companyPolicy);

And this is it. First the standard update then our workaround.

Known Issues:
- The method UpdateChildValueObjects sets the values of the non updated fields of the entity to what they where in the database. This means you can't call this method twice in the same transaction because the second time it will not update the values!

public void Update(CompanyPolicy policy)
{
    Context.UpdateChildValueObjects(policy, x => x.Employee,
        x => x.Owner, x => x.Validator, x => x.Reviewer);
}
OK

public void Update(CompanyPolicy policy)
{
    Context.UpdateChildValueObjects(policy, x => x.Employee,
        x => x.Owner);
    Context.UpdateChildValueObjects(policy, x => x.Employee,
        x => x.Reviewer);
    Context.UpdateChildValueObjects(policy, x => x.Employee,
        x => x.Validator);
}
Only the owner will be updated!

I hope this helps someone!

1 comment:

Post a Comment

11.6.15

Migrating SharePoint Users to a New Domain

I have been dreading this type of migration for years... so many years that I already had planned a couple of ways of solving the issue. It finally happened.

Scenario:

Someone decides we need to change the farm from one environment to a new one completely different with a new AD and in a new city.

Well, let's get to it. We have created a new SharePoint farm in the new environment and we have backed up and restored the content databases. We have manually changed the admin of the site collection to the new admin in the new AD in the new farm and we can access the site and see the data. Fantastic.

Fantastic?

The users in the list items are the users from the old farm. And we have several lists with a lot of user fields. And some of our lists have tens or hundreds of thousands of rows. Changing them manually is not an option.

First idea: Go refined and try stsadm -o migrateuser:

Ohh so easy... we change the login name of the user to something else and we are good because the user IDs are still the same... NO.

This is a new domain and we don't have access to the old domain users so the migrateuser parameter throws a nice User not found error.

Second idea: Go berserk and change the strings in the list items

And that worked. Oh the beauty of a simple idea. The process is pure brute force... beautiful in its barbarity... If you have read up to here you are probably desperate for a solution.

Step One:
Get all of the users from the old farm in an XML file or something really high tech (a csv could work too).

static void Main(string[] args)
{
    using (SPSite site = new SPSite(args[0]))
    {
        using (SPWeb web = site.OpenWeb())
        {
            XElement users = new XElement("Users");

            foreach (SPUser user in web.SiteUsers)
            {
                XElement xmlUser = new XElement("User");
                xmlUser.Add(new XAttribute("Name", user.Name));
                xmlUser.Add(new XAttribute("LoginName", user.LoginName));
                xmlUser.Add(new XAttribute("ID", user.ID));

                users.Add(xmlUser);
            }

            users.Save("SiteUsers.xml");
        }
   
    }

    Console.WriteLine("\nProcess finished...");
    Console.ReadLine();
}


Step Two:
Make sure all the users you need are in the new AD. As you have a list in XML you can pass it to someone with privileges in the AD.

Step Three:
Ensure the users in SharePoint, add them to a group with reading permissions and then iterate through all the items in the list changing the users from the old domain to the users in the new one.

static void Main(string[] args)
{
    XElement users = XElement.Load("SiteUsers.xml");
    string newDomain = "XXXXXXXX";

    string ListName = string.Empty;
    if (args.Length == 2) ListName = "Stratex Framework";
    else ListName = args[2];

    //Args are SiteUrl VisitorsGroup ListName
    using (SPSite site = new SPSite(args[0]))
    {
        using (SPWeb web = site.OpenWeb())
        {
            SPList listToUpdate = web.Lists[ListName];

            Dictionary<string, SPUser> NewUsers = new Dictionary<string, SPUser>();

            foreach (XElement user in users.Descendants("User"))
            {
                string LoginName = FixDomain(user.Attribute("LoginName").Value, newDomain);
                SPUser spUser = null;
                try
                {
                    //We try to ensure all the users from the XML file. We'll probably need them
                    spUser = web.EnsureUser(LoginName);
                }
                catch
                { Logger.WriteLine("The user {0} could not be found.", LoginName); }

                if (spUser != null)
                {
                    SPGroup viewers = web.Groups[args[1]];

                    viewers.AddUser(spUser);
                    //Finally we add them to a group with read permissions
                    //We can worry about restricting this further after the migration

                    if (!NewUsers.ContainsKey(LoginName)) NewUsers.Add(user.Attribute("ID").Value, spUser);
                }
            }

            web.Update();


            UpdateUsersInList(listToUpdate, NewUsers);
        }

    }

    Logger.WriteLine("\nProcess finished...");
    Console.ReadLine();
}

private static void UpdateUsersInList(SPList list, Dictionary<string, SPUser> NewUsers)
{
    int itemsInList = list.ItemCount;
    Logger.WriteLine("Updating users at {0}. {1} items.", list.Title, itemsInList.ToString());

    SPQuery qry = new SPQuery();
    qry.ViewAttributes = "Scope=\"RecursiveAll\"";
    SPListItemCollection allItems = list.GetItems(qry);
    int count = 0;

    UpdateCount(count++, itemsInList);

    foreach (SPListItem item in allItems)
    {
        try
        {
            bool changed = false;
            SPFieldCollection allFields;
            //If the item has content type it has usualy less fields
            if (item.ContentType == null)
                allFields = item.Fields;
            else
                allFields = item.ContentType.Fields;

            foreach (SPField field in allFields)
            {
                if (field is SPFieldUser)
                    changed = ChangeUserToNewDomain(item, field, NewUsers) || changed;
            }

            changed = ChangeUserToNewDomain(item, item.Fields.GetFieldByInternalName("Author"), NewUsers) || changed;
            changed = ChangeUserToNewDomain(item, item.Fields.GetFieldByInternalName("Editor"), NewUsers) || changed;

            if (changed) item.SystemUpdate(false); //if the item has not been changed we won't update it to save time
        }
        catch (Exception ex) { Logger.WriteLine("Failed to update item {0}. Exception {1}", item.Title, ex.Message); }

        UpdateCount(count++, itemsInList);
    }

    UpdateCount(count++, 0);
}

private static bool ChangeUserToNewDomain(SPListItem item, SPField field, Dictionary<string, SPUser> NewUsers)
{
    bool changed = false;
    string fieldContent = item[field.InternalName] == null ? null : item[field.InternalName].ToString();

    if (string.IsNullOrEmpty(fieldContent)) return false;

    List<string> oldUserIds = GetUserIDs(fieldContent.Split(new string[] { ";#" }, StringSplitOptions.RemoveEmptyEntries));

    if (oldUserIds.Count == 1)
    {   //The field has only one user in it
        SPFieldUserValue foundUser = FindUser(NewUsers, oldUserIds[0]);

        if (foundUser != null)
        {
            item[field.InternalName] = foundUser;
            changed = true;
        }
    }
    else if (oldUserIds.Count > 1)
    {   //The field has several users in it
        SPFieldUserValueCollection usersInField = new SPFieldUserValueCollection();
        foreach (string oldUser in oldUserIds)
        {
            SPFieldUserValue foundUser = FindUser(NewUsers, oldUser);

            if (foundUser != null)
                usersInField.Add(foundUser);
        }

        if (usersInField.Count > 0)
        {
            item[field.InternalName] = usersInField;
            changed = true;
        }
    }
            
            
    return changed;
}

private static List<string> GetUserIDs(string[] UserTokens)
{   //We do not care about the login name. The ID is gold
    List<string> result = new List<string>();

    if (UserTokens.Length > 0)
    {
        for (int i = 0; i < UserTokens.Length; i++)
        {
            int id;

            if (i % 2 == 0 && int.TryParse(UserTokens[i], out id))
                result.Add(id.ToString());
        }
    }

    return result;
}

private static SPFieldUserValue FindUser(Dictionary<string, SPUser> NewUsers, string oldUser)
{
    SPUser foundUser = null;

    if (NewUsers.ContainsKey(oldUser)) foundUser = NewUsers[oldUser];
    else
    {
        //If we can't find the ID of the user we will still try with the login or even with the Display Name
        foreach (SPUser newUser in NewUsers.Values)
        {
            if (newUser.Name == oldUser || newUser.LoginName == oldUser) { foundUser = newUser; break; }
        }
    }

    if (foundUser != null)
        return new SPFieldUserValue(foundUser.ParentWeb, foundUser.ID, foundUser.Name);
    else
        return null;
}

private static string FixDomain(string loginName, string newDomain)
{
    //Here we change the users from XXXXX\\User to YYYYY\\User
    //The source domain was claim based
    if (loginName.Contains("|")) loginName = loginName.Split('|')[1];

    string[] tokens = loginName.Split('\\');

    tokens[0] = newDomain;

    return string.Join("\\", tokens);
}

private static void UpdateCount(int currentItem, int itemsInList)
{
    int percentage;

    if (currentItem == 0) percentage = 0;
    else if (itemsInList == 0) percentage = 100;
    else
    {
        //We will only change the value every 10 times to make the process faster.
        if (currentItem % 10 != 0) return;
        percentage = currentItem * 100 / itemsInList;
    }
    Console.Write("\r");
    if (percentage >= 0 && percentage < 10)
        Console.Write("  ");
    else if (percentage >= 10 && percentage < 100)
        Console.Write(" ");

    Console.Write("{0}%", percentage);
}

This is a first prototype that has worked as expected but it's not fully tested (by far) if you need it you can use it as a base to develop your own tool.

The one who possesses the strings has the power.

No comments:

Post a Comment

19.9.13

Get the fields available in an SPListItem

When you retrieve the items from a CAML query with the ViewFields parameters set there’s no way (or at least i don’t know it) to find out which fields you have available and populated with data.

The usual way of getting the list of fields from the content type doesn’t work because the content type is null in that kind of items…

The answer, my friend, is sitting in the Xml.

static List<string> ExcludedFields = new List<string> { "z", "ows_ServerRedirected", "ows_FileRef", "ows_PermMask", "ows_FSObjType", "ows__Level", "ows__ModerationStatus" };
/// <summary>
/// Gets a dictionary with the available fields and its values.
/// </summary>
public static Dictionary<string, string> getAvailableFields(this SPListItem item)
{
    XElement row = XElement.Parse(item.Xml);
    Dictionary<string, string> Fields = new Dictionary<string, string>();

    foreach (XAttribute field in row.Attributes())
    {
        if (!ExcludedFields.Contains(field.Name.LocalName))
            Fields.Add(field.Name.LocalName.Substring(4), field.Value);
    }

    return Fields;
}

Using this I have been able to reduce the traffic on a web service method in a 65%.

No comments:

Post a Comment

19.8.13

SharePoint Client Object Model Is Great

This is one of those things that you know they are there but never use because you already know a different way.

Just four years after I started working with SharePoint 2010 I thought “Why not giving the Client Object Model a go?” actually I had a requirement from a client. and I must say I am VERY impressed. The simplicity, the speed and the predictability is pretty good for the standards we are used to.

Even though the COM is very good I have created a series of methods, wrappers and extensions to help me deal with my most common functions. Some of them translated from the methods I use in the SharePoint Object Model and some of them new. Let’s begin.

It is really easy to connect using Windows authentication or Forms Based Authentication. Easier than anything else I have seen to date.

public static ClientContext GenerateClientContextWinAuth(string URL)
{
    return new ClientContext(URL);
}

public static ClientContext GenerateClientContextFBAAuth(string URL, string userName, string password)
{
    ClientContext ctx = new ClientContext(URL);
    ctx.AuthenticationMode = ClientAuthenticationMode.FormsAuthentication;
    ctx.FormsAuthenticationLoginInfo = new FormsAuthenticationLoginInfo(userName, password);

    return ctx;
}

Windows auth 1 line of code FBA 3. Nice.

Do you remember how it was reading and writing files to SharePoint? Look how easy it is using the COM.

public static string ReadFile(ClientContext Context, string ListName, string FileName)
{
    List StorageList = Context.Web.Lists.GetByTitle(ListName);
    string SharePointFilePath = GetFilePathInSharePoint(Context, StorageList, FileName);

    FileInformation fileInfo = Microsoft.SharePoint.Client.File.OpenBinaryDirect(Context, SharePointFilePath);

    using (fileInfo.Stream)
    {
        using (StreamReader sr = new StreamReader(fileInfo.Stream))
        {
            return sr.ReadToEnd();
        }
    }
}

public static void WriteFile(ClientContext Context, string LocalFilePath, string ListName, string SPFileName)
{
    List StorageList = Context.Web.Lists.GetByTitle(ListName);

    string SharePointFilePath = GetFilePathInSharePoint(Context, StorageList, SPFileName);

    using (FileStream fs = new FileStream(LocalFilePath, FileMode.Open))
        Microsoft.SharePoint.Client.File.SaveBinaryDirect(Context, SharePointFilePath, fs, true);
}

private static string GetFilePathInSharePoint(ClientContext ctx, List StorageList, string FileName)
{
    if (StorageList.RootFolder.ServerObjectIsNull != false)
    {
        ctx.Load(StorageList.RootFolder);
        ctx.ExecuteQuery();
    }

    string ListRootFolderURLDocuments = StorageList.RootFolder.ServerRelativeUrl;
    return Path.Combine(ListRootFolderURLDocuments, FileName);
}

Other than the small function to find the url of the item in the document list using folders it's as simple as it can be. I have also created a couple of methods for making easier to work with the StratexFramework.

If you are creating some program and want to use some of the code feel free. If you are not working with StratexPoint feel free to modify it to suit your environment.

Here's a method to bring back the root entity of the framework:
public static ListItem GetRootEntity(this List ComList)
{
    CamlQuery camlQuery = new CamlQuery();
    camlQuery.ViewXml = string.Format(@"<View>
                                            <Query>
                                                <Where>
                                                    <Eq>
                                                        <FieldRef Name='ContentType'/>
                                                        <Value Type='Choice'>Entity</Value>
                                                    </Eq>
                                                </Where>
                                                <RowLimit>1</RowLimit>
                                            </Query>
                                        </View>");

    ListItemCollection items = ComList.GetItemsExecuted(camlQuery);

    if (items.Count == 1)
        return items[0];
    else
        return null;
}

Nice and easy.

The query is a bit different to the usual we do in the SPQuery, but still very similar.

Then I have created a method that will help when you want to run a query under a folder regardless of the rest of the framework. First I will paste the helper query and then I will paste the samples on how to use it:

public static CamlQuery CreatePositionedQuery(this ListItem StartEntity)
{
    CamlQuery camlQuery = new CamlQuery();
    camlQuery.ViewXml = string.Format(@"<View Scope='RecursiveAll' >
                                            <Query>
                                                <Where>
                                                    <And>
                                                        <Eq><FieldRef Name='FileDirRef' /><Value Type='Text'>{0}</Value></Eq>
                                                        {1}
                                                    </And>
                                                </Where>
                                            </Query>
                                        </View>", GetChildrenFolder(StartEntity), "{0}"); //This is the folder url and the placeholder for the real query.


    return camlQuery;

}

private static string GetChildrenFolder(ListItem startItem)
{
    startItem.InitializeIfNeeded("FileDirRef");
    startItem.InitializeIfNeeded("FileLeafRef");

    return startItem["FileDirRef"] + "/" + startItem["FileLeafRef"];
}

With this we basically are saying SharePoint to execute the query under the folder of the item we are passing as a parameter. And we can use this code easily to position our queries in the folder tree.

First one function to get all the child entities under a given entity, and then another function to bring an item with a given title inside a given folder:

public static ListItemCollection GetChildEntities(ListItem StartEntity)
{
    List ComList = StartEntity.ParentList;

    CamlQuery camlQuery = CreatePositionedQuery(StartEntity);
    camlQuery.ViewXml = string.Format(camlQuery.ViewXml,
                                    @"<And>
                                        <Eq><FieldRef Name='State' /><Value Type='Choice'>Live</Value></Eq>
                                        <Eq><FieldRef Name='ContentType' /><Value Type='Choice'>Entity</Value></Eq>
                                        </And>");

    return ComList.GetItemsExecuted(camlQuery);
}

public static ListItem GetChildItem(this ListItem StartEntity, string Title)
{
    List ComList = StartEntity.ParentList;

    CamlQuery camlQuery = CreatePositionedQuery(StartEntity);
    camlQuery.ViewXml = string.Format(camlQuery.ViewXml,
                                    string.Format("<Eq><FieldRef Name='Title' /><Value Type='Text'>{0}</Value></Eq>", Title));

    ListItemCollection result = ComList.GetItemsExecuted(camlQuery);

    if (result.Count > 0)
        return result[0];
    else
        return null;
}
Creating a new item is also easy. The hardest thing to do is deciding if it's a folder or a leaf:
public static ListItem CreateItemUnder(this ListItem ParentItem, string Title, string ContentType, bool isLeaf)
{
    ListItemCreationInformation itemCreateInfo = new ListItemCreationInformation();
    itemCreateInfo.FolderUrl = GetChildrenFolder(ParentItem);
    itemCreateInfo.LeafName = Title;

    if (isLeaf)
        itemCreateInfo.UnderlyingObjectType = FileSystemObjectType.File;
    else
        itemCreateInfo.UnderlyingObjectType = FileSystemObjectType.Folder;

    ListItem NewItem = ParentItem.ParentList.AddItem(itemCreateInfo);

    if (ContentType != null)
        NewItem["ContentTypeId"] = GetContentType(ParentItem.ParentList, ContentType).Id;

    return NewItem;
}

A small shortcut to execute queries:


public static ListItemCollection GetItemsExecuted(this List listToQuery, CamlQuery query)
{
    ListItemCollection childItems = listToQuery.GetItems(query);

    listToQuery.Context.Load(listToQuery);
    listToQuery.Context.Load(childItems);
    listToQuery.Context.ExecuteQuery();

    return childItems;
}

In some cases the field you want to read didn't come in the execution maybe because you were a bit too restrictive with the viewfields clause... You can try with this trick:


public static void InitializeIfNeeded(this ListItem item, string InternalName)
{
    if (!item.FieldValues.ContainsKey(InternalName))
    {
        item.Context.Load(item);
        item.Context.ExecuteQuery();
    }
}

Are you trying to get a content type from a List? Easy.


public static ContentType GetContentType(List list, string cTypeName)
{
    list.Context.Load(list.ContentTypes);
    list.Context.ExecuteQuery();

    foreach (ContentType ctype in list.ContentTypes)
    {
        if (ctype.Name == cTypeName) return ctype;
    }

    return null;
}

Are you trying to save a SPFieldUserValue in a SPFieldUser using the client object model or read it back? Complex, I would say unnecessarily complex, but I have one last trick:

public static string UserToString(object fieldUserValue)
{
    if (fieldUserValue == null) return string.Empty;

    FieldUserValue user = fieldUserValue as FieldUserValue;

    return string.Format("{0};{1}", user.LookupId, user.LookupValue);
}

public static FieldUserValue StringToUser(object fieldUserValueString)
{
    FieldUserValue user = new FieldUserValue();

    if (string.IsNullOrEmpty(fieldUserValueString.ToStringSafe())) return user;

    string[] tokens = fieldUserValueString.ToStringSafe().Split(';');

    user.LookupId = tokens[0].ToNullableInt() ?? 0;

    return user;
}

The client object model has been really powerful and really easy to work with for me so far. I really encourage you to give it a try if you haven't already.

No comments:

Post a Comment

22.4.13

Editing SPListItems from SPWeb.GetSiteData

In my solution I have an unknown number of subsites that contain an unknown number of SPListItems that need to be updated. Looks like the perfect way of testing GetSiteData.

The documentation is useful and you'll be able to test the queries without problems really? yes, the documentation is ok but this method brings back a DataTable and what I needed was to update the SPListItems.

Well if you look at the DataTable you'll see that you have all the fields you need there to retrieve the SPListItem easily.

I have created an extension method, well two really... I love extension methods.
public static SPListItem GetListItemFromSiteData(this DataRow ItemRow, SPSite ParentSite)
{
    using (SPWeb Web = ItemRow.GetWebSiteData(ParentSite))
    {
        return Web.Lists[new Guid(ItemRow["ListId"].ToString())].GetItemById(Convert.ToInt32(ItemRow["ID"]));
    }
}

public static SPWeb GetWebSiteData(this DataRow ItemRow, SPSite ParentSite)
{
    return ParentSite.OpenWeb(new Guid(ItemRow["WebId"].ToString()));
}

Using these two you will be able to iterate through the collection of rows of the DataTable, select which elements need to be updated and update them.

I haven't tested how fast is this compared with bringing the items with a CAML query. Not having to create the SPListItemCollection could make this method faster and more convenient for some things... I'll give it a go.

No comments:

Post a Comment

8.4.13

SharePoint List Names Localization

You can’t call a list by its internal name by default in SharePoint or I don’t know how, you have to use the display name or the GUID and that’s an issue when it comes to localization.

In order to solve this issue I have created a couple of wrappers based on the URL. The idea is simple, I use a set of constants to store the “internal names” of the lists and always retrieve them using this constants. So far it has worked fine.
/// <summary>
/// Returns null if the list is not found
/// </summary>
public static SPList GetListByInternalName(this SPListCollection Lists, string InternalName)
{
    if (string.IsNullOrEmpty(InternalName)) return null;

    Guid ListGuid = Lists.GetListIdByInternalName(InternalName);

    if (ListGuid != Guid.Empty)
        return Lists[ListGuid];

    return null;
}

/// <summary>
/// Returns the UIDC of the list
/// </summary>
static Guid GetListIdByInternalName(this SPListCollection Lists, string InternalName)
{
    if (string.IsNullOrEmpty(InternalName)) return Guid.Empty;

    foreach (SPList list in Lists)
        if (list.GetInternalName().ToLower() == InternalName.ToLower())
            return list.ID;

    return Guid.Empty;
}

/// <summary>
/// Gets the Url of the list. That's what we consider its internal name
/// </summary>
public static string GetInternalName(this SPList list)
{
    string Url = list.RootFolder.ServerRelativeUrl;

    string[] split = Url.Split('/');

    return split[split.Length - 1];
}

After this the display name of the list is not relevant any more and localization is simple or, at least, a bit simpler.

No comments:

Post a Comment