During network operations and IO, there are often limitations that can be easily solved even by the user (albeit experienced, yes), but it is difficult to learn about them in advance. A busy file, an unstable connection - I often lack the buttons to repeat, especially when the whole process takes and rolls back.

So far, on my knee, I have implemented a more or less universal solution to this problem:

public enum ExceptionHandle { Abort, Retry, Ignore } public class ExceptionEventArgs { public Exception Exception { get; } public ExceptionHandle? Handled { get; set; } public ExceptionEventArgs(Exception ex) { this.Exception = ex; } } public static class ExceptionHandler { public static event EventHandler<ExceptionEventArgs> Handler; public static void TryExecute(Action action) { TryExecute(() => { action(); return true; }, false); } public static T TryExecute<T>(Func<T> action, T whenIgnored) { ExceptionHandle? handled = ExceptionHandle.Retry; while (handled == ExceptionHandle.Retry) { try { return action(); } catch (Exception ex) { handled = OnHandler(new ExceptionEventArgs(ex)); if (handled.HasValue) { switch (handled.Value) { case ExceptionHandle.Abort: throw; break; case ExceptionHandle.Retry: break; case ExceptionHandle.Ignore: break; default: throw new ArgumentOutOfRangeException(); } } else { throw; } } } return whenIgnored; } private static ExceptionHandle? OnHandler(ExceptionEventArgs e) { if (Handler == null || !Handler.GetInvocationList().Any()) { ExceptionDispatchInfo.Capture(e.Exception).Throw(); } else { Handler.Invoke(null, e); } return e.Handled; } } 

Thus, any ExceptionHandler.Handler subscriber can either resolve the problems automatically, or throw the solution on the user. Any dangerous code can now be wrapped:

  var tested = ExceptionHandler.TryExecute(() => { using (var destination = new MemoryStream()) { using (Stream stream = entry.Open()) stream.CopyTo(destination); return destination.Length == entry.Length; } }, false); 

In general, the current implementation seems to me already tolerable and it works. But, I suspect that such solutions are already there somewhere, I just could not find them. Can someone advise where to get or at least look at ready-made solutions? Well, if there are jambs in my code, I would not refuse help either.

UPD: Yes, I understand that even so there are problem situations - the action can be one-time (close the connection, crash the sql session, and whatever you want to do). It already remains on conscience of the one who uses the code. Although, I would also look at interesting options on this issue, you will limit this fig.

UPD2: I have not yet been able to figure out whether it is possible to wrap one such unit in another, but now as a result, on the abortion of the internal unit, the external unit goes back to processing.

    3 answers 3

    I do not think that there is a general solution to the problem. Since what you encode is essentially a business logic that is “as diverse as life itself”, you cannot cover all possible cases in advance.

    Offhand that the code is wrong:

    1. Execution occurs synchronously, in a blocking manner. This is not always the case; very often, the “building blocks” of business logic are formed from asynchronous procedures. You do not have this opportunity.
    2. It seems to me that the model of subscription to an event at an external object is too complicated and violates linear logic. You have one, static ExceptionHandler instance, which means that you will have to sign several handlers at the same time. At the same time, a mechanism is needed that decides whether this handler is responsible for this error or not. You do not have this logic, and it will turn out to be quite complicated. Also, a universal object should have problems with multithreaded access as soon as you try to make it harder.
    3. Very often, simply repeating an action is not enough, since the conditions have not changed, which means that a new action will end with the same error. Some additional action is needed: pause, pick up other source data, conduct a dialogue with the user and. etc. In your design, all these additional actions will have to be packaged in an event handler, which is not very readable.

    It seems to me that you should not try to build a universal logic, this will not work, because there are a lot of cases. It is much better, simpler and more efficient to write small auxiliary functions for every occasion, and collect them into utility classes.

    For example, such a simple logic

     static async Task<T> TrySeveralTimesWithGrowingTimeout<T>( int count, Func<Task<T>> taskCreator, TimeSpan timeoutDiff, Action<string> failureLogger) { TimeSpan timeout = TimeSpan.Zero; for (int i = 0; i < count; i++) // count раз: { timeout += timeoutDiff; // нарастим таймаут try { // пытаемся выполнить Task, если не будет return await taskCreator(); // исключения, возвращаем результат } catch (Exception ex) when (i < count - 1) // ловим исключение всегда, кроме { // последней итерации failureLogger($"Operation failed #{i}, retrying after {timeout}. " + $"Exception was {ex.Message}"); // залогировали } await Task.Delay(timeout); // выдерживаем паузу до следующего запуска } // сюда мы не попадём, т. к. если последняя итерация провалилась, // то было выброшено исключение throw new Exception("cannot happen"); } 

    It's hard enough to code in ExceptionHandler terms.

    The test function is as follows:

     async Task<string> GetFileContent(string path) { using (var sr = new StreamReader(path)) return await sr.ReadToEndAsync(); } // ... try { var text = await TrySeveralTimesWithGrowingTimeout( count: 3, taskCreator: () => GetFileContent(path), timeoutDiff: TimeSpan.FromSeconds(10), failureLogger: Console.Error.WriteLine); // тут ещё километр логики Console.WriteLine(text); } catch (IOException ex) { Console.Error.WriteLine($"Failed: {ex.Message}"); } 

    But this is not a universal function, it is just an example of something that can be easily coded manually.

    • c # 6 to all fields :-D - Grundy
    • @Grundy: And that! Need to promote progress! - VladD
    • I honestly never mastered the exception handling with await, it feels like you can just do it. - Monk
    • @Monk: Why, strange. Everything is very simple there. await throws an exception that occurred in Task 'e. - VladD
    • Brr, read your sample code, and did not understand why it =) How to use it, can you add an example? - Monk

    "I am not a doctor, but I can see" (C)

    The ready solution was found in the vastness of the web and looks quite good:

     public interface ISequentialActivity { bool Run(); } public enum UserAction { Abort, Retry, Ignore } public class FailureEventArgs { public UserAction Action = UserAction.Abort; } public class SequentialActivityMachine { private Queue<ISequentialActivity> activities = new Queue<ISequentialActivity>(); public event Action<FailureEventArgs> OnFailed; protected void PerformOnFailed(FailureEventArgs e) { var failed = this.OnFailed; if (failed != null) failed(e); } public void Add(ISequentialActivity activity) { this.activities.Enqueue(activity); } public void Run() { while (this.activities.Count > 0) { var next = activities.Peek(); if (!next.Run()) { var failureEventArgs = new FailureEventArgs(); PerformOnFailed(failureEventArgs); if (failureEventArgs.Action == UserAction.Abort) return; if (failureEventArgs.Action == UserAction.Retry) continue; } activities.Dequeue(); } } } 
    • I see no fundamental difference. Regarding my option - added dependency on the interface, added the need to call Run on SequentialActivityMachine . - Monk

    There is a way using PostSharp .
    The solution was developed on the basis of examples: example number 1 , example number 2 .

    Benefits:

    • The source code undergoes minimal changes, only one attribute is added to the methods.
    • No restrictions on the methods. The attribute can be applied to both static and instance methods, to constructors and properties, with or without parameters.

    Disadvantages:

    • Although PostSharp has a free version, the normal version costs money. Perhaps there are free analogs with similar functionality ...
    • Exception autoprocessing works only for the entire method, and does not work for part of the method.
    • Albeit small, but an increase in compile time and code execution time.
    • Additional dependency in the project.
    • It is not possible to stuff a delegate with an attribute in the standard means, so the current exception handler is in the static property CurrentExceptionHandler.HandlerFunc . This is a crutch.

    Code:

     public enum ExceptionHandlerResult { Abort, Retry, Ignore } public static class CurrentExceptionHandler { public static Func<Exception, ExceptionHandlerResult> HandlerFunc { get; set; } = DefaultHandlerFunc; public static ExceptionHandlerResult DefaultHandlerFunc(Exception e) { var response = MessageBox.Show(e.ToString(), "Error", MessageBoxButtons.AbortRetryIgnore, MessageBoxIcon.Error); switch (response) { case DialogResult.Abort: return ExceptionHandlerResult.Abort; case DialogResult.Retry: return ExceptionHandlerResult.Retry; case DialogResult.Ignore: return ExceptionHandlerResult.Ignore; default: throw new ArgumentOutOfRangeException(); } } } [PSerializable] public class HandleExceptionAttribute : MethodInterceptionAspect { /// <summary> /// If the value is negative, ignore the retries count. /// </summary> public int MaxRetries { get; set; } public override void OnInvoke(MethodInterceptionArgs args) { int retriesCount = 0; while (true) { try { base.OnInvoke(args); return; } catch (Exception e) { Console.WriteLine("Exception during attempt {0} of calling method {1}.{2}: {3}", retriesCount, args.Method.DeclaringType, args.Method.Name, e.Message); var handlerFunc = CurrentExceptionHandler.HandlerFunc; if (handlerFunc == null) throw; var response = handlerFunc(e); switch (response) { case ExceptionHandlerResult.Abort: throw; case ExceptionHandlerResult.Retry: retriesCount++; if (MaxRetries >= 0 && retriesCount > MaxRetries) throw; continue; case ExceptionHandlerResult.Ignore: return; } } } } } 

    Example of use:

     [HandleException(MaxRetries = 10)] private static void TestMethod(int n) { throw new ApplicationException(); } TestMethod(1); 

    To use your exception handler, you need to set it as the value of the static CurrentExceptionHandler.HandlerFunc property.

    • Never worked with postsharp, I will try your example and see how it goes. The idea itself is interesting, but with the handler it becomes already sad. Must watch. - Monk
    • I tried. In general, it looks interesting. I didn’t even think about the idea with the maximum number of attempts. It works - it works, but the restriction that you can only use the method, plus the limitations of the free version ... not for home use, all the same. - Monk