Sunday, April 27, 2014

Create custom search result source programmatically in Sharepoint 2013

If you need to customize search results on your Sharepoint 2013 site, e.g. add additional search rules which will be used with the keywords entered by the user in the search box, then you will need to create custom search result source (there are other ways to do it also, but custom result source is the most straightforward and designed exactly for such requirements). Result source may be created on site collection level and marked as default. In this case it will be used on default search results page or in content by search web parts (if another result source is not specified in web parts properties explicitly). In this post I will show how to create custom search result source programmatically during provisioning. It will help to automate installation process.

Let’s create result source which will have additional filter for the News by managed metadata property Country. I.e. we want to show only those news which are tagged for appropriate country. Here is how it can be done:

   1: string query = "{{?{{searchTerms}} -ContentClass=urn:content-class:SPSPeople " +
   2:     "(ContentType<>News OR (ContentType=News AND Countries:\"Russia\"}}";
   3:  
   4: SPSite site = ...;
   5: var searchAppProxy = getSearchServiceApplicationProxy(site);
   6: var federationManager = new FederationManager(searchAppProxy);
   7: var searchOwner = new SearchObjectOwner(SearchObjectLevel.SPSite, site.RootWeb);
   8: var resultSource = federationManager.CreateSource(searchOwner);
   9: resultSource.Name = "Localized Search Results";
  10: resultSource.ProviderId =
  11:     federationManager.ListProviders()["Local SharePoint Provider"].Id;
  12: resultSource.CreateQueryTransform(new QueryTransformProperties(), query);
  13: resultSource.Commit();
  14: federationManager.UpdateDefaultSource(resultSource.Id, searchOwner);

Here is the parts included to our query:

?{searchTerms} – placeholder for the keywords entered by the user in search box;
-ContentClass=urn:content-class:SPSPeople – exclude people from search result;
(ContentType<>News OR (ContentType=News AND Countries:"Russia"} – show news which are tagged for Russia. Other content is shown regardless of the country.

These rules are combined by space which means AND operator in KQL. As result only those content will be shown in the search results which matches to all rules.

Note that on line 14 we also set created result source as default (federationManager.UpdateDefaultSource(…)). Note that after that catalog connections will disappear in Site settings > Manage catalog connections > Connect to a catalog and you won’t be able to connect to publishing catalog. I wrote about this problem in Problem with missing catalog connections when use customized search result source in Sharepoint 2013 article. Workaround is to set OTB result source “Local SharePoint Results” as default temporary, connect to catalog and set custom result source back to default.

Sunday, April 20, 2014

Problem with using resx files in App_GlobalResources and App_LocalResources in ASP.Net MVC web applications

In ASP.Net MVC applications it is technically possible to use old ASP.Net approach for storing resources when resx files are located in special folders App_GlobalResources for application-wide resources and App_LocalResources  for specific views. By using here I mean that only localizable strings will be stored in resx files, but in views they will be referenced by using strongly typed properties @MyResource.Foo, not via ASP.Net resource expressions <%$ Resources:Foo %> of course (the last one is against ASP.Net MVC spirit). In order to do this all resources should be embedded.

This approach will work with single language default resources. But when you will add culture-specific translation (e.g. en-us) then you may encounter with the following exception when will try to open your web application in browser:

Event code: 3006
Event message: A parser error has occurred.
Exception information:
Exception type: HttpParseException
Exception message: Access to the path 'foo.en-US.resx' is denied.
at System.Web.Compilation.AssemblyBuilder.AddBuildProvider(BuildProvider buildProvider)
at System.Web.Compilation.BuildProvidersCompiler.ProcessBuildProviders()
at System.Web.Compilation.BuildProvidersCompiler.PerformBuild()

at System.Web.HttpApplication.ExecuteStep(IExecutionStep step, Boolean& completedSynchronously)

Access to the path 'foo.en-US.resx' is denied.
at System.IO.__Error.WinIOError(Int32 errorCode, String maybeFullPath)
at System.IO.FileStream.Init(String path, FileMode mode, FileAccess access, Int32 rights, Boolean useRights, FileShare share, Int32 bufferSize, FileOptions options, SECURITY_ATTRIBUTES secAttrs, String msgPath, Boolean bFromProxy, Boolean useLongPath, Boolean checkHost)
at System.IO.FileStream..ctor(String path, FileMode mode, FileAccess access, FileShare share, Int32 bufferSize, FileOptions options, String msgPath, Boolean bFromProxy)
at System.IO.FileStream..ctor(String path, FileMode mode, FileAccess access, FileShare share)
at System.Web.Hosting.MapPathBasedVirtualFile.Open()
at System.Web.Compilation.BaseResourcesBuildProvider.GenerateCode(AssemblyBuilder assemblyBuilder)
at System.Web.Compilation.AssemblyBuilder.AddBuildProvider(BuildProvider buildProvider)

You may of course try to fix this problem by adding permissions to the appropriate user account (e.g. to IIS_IUSRS if your web app works in anonymous mode), but as error above shows, it will still try to compile generated .cs files for your resources in runtime, which is not what we expect, because in our case resources are embedded and we want to have all code precompiled.

In order to avoid runtime compilation of resources in ASP.Net MVC application instead of App_GlobalResources and App_LocalResources use other not-special folder names and still embed all resx files to your assemblies. In this case ASP.Net runtime won’t try to compile your resources and there won’t be problem shown above. The same conclusion was made in the following article Resx Files In App_GlobalResources, although it considered different issue.

Saturday, April 19, 2014

Connect to publishing catalog programmatically in Sharepoint 2013

In this post I will describe the process of connecting to publishing catalog programmatically. As you probably know in Sharepoint a lot of actions can be done manually in UI. However if we develop solution with repeatable deployments it is more efficient to automate as much actions as possible. Almost all actions which you may do via UI can be done programmatically or by script in Sharepoint (in some cases you will need to use internal classes via Reflection, but it is still possible). And connecting to the publishing catalog is not exception from this point of view.

First of all before to go forward I recommend to check my previous articles about cross-site publishing and catalogs in Sharepoint 2013, especially Provision Sharepoint 2013 site collection with cross-site publishing. It will help to understand on which step during provisioning methods shown below should be called and other important moments:

Problem with cross site publishing catalog connections when use SPExport/SPImport for copying sites
Restore Sharepoint site with configured cross-site publishing on different environment
Cross site publishing and anonymous access in Sharepoint 2013
Problem with connecting to catalog on site collections imported with SPExport/SPImport API in Sharepoint

First of all we created Site scope feature for configuring search-related functionalities. This feature was activated with assumption that it can be activated and deactivated several times on the site collection. So for each step we have both configuration in FeatureActivated() method of feature receiver and deleting of configuration in FeatureDeactivating() method:

   1: public class SiteSearchEventReceiver : SPFeatureReceiver
   2: {
   3:     public override void FeatureActivated(SPFeatureReceiverProperties properties)
   4:     {
   5:         SPSite site = properties.Feature.Parent as SPSite;
   6:         SearchConfigurationHelper.ConnectAndConfigureNewsPublishingCatalog(
   7:             site.RootWeb, "My Catalog");
   8:         ...
   9:     }
  10:  
  11:     public override void FeatureDeactivating(SPFeatureReceiverProperties properties)
  12:     {
  13:         SPSite site = properties.Feature.Parent as SPSite;
  14:         SearchConfigurationHelper.DeleteNewsPublishingCatalogConnection(site.RootWeb);
  15:         ...
  16:     }
  17: }

In SearchConfigurationHelper.ConnectAndConfigureNewsPublishingCatalog() we used slightly modified CatalogSubscriber from this MSDN example:

   1: public class CatalogSubscriber
   2: {
   3:     string portalSiteUrl;
   4:     string publishingCatalogUrl;
   5:     string subscribingWebServerRelativeUrl;
   6:     public CatalogSubscriber(string publishingCatalogUrl, string portalSiteUrl,
   7:         string subscribingWebServerRelativeUrl = "/")
   8:     {
   9:         if (string.IsNullOrWhiteSpace(portalSiteUrl) ||
  10:             string.IsNullOrWhiteSpace(publishingCatalogUrl))
  11:         {
  12:             throw new ArgumentNullException("A valid url must be provided for " +
  13:                 "publishingCatalogUrl and portalSiteUrl");
  14:         }
  15:         this.portalSiteUrl = portalSiteUrl;
  16:         this.publishingCatalogUrl = publishingCatalogUrl;
  17:         this.subscribingWebServerRelativeUrl = subscribingWebServerRelativeUrl;
  18:     }
  19:  
  20:     public void ConnectToCatalog()
  21:     {
  22:         using (SPSite publishingPortalSite = new SPSite(this.portalSiteUrl))
  23:         {
  24:             // Get the connection manager for the site where the catalog content
  25:             // will be rendered.
  26:             CatalogConnectionManager manager = new CatalogConnectionManager(
  27:                 publishingPortalSite, createResultSource: true);
  28:             // Use "Contains" to check whether a connection exists.
  29:             if (manager.Contains(this.publishingCatalogUrl))
  30:             {
  31:                 throw new ArgumentException(string.Format("A connection to {0} " +
  32:                     "already exists", this.publishingCatalogUrl));
  33:             }
  34:             // Get the catalog to connect to.
  35:             CatalogConnectionSettings settings =
  36:                 PublishingCatalogUtility.GetPublishingCatalog(publishingPortalSite,
  37:                     this.publishingCatalogUrl);
  38:             settings.ConnectedWebServerRelativeUrl =
  39:                 this.subscribingWebServerRelativeUrl;
  40:  
  41:             manager.AddCatalogConnection(settings);
  42:             // Perform any other adds/Updates
  43:             // ...
  44:             // Finally commit connection changes to store.
  45:             manager.Update();
  46:         }
  47:     }
  48:  
  49:     public void ConnectToCatalog(string[] orderedPropertiesForURLRewrite,
  50:         string taxonomyFieldManagedProperty)
  51:     {
  52:  
  53:         using (SPSite publishingPortalSite = new SPSite(this.portalSiteUrl))
  54:         {
  55:             // Get the connection manager for the site where the catalog
  56:             // content will be rendered.
  57:             CatalogConnectionManager manager =
  58:                 new CatalogConnectionManager(publishingPortalSite);
  59:             // Use "Contains" to check whether a connection exists.
  60:             if (manager.Contains(this.publishingCatalogUrl))
  61:             {
  62:                 throw new ArgumentException(string.Format("A connection to {0} " +
  63:                     "already exists", this.publishingCatalogUrl));
  64:             }
  65:             // Get the catalog to connect to.
  66:             CatalogConnectionSettings settings =
  67:                 PublishingCatalogUtility.GetPublishingCatalog(publishingPortalSite,
  68:                     this.publishingCatalogUrl);
  69:  
  70:             manager.AddCatalogConnection(settings,
  71:                 // If the items in the catalog should have rewritten urls, specify
  72:                 // what properties should be used in rewriting the url.
  73:                 orderedPropertiesForURLRewrite,
  74:                 this.subscribingWebServerRelativeUrl,
  75:                 // The managed property which contains the category for the catalog
  76:                 // item. Typically starts with owstaxid, when created by the system.
  77:                 taxonomyFieldManagedProperty);
  78:             // Perform any other adds/Updates
  79:             // ...
  80:             // Finally commit connection changes to store.
  81:             manager.Update();
  82:         }
  83:     }
  84:  
  85:     public void ConnectToCatalog(string newUrlTemplate,
  86:         string taxonomyFieldManagedProperty,
  87:         bool isCustomCatalogItemUrlRewriteTemplate,
  88:         string catalogName)
  89:     {
  90:         using (SPSite publishingPortalSite = new SPSite(this.portalSiteUrl))
  91:         {
  92:             // Get the connection manager for the site where the catalog
  93:             // content will be rendered.
  94:             CatalogConnectionManager manager =
  95:                 new CatalogConnectionManager(publishingPortalSite);
  96:             // Use "Contains" to check whether a connection exists.
  97:             if (manager.Contains(this.publishingCatalogUrl))
  98:             {
  99:                 throw new ArgumentException(string.Format("A connection to {0} " +
 100:                     "already exists", this.publishingCatalogUrl));
 101:             }
 102:             // Get the catalog to connect to.
 103:             //CatalogConnectionSettings settings =
 104:                 PublishingCatalogUtility.GetPublishingCatalog(publishingPortalSite,
 105:                     this.publishingCatalogUrl);
 106:             var settings = this.getCatalogByName(publishingPortalSite, catalogName);
 107:             if (settings == null)
 108:             {
 109:                 throw new Exception(string.Format("Can't connect to catalog '{0}': " +
 110:                     "it is not found", catalogName));
 111:             }
 112:  
 113:             manager.AddCatalogConnection(settings,
 114:                 // If the items in the catalog should have rewritten urls,
 115:                 // specify what properties should be used in rewriting the url.
 116:                 newUrlTemplate,
 117:                 this.subscribingWebServerRelativeUrl,
 118:                 // The managed property which contains the category for the catalog
 119:                 // item. Typically starts with owstaxid, when created by the system.
 120:                 taxonomyFieldManagedProperty,
 121:                 isCustomCatalogItemUrlRewriteTemplate);
 122:             // Perform any other adds/Updates
 123:             // ...
 124:             // Finally commit connection changes to store.
 125:             manager.Update();
 126:         }
 127:     }
 128:  
 129:     private CatalogConnectionSettings getCatalogByName(SPSite site,
 130:         string catalogName)
 131:     {
 132:         int num;
 133:         var catalogs = PublishingCatalogUtility.GetPublishingCatalogs(site, 0, 10,
 134:             string.Empty, out num);
 135:         if (catalogs == null)
 136:         {
 137:             return null;
 138:         }
 139:         foreach (var c in catalogs)
 140:         {
 141:             if (c.CatalogName == catalogName)
 142:             {
 143:                 return c;
 144:             }
 145:         }
 146:         return null;
 147:     }
 148:  
 149:     public void UpdateCatalogUrlTemplate(string newUrlTemplate)
 150:     {
 151:         using (SPSite publishingPortalSite = new SPSite(this.portalSiteUrl))
 152:         {
 153:             // Get the connection manager for the site where the catalog
 154:             // content will be rendered.
 155:             CatalogConnectionManager manager =
 156:                 new CatalogConnectionManager(publishingPortalSite);
 157:             // Get the current settings
 158:             CatalogConnectionSettings settings =
 159:                 manager.GetCatalogConnectionSettings(this.publishingCatalogUrl);
 160:             if (settings == null)
 161:             {
 162:                 throw new ArgumentException(
 163:                     string.Format("This site is not connected to catalog {0}",
 164:                         this.publishingCatalogUrl));
 165:             }
 166:             settings.CatalogItemUrlRewriteTemplate = newUrlTemplate;
 167:             // Update in memory
 168:             manager.UpdateCatalogConnection(settings);
 169:             // Perform any other adds/Updates
 170:  
 171:             // Finally Commit the update(s) to store.
 172:             manager.Update();
 173:         }
 174:     }
 175:  
 176:     public void DeleteCatalog()
 177:     {
 178:         using (SPSite publishingPortalSite = new SPSite(this.portalSiteUrl))
 179:         {
 180:             // Get the connection manager for the site where the catalog
 181:             // content will be rendered.
 182:             CatalogConnectionManager manager =
 183:                 new CatalogConnectionManager(publishingPortalSite);
 184:             // Use "Contains" to check whether a connection exists.
 185:             if (manager.Contains(this.publishingCatalogUrl))
 186:             {
 187:                 // Get the current settings
 188:                 CatalogConnectionSettings settings =
 189:                     manager.GetCatalogConnectionSettings(
 190:                         this.publishingCatalogUrl);
 191:  
 192:                 manager.DeleteCatalogConnection(this.publishingCatalogUrl);
 193:                 // Perform any other adds/Updates
 194:  
 195:                 // Finally Commit the update(s) to store.
 196:                 manager.Update();
 197:             }
 198:         }
 199:     }
 200: }

Here we used method getCatalogByName() (lines 126-147) which returns catalog from site collection by its name which work more stable than those which was used in MSDN example.

And in SearchConfigurationHelper.DeleteNewsPublishingCatalogConnection() method which is called in FeatureDeactivating() method we just called CatalogSubscriber.DeleteCatalog() method shown above (lines 176-197):

   1: public static void DeleteNewsPublishingCatalogConnection(SPWeb web)
   2: {
   3:     string newsCatalogUrl = ...;
   4:     var catalogSubscriber = new CatalogSubscriber(newsCatalogUrl, web.Url);
   5:     catalogSubscriber.DeleteCatalog();
   6: }

Hope that this information will help you if you will need to connect to publishing catalog programmatically.