The project uses the Error class to create new error response objects:
public class Error
{
public int StatusCode { get; set; }
public string Message { get; set; }
public Error(int status, string msg)
{
StatusCode = status;
Message = msg;
}
}The Error class uses business logic needed through the project. This leaves the gaps open for the accidental or purposeful (lazy)
misuse. To keep domain invariants intact, create a wrapper (Reply) class that ensures that all errors follow the same structure and semantics.
The goal is to have only one error-handling solution via the Reply class.
The Reply class is an imagination of the Result pattern (simplified for this example).
public class Reply
{
public static Error ValidationError(string message)
{
return new Error(400, message);
}
internal static Error DataNotFoundError(string message)
{
return new Error(404, message);
}
}The example demonstrate how the Reply class creates instances of the Error class.
So that means you can use (the new API) Reply to get the Error.
But still, it doesn't stop anyone from declaring instnace of both Reply and Error anywhere in the project.
Create a public IError interface accessible from anywhere.
public interface IError
{
int StatusCode { get; }
string Message { get; }
}The interface shares the same properties as the Error class, so let's make the Error class implement it:
public class Error : IErrorMake the Error class private and move it within the Reply class:
public class Reply
{
public static Error ValidationError(string message)
{
return new Error(400, message);
}
internal static Error DataNotFoundError(string message)
{
return new Error(404, message);
}
private class Error : IError
{
public int StatusCode { get; set; }
public string Message { get; set; }
public Error(int status, string msg)
{
StatusCode = status;
Message = msg;
}
}
}The compiler complains. The Error class is private, it cannot be returned from the Reply class due to the protection level:
public static Error ValidationError(string message) // ❌
{
return new Error(400, message);
}The IError interface uses the same properties as the base class and it's also public.
Replace Error with IError as the retrn type:
public class Reply
{
public static IError ValidationError(string message)
{
return new Error(400, message);
}
internal static IError DataNotFoundError(string message)
{
return new Error(404, message);
}
//...
}Because you can't create instances of the interface (IError) this won't cause any problems.
Head over to your main file (Program.cs) and create instnaces of Reply and Error:
var err = Reply.ValidationError("Something went wrong");
var err2 = new Error(); // ❌You'll notice that compiler complains about the Error class as expected.
Currently, the Error class uses set; on properties, which means anyone inside the assembly can mutate the error after creation. Fix that with init;:
public int StatusCode { get; init; }
public string Message { get; init; }Seal the Error to stop anyone from creating custom Error classes that derive from the Error class:
private sealed class Error : IError {...}Abstract the implementation details by exposing custom Error methods to the outside.
Each method will produce a different Error, preventing the Reply class to make modifications.
private sealed class Error : IError
{
public int StatusCode { get; init; }
public string Message { get; init; }
private Error() { }
internal static IError BadRequest(string message)
{
return new Error { StatusCode = 400, Message = message };
}
internal static IError NotFound(string message)
{
return new Error { StatusCode = 404, Message = message };
}
}
}That's why I also made the Error constructor private.
The private constructor is causing errors in the Reply class as expected:
public static IError ValidationError(string message)
{
return new Error { } // ❌
}Replace Error instances with the Error factory methods. Also add a private constructor inside Reply so it cannot be instantiated either:
public class Reply
{
private Reply() { }
public static IError ValidationError(string message)
{
return Error.BadRequest(message);
}
internal static IError DataNotFoundError(string message)
{
return Error.NotFound(message);
}
private sealed class Error : IError
{
public int StatusCode { get; init; }
public string Message { get; init; }
private Error() { }
internal static IError BadRequest(string message)
{
return new Error { StatusCode = 400, Message = message };
}
internal static IError NotFound(string message)
{
return new Error { StatusCode = 404, Message = message };
}
}
}You can create new errors only using the methods that the Reply class exposes:
var err = Reply.ValidationError("Something went wrong");- The
Errorclass is completely sealed off. - The
Replyclass is the key. The Reply exposes theErrorclass viaIError. - No one can inherit, mutate or create instances of
Errorbeyond using the factory methods that class exposes.
The alternative solution would be to move the
Error,IErrorandReplyinto a different project (class library).IErrorwould remain public.Errorwould be internal in it's own file:Resultwould be public and returnIError: