Skip to content
This repository has been archived by the owner on Aug 31, 2021. It is now read-only.

Commit

Permalink
[C#] Add exercise User-defined Exceptions (#1617)
Browse files Browse the repository at this point in the history
* csharp/user-exceptions initial upload

* csharp/user-exceptions initial code/instructions complete

* csharp/user-exceptions tweak to design.md

* csharp/user-exceptions proofing

* csharp/user-exceptions proofing

* csharp/user-exceptions after review

* csharp/user-exceptions after review

* csharp/user-exceptions intro and after

* csharp/user-exceptions tweaking docs

* Update languages/csharp/exercises/concept/user-defined-exceptions/.meta/Example.cs

Co-authored-by: Erik Schierboom <erik_schierboom@hotmail.com>

* Apply suggestions from code review

Co-authored-by: Erik Schierboom <erik_schierboom@hotmail.com>

* csharp/user-exceptions more review changes

* csharp/user-exceptions substantially complete

* csharp/user-exceptions removed text not required for publication

* csharp/user-exceptions added pre-requisites

* csharp/user-exceptions updated out-of-date instructions

* csharp/user-exceptions updated config

* Apply suggestions from code review

Co-authored-by: Erik Schierboom <erik_schierboom@hotmail.com>

* Update languages/csharp/exercises/concept/user-defined-exceptions/.docs/after.md

Co-authored-by: Erik Schierboom <erik_schierboom@hotmail.com>

* csharp/user-exceptions late review points

* Update after.md

* copy/paste error

Co-authored-by: Erik Schierboom <erik_schierboom@hotmail.com>
  • Loading branch information
mikedamay and ErikSchierboom authored Jun 17, 2020
1 parent 2b62cd8 commit 3945e09
Show file tree
Hide file tree
Showing 11 changed files with 363 additions and 0 deletions.
13 changes: 13 additions & 0 deletions languages/csharp/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,19 @@
"uuid": "eda235a5-37de-4fb6-a5c5-cf7aa2309987",
"concepts": ["strings"],
"prerequisites": ["basics"]
},
{
"slug": "user-defined-exceptions",
"uuid": "1be577b0-95db-4c09-aff4-ddc68fb7520d",
"concepts": ["user-defined-exceptions", "exception-filtering"],
"prerequisites": [
"exceptions",
"inheritance",
"strings",
"conditionals",
"arithmetic-overflow",
"signed-integers"
]
}
],
"practice": []
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
A user-defined exception is any class defined in your code that is derived from `System.Exception`. It is subject to all the rules of class inheritance but in addition the compiler and language runtime treat such classes in a special way allowing their instances to be thrown and caught outside the normal control flow as discussed in the `exceptions` exercise. User-defined exceptions can be used in every way like runtime and Microsoft Base Class Library exceptions.

This special treatment applies only to `Exception`-derived classes. You cannot throw instances of any other type.

User-defined exceptions are often used to carry extra information such as a message and other relevant data to be made available to the catching routines. This can then be recorded in logs, reported to a user or perhaps used in a retry operation. `System.Exception` has convenient data members and appropriate constructors to hold a message and the "inner" exception.

By convention exception class names end with "Exception", e.g. `MyTerribleException`.

Whilst using user-defined exceptions to wrap and enhance third party exceptions is a frequently seen pattern, the general advice is not to use them outside of this use case too liberally in your own code. It is considered an anti-pattern. There are challenges to this view and you can see both sides of the argument in this [Stack Exchange post][se-exceptions].

This [article][create-user-defined-exceptions] is a good introduction to user-defined exceptions.

As part of their guidance on [creating and throwing exceptions][exceptions-guidance] the .NET team recommend that user defined exceptions have a number of [convenience constructors][convenience-constructors]. For most purposes the combination illustrated below is appropriate.

```csharp
public class IncompleteExerciseException : System.Exception
{
public IncompleteExerciseException() : base() { }
public IncompleteExerciseException(string message) : base(message) { }
public IncompleteExerciseException(string message, System.Exception inner) : base(message, inner) { }
}
```

## Exception Filters

`when` is the keyword in filtering exceptions. It is placed after the catch
statement and can take a boolean expression containing any values in scope at the time. They don't just have to be members of the exception itself. If the type of the exception matches and the expression evaluates to true then the block associated with that `catch` statement is executed otherwise the next `catch` statement, if any, is checked.

```csharp
try
{
// do stuff
}
catch (Exception ex) when (ex.Message != "")
{
// output the message when it is not empty
}
catch (Exception ex)
{
// show stack trace or something.
}
```

- This [Exception filters][exception-filters] article shows how to filter exceptions.

[create-user-defined-exceptions]: https://docs.microsoft.com/en-us/dotnet/standard/exceptions/how-to-create-user-defined-exceptions
[exception-filters]: https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/when
[se-exceptions]: https://softwareengineering.stackexchange.com/questions/189222/are-exceptions-as-control-flow-considered-a-serious-antipattern-if-so-why
[exceptions-guidance]: https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/exceptions/creating-and-throwing-exceptions
[convenience-constructors]: https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/exceptions/creating-and-throwing-exceptions#defining-exception-classes
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
## General

- [Create user-defined exceptions][create-user-defined-exceptions]: how to create user-defined exceptions
- [Exception filters][exception-filters]: how to filter user-defined-exceptions.

## 1. Complete the definition of the user-defined exception `CalculationException`

- The constructors of the `Exception` base class are discussed [here][exception-constructors].

## 2. Implement the `Multiply()` method

- `try-catch` blocks are discussed [here][try-catch] as well as in the `exceptions` exercise.

## 4. Implement the `TestMultiplication()` method

- This [article][try-catch-when] has an example of the use of `when`.

[create-user-defined-exceptions]: https://docs.microsoft.com/en-us/dotnet/standard/exceptions/how-to-create-user-defined-exceptions
[exception-filters]: https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/when
[exception-constructors]: https://docs.microsoft.com/en-us/dotnet/api/system.exception.-ctor?view=netcore-3.1
[try-catch]: https://docs.microsoft.com/en-us/dotnet/standard/exceptions/how-to-use-the-try-catch-block-to-catch-exceptions
[try-catch-when]: https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/try-catch
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
While working at _Instruments of Texas_, you are tasked to work on an experimental calculator written in C#. You are building a test harness to verify a number of calculator functions starting with multiplication. You will see that there is particular concern when the two operands of the multiplication are negative.

The `Calculator` class has been provided for you and should not be modified.

## 1. Complete the definition of the user-defined exception `CalculationException`

Complete the definition of the constructor of `CalculationException` which will need to store (wrap) any exception thrown by the calculator as well as the operands that are being processed at the time the exception is thrown.

## 2. Handle overflow conditions in the calculator and provide enhanced information to the caller

Implement the `CalculatorTestHarness.Multiply()` method, which should call the `Calculator.Multiply()` method of the `Calculator` instance passed to the constructor.
passing in x and y integer values. If an overflow occurs in the `Calculator.Multiply()` method, it will throw an `OverflowException`. This exception should be caught in the `CalculatorTestHarness.Multiply()` method and wrapped in a `CalculationException` and the x and y values being passed around should be stored as the exception's operands. The newly created `CalculationException` object should be thrown. You can ignore the value returned by the `Calculator.Multiply()` method if it is successful.

```csharp
var cth = new CalculatorTestHarness(new Calculator());
cth.Multiply(Int32.MaxValue, Int32.MaxValue);
// => throws an instance of CalculationException
var cth2 = new CalculatorTestHarness(new Calculator());
cth2.Multiply(3, 2);
// => silently exits
```

## 3. Test the multiplication operation for valid inputs

Implement the `CalculatorTestHarness.TestMultiplication()` method which takes two integers and calls the `CalculatorTestHarness.Multiply()` method. `"Multiply succeeded"` is returned.

```csharp
var cth = new CalculatorTestHarness(new Calculator());
cth.Multiply(6, 7);
// => "Multiply succeeded"
```

## 4. Test the multiplication operation for negative inputs

Modify the `CalculatorTestHarness.TestMultiplication()` method so that `"Multiply failed for negative operands. <INNER_EXCEPTION_MESSAGE>"`is returned if both integer arguments are negative (less than zero).

The `<INNER_EXCEPTION_MESSAGE>` placeholder should be replaced with the `CalculationException`'s inner exception's message.

```csharp
var cth = new CalculatorTestHarness(new Calculator());
cth.TestMultiplication(-2, -Int32.MaxValue);
// => "Multiply failed for negative operands. " + innerException.Message
```

## 5. Test the multiplication operation for positive inputs

Modify the `CalculatorTestHarness.TestMultiplication()` method so that `"Multiply failed for mixed or positive operands. <INNER_EXCEPTION_MESSAGE>"` is returned if at least one of the integer arguments is not negative.

The `<INNER_EXCEPTION_MESSAGE>` placeholder should be replaced with the `CalculationException`'s inner exception's message.

```csharp
var cth = new CalculatorTestHarness(new Calculator());
cth.TestMultiplication(Int32.MaxValue, Int32.MaxValue);
// => "Multiply failed for mixed or positive operands. " + innerException.Message
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
A user-defined exception is any class defined in your code that is derived from `System.Exception`. It is subject to all the rules of class inheritance but in addition the compiler and language runtime treat such classes in a special way allowing their instances to be thrown and caught outside the normal control flow as discussed in the `exceptions` exercise.

User-defined exceptions are often used to carry extra information such as a message and other relevant data to be made available to the catching routines.

## Exception Filters

`when` is the key word in filtering exceptions. It is placed after the catch
statement and can take a boolean expression containing any values in scope at the time. If the expression evaluates to true then the block associated with that `catch` statement is executed otherwise the next `catch` statement, if any, is checked.

```csharp
try
{
// do stuff
}
catch (Exception ex) when (ex.Message != "")
{
// output the message when it is not empty
}
catch (Exception ex)
{
// show stack trace or something.
}
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
using System;

public class CalculationException : Exception
{
public CalculationException(int operand1, int operand2, string message, Exception inner) : base(message, inner)
{
Operand1 = operand1;
Operand2 = operand2;
}

public int Operand1 { get; }
public int Operand2 { get; }
}

public class CalculatorTestHarness
{
private Calculator calculator;

public CalculatorTestHarness(Calculator calculator)
{
this.calculator = calculator;
}

public string TestMultiplication(int x, int y)
{
try
{
Multiply(x, y);
return "Multiply succeeded";
}
catch (CalculationException cex) when (cex.Operand1 < 0 && cex.Operand2 < 0)
{
return "Multiply failed for negative operands. " + cex.InnerException.Message;
}
catch (CalculationException cex)
{
return "Multiply failed for mixed or positive operands. " + cex.InnerException.Message;
}
}

public void Multiply(int x, int y)
{
try
{
calculator.Multiply(x, y);
}
catch (OverflowException ofex)
{
throw new CalculationException(x, y, string.Empty, ofex);
}
}
}


// Please do not modify the code below.
// If there is an overflow in the multiplication operation
// then a System.OverflowException is thrown.
public class Calculator
{
public int Multiply(int x, int y)
{
checked
{
return x * y;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"authors": [
{
"github_username": "mikedamay",
"exercism_username": "mikedamay"
}
],
"forked_from": ["elixir/errors"]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
## Learning objectives

- Know how to define a user-defined exception.
- Know how to use exception filtering.
- Know that using errors as control logic is an anti-pattern

## Out of scope

- Memory and performance characteristics.

## Concepts

This Concepts Exercise's Concepts are:

- `user-defined-exceptions`: know how to define a user-defined exception.
- `exception-filtering`: know how to use exception filtering.

## Prequisites

This Concept Exercise's prerequisites Concepts are:

- `exceptions`: know how to work with exceptions.
- `inheritance`: inheriting from the `Exception` class for the custom exception.
- `strings`: converting an into a string
- `conditionals`: use of simple `if`/`else`
- `arithmetic-overflow`
- `signed-integers`: `Int32.MaxValue`
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>netcoreapp3.1</TargetFramework>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.6.1" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.1" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
using System;
using Xunit;

public class UserDefinedExceptionsTests
{
[Fact]
public void Multiply_with_overflow_throws_calculationexception()
{
var cth = new CalculatorTestHarness(new Calculator());

Assert.Throws<CalculationException>(() => cth.Multiply(Int32.MaxValue, Int32.MaxValue));
}

[Fact(Skip = "Remove this Skip property to run this test")]
public void TestMultiplication_with_negative_overflow()
{
var cth = new CalculatorTestHarness(new Calculator());
Assert.Equal("Multiply failed for negative operands. Arithmetic operation resulted in an overflow.",
cth.TestMultiplication(-2, -Int32.MaxValue));
}

[Fact(Skip = "Remove this Skip property to run this test")]
public void TestMultiplication_with_positive_overflow()
{
var cth = new CalculatorTestHarness(new Calculator());
Assert.Equal("Multiply failed for mixed or positive operands. Arithmetic operation resulted in an overflow.",
cth.TestMultiplication(Int32.MaxValue, Int32.MaxValue));
}

[Fact(Skip = "Remove this Skip property to run this test")]
public void Call_TestMultiplication_with_success()
{
var cth = new CalculatorTestHarness(new Calculator());
Assert.Equal("Multiply succeeded", cth.TestMultiplication(6, 7));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
using System;

public class CalculationException : Exception
{
public CalculationException(int operand1, int operand2, string message, Exception inner)
// TODO: complete the definition of the constructor
{
}

public int Operand1 { get; }
public int Operand2 { get; }
}

public class CalculatorTestHarness
{
private Calculator calculator;

public CalculatorTestHarness(Calculator calculator)
{
this.calculator = calculator;
}

public string TestMultiplication(int x, int y)
{
throw new NotImplementedException("Please implement the CalculatorTestHarness.TestMultiplication() method");
}

public void Multiply(int x, int y)
{
throw new NotImplementedException("Please implement the CalculatorTestHarness.Multiply() method");
}
}


// Please do not modify the code below.
// If there is an overflow in the multiplication operation
// then a System.OverflowException is thrown.
public class Calculator
{
public int Multiply(int x, int y)
{
checked
{
return x * y;
}
}
}

0 comments on commit 3945e09

Please sign in to comment.