.NET Core Api – Catch exceptions using a middleware

In .NET Core, we are starting to get used to the fact that nothing comes for free, and in the world of ultimate freedom of choice, every feature needs to be implemented by us.

This includes error handling. In a previous post, .NET Core Catch Model Binding Exceptions, I explained how you can use ApiBehaviorOptions to catch exceptions thrown when someone POST the wrong JSON format to your endpoint.

It should be noted, that you can still catch exceptions in your endpoint using try…catch:

namespace MyCode.Controllers
{
  [ApiController]
  [Route("/api")]
  [Produces("application/json")]
  public class MyController : ControllerBase
  {
    [HttpPost()]
    public IActionResult LogDoNavigate([FromBody] MyModel model)
    {
      try
      {
	    ...
            ...
      }
      catch (Exception ex)
      {
        return base.BadRequest(new ProblemDetails() { Title = ex.Message, Detail = ex.ToString() });
      }
    }
  }
}

But what if the exception is not in this part of the Controller? What if the exception is in a System.Text.Serializion.JsonConverter that runs before your endpoint is called? The code will still throw an exception of course, but there is no immediate default handler, and the exception will not end up in your log file, or your Application Insights instance.

To handle such a scenario, you need to implement a Middleware. Middlewares are a piece of code that handles request and responses. You can use middlewares for many situations, but the most obvious are as exception handlers.

STEP 1: CREATE A CUSTOM MIDDLEWARE

This middleware will catch errors and return the exception as a JSON result object. You need to add your own logging or Application Insights code to write to log.

using Microsoft.AspNetCore.Diagnostics;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using System;
using System.ComponentModel.DataAnnotations;
using System.Net;
using System.Security.Authentication;
using System.Threading.Tasks;
using Newtonsoft.Json;

namespace MyCode
{
  public class JsonExceptionMiddleware
  {
    public JsonExceptionMiddleware()
    {
    }

    public async Task Invoke(HttpContext context)
    {
      var contextFeature = context.Features.Get<IExceptionHandlerFeature>();
      if (contextFeature != null && contextFeature.Error != null)
      {
        // ...
        // Add lines to your log file, or your 
        // Application insights instance here
        // ...
        context.Response.StatusCode = (int)GetErrorCode(contextFeature.Error);
        context.Response.ContentType = "application/json";

        await context.Response.WriteAsync(JsonConvert.SerializeObject(new ProblemDetails()
        {
          Status = context.Response.StatusCode,
          Title = contextFeature.Error.Message
        }));
      }
    }

    private static HttpStatusCode GetErrorCode(Exception e)
    {
      switch (e)
      {
        case ValidationException _:
          return HttpStatusCode.BadRequest;
        case FormatException _:
          return HttpStatusCode.BadRequest;
        case AuthenticationException _:
          return HttpStatusCode.Forbidden;
        case NotImplementedException _:
          return HttpStatusCode.NotImplemented;
        default:
          return HttpStatusCode.InternalServerError;
      }
    }
  }
}

STEP 2: ADD THE MIDDLEWARE TO YOUR IApplicationBuilder

Add the middleware to the Configure method in the Startup.cs file. Call the UseExceptionHandler method to use the middleware:

public void Configure(IApplicationBuilder app)
{
  ...
  ...
  app.UseExceptionHandler(new ExceptionHandlerOptions { 
    ExceptionHandler = new JsonExceptionMiddleware().Invoke 
    }
  );
  ...
  ...
}

You would expect the middleware to catch any exception. It does catch exceptions in your Controller code, so don’t need a try-catch handler in every Controller. But it does not catch model binding exceptions, so you still need to catch those.

MORE TO READ:

Posted in .NET Core, c#, Microsoft Azure | Tagged , , , | Leave a comment

Sitecore and Application Insights – How to remove 90% of all log lines without sacrificing traceability

In this article I will explain how you can remove up to 90% of all log lines from the Application Insights log, but still keep every log line in the file log. All of this without loosing any important information.

Sitecore uses a Log4Net Appender to inject log lines into Application Insights. If you take a look at the App_Config/Sitecore/Azure/Sitecore.Cloud.ApplicationInsights.config file that is deployed to your CM/CD servers, you will find the appender at the top, and it could look like this:

<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/" xmlns:env="http://www.sitecore.net/xmlconfig/env/" xmlns:role="http://www.sitecore.net/xmlconfig/role/"> 
<sitecore> 
  <log4net> 
    <root> 
      <appender-ref ref="ApplicationInsightsAppender" patch:after="appender-ref[@ref='LogFileAppender']" />
    </root> 
    <appender name="ApplicationInsightsAppender" type="Sitecore.Cloud.ApplicationInsights.Logging.LevelTraceAppender, Sitecore.Cloud.ApplicationInsights" patch:after="appender[@name='LogFileAppender']"> 
      <threshold value="INFO" /> 
        <layout type="log4net.Layout.PatternLayout"> 
          <conversionPattern value="%4t %d{ABSOLUTE} %-5p %m%n" />
        </layout> 
    </appender> 
  </log4net> 
</sitecore> 
</configuration>

Sitecore does log a lot of information that probably should have been marked as debug information. This is fine when logging to a log file. But with Application Insights there are issues with logging too much information:

  • You have to pay for what you log. Although Azure is priced reasonable, it is not free. Any excessive logging can make your budget explode.
  • You might run into  sampling issues (“Data received from your application is being sampled to reduce the volume of telemetry data retained; only sampled documents will be returned. The sampling may be applied by the Application Insights SDK or on ingestion by Application Insights.“). This can be disabled, but sampling usually are there for a reason, as Application Insights cost money the more lines you log.

When I analyze my log files, there are several lines that have never helped me:

ManagedPoolThread #6 00:02:46 INFO Job started: Job started: Index_Update_IndexName=sitecore_Master_index
ManagedPoolThread #9 00:04:48 INFO Scheduling.DatabaseAgent started. Database: master
ManagedPoolThread #13 00:04:48 INFO Cache created: 'ExperienceAnalytics.DimensionItems' (max size: 2MB, running total: 9090MB)

The “Job started” alone can take up to 90% of the log lines in a solution!

To remove these log lines from the Application Insights log, we can use the log4net.Filter.StringMatchFilter. This filter can add or remove lines containing a string.

Filters are added to the appender attribute in the config file like this:

<log4net>
  <root>
    <appender-ref ref="ApplicationInsightsAppender" patch:after="appender-ref[@ref='LogFileAppender']" />
  </root>
  <appender name="ApplicationInsightsAppender" type="Sitecore.Cloud.ApplicationInsights.Logging.LevelTraceAppender, Sitecore.Cloud.ApplicationInsights" patch:after="appender[@name='LogFileAppender']">
    <threshold value="INFO" />
    <layout type="log4net.Layout.PatternLayout">
      <conversionPattern value="%4t %d{ABSOLUTE} %-5p %m%n" />
    </layout>
    <filter type="log4net.Filter.StringMatchFilter">
      <stringToMatch value="Job started: Index_Update_IndexName=" />
      <acceptOnMatch value="false" />
    </filter>
    ...
    ...
  </appender>
</log4net>

The “stringToMatch” is the string that Log4Net looks for, and the acceptOnMatch=false ensure that the log line is not appended to the log.

I have found the following candidates for removal from the Application Insights log:

  • Job started: Index_Update_IndexName=
  • Job ended: Index_Update_IndexName=
  • Job started: UpdateIndexDocuments
  • Job ended: UpdateIndexDocuments
  • Job started: Sitecore.
  • Job ended: Sitecore.
  • Request is redirected to no layout page
  • HttpModule is being initialized
  • File is being deleted by cleanup task
  • Cache created:
  • Health.

There are probably more depending on your Sitecore installation. You should use the old but excellent Sitecore Log Analyzer to analyse the file logs and find the repetitive log lines that you never use, and filter them from the Application Insights log. Remember that what does not help me, might help you. But removing noise really does give a better overview, and your client’s wallet will be happy too :).

MORE TO READ:

 

Posted in Microsoft Azure, Sitecore 8, Sitecore 9 | Tagged , , , , , | 1 Comment

.NET Core Catch Model Binding Exceptions

In .NET Core, if you create an API project, and you have an controller receiving an object you defined yourself, the controller will not be called if you use the wrong model, or if the model is not in a correct JSON/XML format.

This means that you cannot catch the exception in your own try/catch block, which is annoying if you would like to log the exception in your own log file, or to your own Application Insights instance.

To catch model binding exceptions, you need to configure the ApiBehaviorOptions in your ConfigureServices(IServiceCollection services) method.

STEP 1: CALL services.Configure<ApiBehaviorOptions>:

I have made a private method in my Startup.cs file:

private void ConfigureModelBindingExceptionHandling(IServiceCollection services)
{
  services.Configure&lt;ApiBehaviorOptions&gt;(options =>
  {
    options.InvalidModelStateResponseFactory = actionContext =>
    {
      ValidationProblemDetails error = actionContext.ModelState
          .Where(e => e.Value.Errors.Count > 0)
          .Select(e => new ValidationProblemDetails(actionContext.ModelState)).FirstOrDefault();

      // Here you can add logging to you log file or to your Application Insights.
	  // For example, using Serilog:
	  // Log.Error($"{{@RequestPath}} received invalid message format: {{@Exception}}", 
	  //   actionContext.HttpContext.Request.Path.Value, 
	  //   error.Errors.Values);
      return new BadRequestObjectResult(error);
    };
  });
}

STEP 2: CALL THE METHOD AFTER LOG AND/OR APPLICATION INSIGHTS HAVE BEEN INSTANTIATED:

Sequence is important. In the public void ConfigureServices(IServiceCollection services) method, after you have instantiated logging, Application Insights, or whatever you use. Then call the new method:

public void ConfigureServices(IServiceCollection services)
{
  // Add logging, Application Insights, Controllers, ...
  ...
  ...
  // Add exception logging on model binding exceptions
  ConfigureModelBindingExceptionHandling(services, telemetryClient);
  ...
  ...
}

MORE TO READ:

Posted in .NET Core, c#, Microsoft Azure | Tagged , , | 1 Comment

Sitecore use global and environment variables in config files

The Sitecore config files are a complex machine and it requires a healthy mind to work with it. Fortunately Sitecore have implemented a few tricks to ease our pain. This article focuses on 2 parts: global variable replacement and environment variable replacement.

GLOBAL VARIABLES:

Global variables are defined once and can then be reused in all your config settings.

Maybe you have seen these variables:

<sitecore>
  <sc.variable name="dataFolder" value="/App_Data" />
  <sc.variable name="mediaFolder" value="/upload" />
  <sc.variable name="tempFolder" value="/temp" />
</sitecore>

The sc.variable element defines the variable. The variables can then be used where you need them by writing $(variable_name):

<file value="$(dataFolder)/logs/myfile.log"/>
<setting name="IndexFolder" value="$(dataFolder)/indexes" />

The above variables are just the built in ones. You can create your own variables and use them too.

ENVIRONMENT VARIABLES:

Since Sitecore 8.2 we have been blessed with environment variable replacements too.

Let’s say you have an environment variable on your machine:

Global Environment Variables

Global Environment Variables

You can add the environment variable to the config by writing $(env:variable_name):

<setting name="Environment" value="$(env:ASPNETCORE_ENVIRONMENT)" />

This is especially nifty as you can deploy the same config file to different environments and control your application per server.

MORE TO READ:

 

Posted in .net, General .NET, Sitecore 8, Sitecore 9 | Tagged , , , | Leave a comment

ASP.Net Core API – “‘s’ is an invalid start of a value. Path: $ | LineNumber: 0 | BytePositionInLine: 0.”

If you create an ASP.Net Core API Controller, and you wish to create an endpoint that can accept any string, you would expect this to be correct:

using Microsoft.ApplicationInsights;
using Microsoft.AspNetCore.Cors;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Serilog;

namespace My.Api.Controllers
{
  [ApiController]
  [Route("/api/[controller]")]
  [Produces("application/json")]
  public class TestController : ControllerBase
  {
    [HttpPost()]
    [ProducesResponseType(200)]
    [ProducesResponseType(404)]
    public string PostTest([FromBody] string text)
    {
      Log.Information(text);
      return text;
    }
  }
}

But this endpoint will only accept strings in quotes, so posting string will not work, and you get the “‘s’ is an invalid start of a value. Path: $ | LineNumber: 0 | BytePositionInLine: 0.” error. But if you post “string”, the function works.

This is because the method accepts a JSON string, and not a raw string.

There is no direct way of allowing any string to be posted to your endpoint, as the endpoints always expects a type-strong value.

So what can you do?

OPTION 1: CHANGE THE INPUT TO OBJECT

If you know that the input is a JSON object, you can post an object instead:

[HttpPost()]
[ProducesResponseType(200)]
[ProducesResponseType(404)]
public string PostTest([FromBody] object text)
{
  Log.Information(text.ToString());
  return text.ToString();
}

This will accept any JSON object (for example { “a”: “b” }), but not a clean string.

OPTION 2: READ FROM THE REQUEST BODY DIRECTLY:

To accept any raw string, you need to indirectly read the request body:

[HttpPost()]
[ProducesResponseType(201)]
[ProducesResponseType(400)]
public async Task<IActionResult> PostTest()
{
  using (StreamReader reader = new StreamReader(Request.Body, Encoding.UTF8))
  {
    string message = await reader.ReadToEndAsync();
    return message;
  }
}

The downside of streaming the request body to a string is that you cannot test the endpoint using Swagger (or OpenApi as it is called today), as there is nowhere to write the request body. To test it you need a tool such as Postman or CURL.

MORE TO READ:

 

Posted in .NET Core, c# | Tagged , , | Leave a comment

.NET Core API and CORS – allow POST from Javascript using Microsoft.AspNetCore.Cors

After hours of debugging, I finally managed to apply CORS correctly to my .NET Core 3.0 Application. Like so many other before me, I used this article as reference:

But I still managed to get it wrong so many times. CORS is not easy to set up, and even harder to test. But here is how I did it:

STEP 1: DEFINE AN ARBITRARY POLICY NAME:

Since .NET Core allows you to have several CORS policies within your API, you need to define a policy name. I will have one CORS policy for my entire API, and it’s just a string, you can name it whatever you like:

 readonly string _POLICY_NAME = "_myAllowSpecificOrigins";

STEP 2: ADD CORS TO Startup.cs – ConfigureServices(IServiceCollection services)

The Actual CORS rules are added to the ConfigureServices method:

services.AddCors(options =>
{
  options.AddPolicy(_POLICY_NAME,
  builder =>
  {
    builder.WithOrigins("https://*.test-cors.org")
	  .SetIsOriginAllowedToAllowWildcardSubdomains()
	  .AllowAnyHeader()
	  .AllowAnyMethod()
	  .AllowCredentials();
  });
});

Here I allow any subdomain from *.test-cors.org, with any header, any method and also allow credentials.

STEP 3: APPLY CORS TO Startup.cs – Configure(IApplicationBuilder app)

The CORS rules must be applied:

app.UseRouting();
// app.UseCors MUST be placed between UseRouting and UseEndpoints
app.UseCors(_POLICY_NAME); 
...
...
// Also add RequireCors to the controllers:
app.UseEndpoints(endpoints => { endpoints.MapControllers().RequireCors(_POLICY_NAME); });

REMEMBER TO ADD THE POLICY NAME TO BOTH app.UseCors AND app.UseEndpoints. This is where I failed several times.

STEP 4: TEST THE CORS SETTINGS WITH GET AND POST

Use this website to test your CORS settings:

First you test that this website can access your API. It should have access, since it has been added with builder.WithOrigins(https://*.test-cors.org), and we allow wildcard subdomains.

Set a breakpoint in a GET and a POST method, call the endpoints from http://www.test-cors.org. Remember to open the console window (F12) in the browser to see any CORS errors:

STEP 5: TEST THAT CORS BLOCKS DOMAINS FROM GET AND POST

Now go to your code and change the allowed origins, so http://www.test-cors.org is not allowed:

services.AddCors(options =>
{
  options.AddPolicy(_POLICY_NAME,
  builder =>
  {
    builder.WithOrigins("https://*.anotherdomain.org")
	  .SetIsOriginAllowedToAllowWildcardSubdomains()
	  .AllowAnyHeader()
	  .AllowAnyMethod()
	  .AllowCredentials();
  });
});

Go back to the test and try again. Add breakpoints in GET and POST methods, and call the API endpoints once again from http://www.test-cors.org.

Please notice that for GET, your breakpoint is actually hit, but the CORS settings will block the result from getting to the client. For POST, the endpoint is not hit.

In the console you will see the following error:

CORS failed

CORS failed

Access to XMLHttpRequest at ‘https://localhost:44370/api/endpoint from origin ‘https://www.test-cors.org&#8217; has been blocked by CORS policy: Response to preflight request doesn’t pass access control check: No ‘Access-Control-Allow-Origin’ header is present on the requested resource.

So there it is. You are now a CORS expert. Happy coding.

MORE TO READ:

 

Posted in .net, .NET Core, c# | Tagged , , | 1 Comment

Because this call is not awaited, execution of the current method continues before the call is completed. Consider applying the ‘await’ operator to the result of the call

This C# warning occurs if you call an async method from your non-async code.

Imagine you have this imaginary async method:

internal class QueueRepository
{
  public async Task AddMessage<T>(T message)
  {
    await _mock.AddMessageAsync(message);
  }
}

And you call the method from this imaginary non-async method:

QueueRepository rep = new QueueRepository();
rep.AddMessage("hello world");

The compiler will warn you with the following message:

Warning CS4014 Because this call is not awaited, execution of the current method continues before the call is completed. Consider applying the ‘await’ operator to the result of the call.

In this case, the compiler is just being nice, telling you that the async code will run in its own thread, and the code calling the method is continuing to run alongside.

But if you follow the compiler’s suggestion, and add the ‘await’ operator, you get a compiler error:

Error CS4032 The ‘await’ operator can only be used within an async method. Consider marking this method with the ‘async’ modifier and changing its return type to ‘Task<IActionResult>’.

So what can you do?

SOLUTION 1: WAIT FOR THE METHOD

If you wish to wait for the method to finish before continuing, you can call .Wait():

QueueRepository rep = new QueueRepository();
rep.AddMessage("hello world").Wait();

SOLUION 2: FIRE AND FORGET

You are the grown up here, you would actually like to have the method run asynchronously. You can ignore the warning, or add the secret discards ‘_ = ‘ return variable:

QueueRepository rep = new QueueRepository();
_ = rep.AddMessage("hello world");

With the _ = return variable, you tell the compiler to leave you alone you know what you are doing. Please note that you need C# 7.0 for the discards to work.

MORE TO READ:

Posted in .net, .NET Core, c#, General .NET, Microsoft Azure | Tagged , , , | Leave a comment

.NET Core MVC Web API – control DateTime format in JSON output using JsonSerializerOptions

When creating API’s with .NET Core MVC, you can control the JSON output by adding JsonOptions to the controllers:

public void ConfigureServices(IServiceCollection services)
{
  ...
  ...
  services.AddControllers().AddJsonOptions();
  ...
  ...
}

This will ensure that when requesting application/json from a GET method, the format returned is JSON.

You can then add Converters to the configuration, controlling the default behavior of the JSON. This is a DateTime converter, forcing any DateTime type to be outputted as “2019-15-09Y22:30:00

using System.Text.Json;
using System.Text.Json.Serialization;

namespace MyCode.Converters
{
  public class DateTimeConverter : JsonConverter<DateTime>
  {
    public override DateTime Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
    {
      return DateTime.Parse(reader.GetString());
    }

    public override void Write(Utf8JsonWriter writer, DateTime value, JsonSerializerOptions options)
    {
      writer.WriteStringValue(value.ToLocalTime().ToString("yyyy-MM-ddTHH:mm:ss"));
    }
  }
}

The converter needs to be added to my configuration:

public void ConfigureServices(IServiceCollection services)
{
  ...
  ...
  services.AddControllers().AddJsonOptions(options =>
    {
      options.JsonSerializerOptions.Converters.Add(new Converters.DateTimeConverter());
    }
  );
  ...
  ...
}

MORE TO READ:

Posted in .net, .NET Core, c# | Tagged , , , , | 1 Comment

Sitecore Publish Items using the PublishManager

Sitecore have two entries to publishing:

You should use the PublishManager. Sitecore introduced the Publishing Service which is an optional external publisher that is much faster than the built in publishing. It comes with a small price as there are differences to integration points, and publishing behavior. But if you use the PublishManager you will be ready to switch in case you need the improved performance and are willing to manage yet another external service.

Using the Publisher class will only publish through the built in publisher.

The PublishManager is very simple to use:

using Sitecore.Data.Items;
using System;
using Sitecore.Data;
using Sitecore.Publishing;

PublishOptions publishOptions = new PublishOptions(item.Database, Database.GetDatabase("web"), publishMode, item.Language, DateTime.Now);
publishOptions.RootItem = item;
publishOptions.Deep = deepPublish;
publishOptions.PublishRelatedItems = publishRelatedItems;
publishOptions.CompareRevisions = compareRevisions;

var handle = PublishManager.Publish(new PublishOptions[] { publishOptions });
PublishManager.WaitFor(handle);

The WaitFor() call is only needed if you wish to wait for the publish to end before continuing.

You can create a simple extension method that can publish an item like this:

using Sitecore.Data.Items;
using System;
using System.Globalization;
using Sitecore.Data;
using Sitecore.Diagnostics;
using Sitecore.Publishing;

namespace MyCode
{
  public static class ItemExtensions
  {
    public static void PublishItem(this Item item, PublishMode publishMode, bool publishAsync = false, bool deepPublish = false, bool publishRelatedItems = false, bool compareRevisions = false)
    {
      
      if (item == null)
        return;

      PublishOptions publishOptions = new PublishOptions(item.Database, Database.GetDatabase("web"), publishMode, item.Language, DateTime.Now);
      publishOptions.RootItem = item;
      publishOptions.Deep = deepPublish;
      publishOptions.PublishRelatedItems = publishRelatedItems;
      publishOptions.CompareRevisions = compareRevisions;

      var handle = PublishManager.Publish(new PublishOptions[] { publishOptions });
      if (publishAsync)
        return;
      PublishManager.WaitFor(handle);
    }
  }
}

And you can use the extension method like this:

Item dbItem = Context.ContentDatabase.GetItem(xxx,xxx,xxx);
dbItem.PublishItem(PublishMode.SingleItem, false, false, false, false);

Thanks to Stephen Pope for reminding me of the existence of the PublishingManager class.

MORE TO READ:

Posted in .net, c#, General .NET, Sitecore 8, Sitecore 9 | Tagged , , | 2 Comments

Sitecore publish certain items on save

My Sitecore solution have some items, where changes to certain fields are so critical that they need to be published immediately. So when my user presses the Save button, I will open a popup dialog, asking if I should publish the change:

Catalog Changed

Catalog Changed

If you click OK, the item is published.

EXTENDING THE SAVEUI PIPELINE

To achieve this, you have several tools in the Sitecore toolbox. One is to extend the SaveUI pipeline. This operation requires me to hook in just after the Save processor, as we need to publish the saved changes:

<?xml version="1.0" encoding="utf-8"?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/" xmlns:env="http://www.sitecore.net/xmlconfig/env/">
  <sitecore>
    <processors>
      <saveUI>
        <processor patch:after="processor[@type='Sitecore.Pipelines.Save.Save, Sitecore.Kernel']" type="MyCode.MyProcessor, MyDll" />
      </saveUI>
    </processors>
  </sitecore>
</configuration>

CREATING A SAVEUI PROCESSOR

The processor consists of 2 parts: One that checks if the template have been saved and shows the dialog box, and another that publishes the item if the user clicks yes in the box:

using System;
using Sitecore;
using Sitecore.Data;
using Sitecore.Data.Items;
using Sitecore.Diagnostics;
using Sitecore.Publishing;

namespace MyCode
{
  public class Myprocessor
  {
    // If this template is changed we request a publish
    private static ID _TEMPLATE_ID = new ID("{C24F3426-F599-482E-BBAA-A50645AD3304}");

    public void Process(Sitecore.Pipelines.Save.SaveArgs args)
    {
      Assert.ArgumentNotNull(args, "args");

      if (!args.HasSheerUI)
      {
        return;
      }

      // This piece of code is executed when the user clicks
      // on yes or no in the dialog box
      if (args.IsPostBack)
      {
        Context.ClientPage.Modified = false;
        if (args.Result == "yes")
        {
          foreach (Sitecore.Pipelines.Save.SaveArgs.SaveItem uiItem in args.Items)
          {
            Item dbItem = Context.ContentDatabase.GetItem(uiItem.ID, uiItem.Language, uiItem.Version);
            // PublishItem is an extension metod. I'll explain this one later
            dbItem.PublishItem(PublishMode.SingleItem, true, false, false, false);
          }
        }
        return;
      }

      Assert.IsNotNull(Context.ContentDatabase, "Sitecore.Context.ContentDatbabase");
      foreach (Sitecore.Pipelines.Save.SaveArgs.SaveItem uiItem in args.Items)
      {
        // This code checks if it is the template in question that have been saved
        Item dbItem = Context.ContentDatabase.GetItem(uiItem.ID, uiItem.Language, uiItem.Version);
        if (dbItem.TemplateID == _TEMPLATE_ID)
        {
          // Yes, the template have been saved, but do not ask for a publish
          // if the item is not publishable
          if (!dbItem.Publishing.IsPublishable(DateTime.Now, false))
            return;
          // Show the popup dialog and wait for the respone
          Sitecore.Web.UI.Sheer.SheerResponse.Confirm($"You have changed this catalog. You need to publish it. Would you like to publish now?");
          args.WaitForPostBack();
          return;
        }
      }
    }
  }
}

PUBLISH ITEM USING AN EXTENSION METHOD:

UPDATE 2019-09-13: There is another way of publishing. Read Sitecore Publish Items using the PublishManager for an improved way of publishing.

To do the publish I created this Item extension method:

namespace MyNamespace
{
  public static class ItemExtensions
  {
    public static void PublishItem(this Item item, PublishMode publishMode, bool publishAsync = false, bool deepPublish = false, bool publishRelatedItems = false, bool compareRevisions = false)
    {
      if (item == null)
        return;

      PublishOptions publishOptions = new PublishOptions(item.Database, Database.GetDatabase("web"), publishMode, item.Language, DateTime.Now);
      publishOptions.RootItem = item;
      publishOptions.Deep = deepPublish;
      publishOptions.PublishRelatedItems = publishRelatedItems;
      publishOptions.CompareRevisions = compareRevisions;

      Publisher publisher = new Publisher(publishOptions);
      if(publishAsync)
        publisher.PublishAsync();
      else
        publisher .Publish();
    }
  }
}

FINAL NOTE: BEWARE OF THE CTRL+S BUG IN SITECORE

Sitecore have a bug where the save event is called twice when clicking CTRL+S instead of the [Save] button. This can mess up the SheerResponse and can lead to your checkboxes and dropdowns to be emptied. The bug is easy to fix:

Sitecore CTRL+S fires item:saving event twice

MORE TO READ:

 

 

Posted in .net, c#, Sitecore 6, Sitecore 7, Sitecore 8, Sitecore 9 | Tagged , , | 1 Comment