Как я должен обрабатывать исключения в конструкторе контроллера в WebAPI?

Скажем, у меня есть конструктор, где его инициализация потенциально может вызвать исключение по причинам, не зависящим от меня.

FantasticApiController(IAwesomeGenerator awesome,
    IBusinessRepository repository, IIceCreamFactory factory)
{
       Awesome = awesome;
       Repository = repository;
       IceCream = factory.MakeIceCream();

       DoSomeInitialization(); // this can throw an exception
}

Обычно, когда действие Controller в WebAPI генерирует исключение, я могу обработать его с помощью csutom ExceptionFilterAttribute:

public class CustomErrorHandler
{
    public override void OnException(HttpActionExecutedContext context)
    {
        // Critical error, this is real bad.
        if (context.Exception is BubonicPlagueException)
        {
            Log.Error(context.Exception, "CLOSE EVERYTHING!");
            Madagascar.ShutdownAllPorts();
        }

        // No big deal, just show something user friendly
        throw new HttpResponseException(new HttpResponseMessage
        {
            Content = new StringContent("Hey something bad happened. " +
                                        "Not closing the ports though"),
            StatusCode = HttpStatusCode.InternalServerError;
        });
    }

Итак, если у меня есть метод API BoardPlane, который генерирует BubonicPlagueException, то мой CustomerErrorHandler отключит порты на Мадагаскар и зарегистрирует его как ошибку, как ожидалось. В других случаях, когда это не очень серьезно, я просто показываю какое-то удобное для пользователя сообщение и возвращаю 500 InternalServerError.

Но в тех случаях, когда DoSomeInitialization генерирует исключение, это абсолютно ничего не делает. Как я могу обрабатывать исключения в конструкторах контроллера WebAPI?

Ответы

Ответ 1

Создаются контроллеры WebApi и, следовательно, конструкторы, вызываемые через HttpControllerActivators. Активатором по умолчанию является System.Web.Http.Dispatcher.DefaultHttpControllerActivator.

Очень грубые примеры для вариантов 1 и 2 на github здесь https://github.com/markyjones/StackOverflow/tree/master/ControllerExceptionHandling/src

Вариант 1, который работает довольно хорошо, подразумевает использование контейнера DI (вы можете использовать его уже). Я использовал Ninject для моего примера и использовал "Перехватчики" Подробнее, чтобы перехватить и попробовать/уловить вызовы метода Create в DefaultHttpControllerActivator. Я знаю, по крайней мере, AutoFac и Ninject, которые могут сделать что-то похожее на следующее:

Создайте перехватчик

Я не знаю, какова продолжительность жизни ваших мадагаскаров и элементов журнала, но они вполне могут быть введены в ваш Interceptor

public class ControllerCreationInterceptor : Ninject.Extensions.Interception.IInterceptor
{
    private ILog _log;
    private IMadagascar _madagascar;

    public ControllerCreationInterceptor(ILog log, IMadagascar madagascar)
    {
        _log = log;
        _madagascar = madagascar;
    }

Но придерживаясь примера в вашем вопросе, где Log и Мадагаскар являются своего рода статическим глобальным

public class ControllerCreationInterceptor : Ninject.Extensions.Interception.IInterceptor
{

    public void Intercept(Ninject.Extensions.Interception.IInvocation invocation)
    {
        try
        {
            invocation.Proceed();
        }
        catch(InvalidOperationException e)
        {
            if (e.InnerException is BubonicPlagueException)
            {
                Log.Error(e.InnerException, "CLOSE EVERYTHING!");
                Madagascar.ShutdownAllPorts();
                //DO SOMETHING WITH THE ORIGIONAL ERROR!
            }
            //DO SOMETHING WITH THE ORIGIONAL ERROR!
        }
    }
}

ЗАКЛЮЧИТЕЛЬНО Зарегистрируйте перехватчик В глобальном asax или App_Start (NinjectWebCommon)

kernel.Bind<System.Web.Http.Dispatcher.IHttpControllerActivator>()
            .To<System.Web.Http.Dispatcher.DefaultHttpControllerActivator>().Intercept().With<ControllerCreationInterceptor>();

Вариант 2 заключается в том, чтобы реализовать собственный Activator Activator, реализующий интерфейс IHttpControllerActivator, и обрабатывать ошибку при создании контроллера в методе Create. Вы можете использовать шаблон декоратора, чтобы обернуть DefaultHttpControllerActivator:

public class YourCustomControllerActivator : IHttpControllerActivator
{
    private readonly IHttpControllerActivator _default = new DefaultHttpControllerActivator();

    public YourCustomControllerActivator()
    {

    }

     public System.Web.Http.Controllers.IHttpController Create(System.Net.Http.HttpRequestMessage request, System.Web.Http.Controllers.HttpControllerDescriptor controllerDescriptor, Type controllerType)
    {
        try
        {
            return _default.Create(request, controllerDescriptor, controllerType);
        }
        catch (InvalidOperationException e)
        {
            if (e.InnerException is BubonicPlagueException)
            {
                Log.Error(e.InnerException, "CLOSE EVERYTHING!");
                Madagascar.ShutdownAllPorts();
                //DO SOMETHING WITH THE ORIGIONAL ERROR!
            }
            //DO SOMETHING WITH THE ORIGIONAL ERROR!
            return null;
        }

    }
}

Когда у вас есть собственный пользовательский активатор, активатор по умолчанию может отключить в глобальном asax:

  GlobalConfiguration.Configuration.Services.Replace(typeof(IHttpControllerActivator), new YourCustomControllerActivator());

Вариант 3 Конечно, если ваша инициализация в конструкторе не нуждается в доступе к действительным методам контроллеров, свойствам и т.д., т.е. предполагается, что он может быть удален из конструктора... тогда он было бы намного проще просто перенести инициализацию на фильтр, например

public class MadagascarFilter : AbstractActionFilter
{
    public override void OnActionExecuting(System.Web.Http.Controllers.HttpActionContext actionContext)
    {
    try{
          DoSomeInitialization(); // this can throw an exception
        }
        catch(BubonicPlagueException e){
    Log.Error(e, "CLOSE EVERYTHING!");
        Madagascar.ShutdownAllPorts();
            //DO SOMETHING WITH THE ERROR                           
        }

        base.OnActionExecuting(actionContext);
    }

public override void OnActionExecuted(System.Web.Http.Filters.HttpActionExecutedContext actionExecutedContext)
    {
        base.OnActionExecuted(actionExecutedContext);
    }

    public override bool AllowMultiple
    {
        get { return false; }
    }


}