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.