Instantly share code, notes, and snippets.
Last active
August 24, 2018 09:05
-
Star
(1)
1
You must be signed in to star a gist -
Fork
(0)
0
You must be signed in to fork a gist
-
Save otto-gebb/87721a21582dd0e55877f3eff506b332 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System; | |
using System.Collections.Generic; | |
using System.Linq; | |
using System.Runtime.Serialization; | |
// A separate NuGet package. | |
namespace Acme.Validation | |
{ | |
public static class Validator | |
{ | |
private static Action<ValidationResult> _throwStrategy = | |
r => throw new ValidationException(r); | |
public static ValidationResult Require() | |
{ | |
return new ValidationResult(_throwStrategy); | |
} | |
/// <summary> | |
/// Call this once at the application start | |
/// to throw a custom exception for validation errors. | |
/// </summary> | |
/// <param name="throwStrategy"> | |
/// A callback that takes a <see cref="ValidationResult" /> | |
/// as a parameter and throws a custom validation excepton. | |
/// </param> | |
public static void UnsafeSetThrowStrategy(Action<ValidationResult> throwStrategy) | |
{ | |
_throwStrategy = throwStrategy; | |
} | |
} | |
public class ValidationResult | |
{ | |
public const string NoKey = "_"; | |
private readonly Action<ValidationResult> _throwStrategy; | |
private Dictionary<string, List<string>> _errors = new Dictionary<string, List<string>>(); | |
internal ValidationResult(Action<ValidationResult> throwStrategy) | |
{ | |
Success = true; | |
_throwStrategy = throwStrategy; | |
} | |
public bool Success { get; private set; } | |
public ValidationResult AddError(string key, string error) | |
{ | |
Success = false; | |
if (_errors.TryGetValue(key, out List<string> messages)) | |
{ | |
messages.Add(error); | |
} | |
else | |
{ | |
_errors.Add(key, new List<string> { error }); | |
} | |
return this; | |
} | |
public ValidationResult AddError(string error) | |
{ | |
return AddError(NoKey, error); | |
} | |
public override string ToString() | |
{ | |
IEnumerable<string> errors = | |
from kv in _errors | |
from v in kv.Value | |
select $"{kv.Key}: {v}"; | |
return string.Join("\n", errors); | |
} | |
public IDictionary<string, string[]> ToDictionary() | |
{ | |
return _errors.ToDictionary(x => x.Key, x => x.Value.ToArray()); | |
} | |
public ValidationResult ThrowIfInvalid() | |
{ | |
if (!Success) | |
{ | |
_throwStrategy(this); | |
} | |
return this; | |
} | |
} | |
[Serializable] | |
public class ValidationException : Exception | |
{ | |
public ValidationException(string message) : base(message) | |
{ | |
} | |
public ValidationException(ValidationResult result) : base("Validation error.") | |
{ | |
Errors = result.ToDictionary(); | |
} | |
protected ValidationException(SerializationInfo info, StreamingContext context) : base(info, context) | |
{ | |
} | |
public IDictionary<string, string[]> Errors { get; } | |
} | |
public static class ValidationExtensions | |
{ | |
public static ValidationResult True( | |
this ValidationResult r, | |
string key, | |
bool condition, | |
string message) | |
{ | |
if (!condition) | |
{ | |
r.AddError(key, message); | |
} | |
return r; | |
} | |
public static ValidationResult True( | |
this ValidationResult r, | |
bool condition, | |
string message) | |
{ | |
if (!condition) | |
{ | |
r.AddError(message); | |
} | |
return r; | |
} | |
public static ValidationResult NonNullOrEmpty( | |
this ValidationResult r, | |
string key, | |
string value) | |
{ | |
if (string.IsNullOrEmpty(value)) | |
{ | |
r.AddError(key, "The value must not be null or empty."); | |
} | |
return r; | |
} | |
} | |
} | |
namespace Acme.Domain | |
{ | |
using Acme.Validation; | |
public class Entity | |
{ | |
public Entity(int age, string name) | |
{ | |
ValidateAgeAndNameParams(age, name).ThrowIfInvalid(); | |
Age = age; | |
Name = name; | |
} | |
private static ValidationResult ValidateAgeAndNameParams(int age, string name) | |
{ | |
return Validator.Require() | |
.NonNullOrEmpty(nameof(name), name) | |
.True(nameof(age), age < 0, "Value must not be negative."); | |
} | |
public void ChangeEverything(int age, string name) | |
{ | |
ValidateAgeAndNameParams(age, name) | |
.ThrowIfInvalid() | |
// Check if entity state allows this operation. | |
.True(Name == "Constantin", "Cannot change anything with this name.") | |
.True(Age > 100 && Name == "Dracula", "Too late to change.") | |
.ThrowIfInvalid(); | |
Age = age; | |
Name = name; | |
} | |
public int Age { get; private set; } | |
public string Name { get; private set; } | |
} | |
} | |
namespace Acme.App | |
{ | |
using System; | |
using Acme.Validation; | |
using Acme.Domain; | |
using Acme.ErrorHandling; | |
public class Startup | |
{ | |
public void ConfigureServices() | |
{ | |
Validator.UnsafeSetThrowStrategy( | |
r => throw new CustomValidationException(r.ToDictionary())); | |
} | |
} | |
class AppService | |
{ | |
void Change(int age, string name) | |
{ | |
Validator.Require() | |
.True(nameof(name), !Db.Exists(name), "This name already exists.") | |
.ThrowIfInvalid(); | |
Entity e = Db.LoadEntity(); | |
e.ChangeEverything(age, name); | |
Db.SaveChanges(); | |
} | |
} | |
} | |
// A separate package depends on ASP.Net core stuff. | |
namespace Acme.ErrorHandling | |
{ | |
public class CustomValidationException : Exception | |
{ | |
public CustomValidationException(IDictionary<string, string[]> errors) | |
{ | |
Errors = errors; | |
} | |
public IDictionary<string, string[]> Errors { get; } | |
} | |
public class ErrorHandlingMiddleware | |
{ | |
private readonly RequestDelegate _next; | |
private static readonly RouteData EmptyRouteData = new RouteData(); | |
private static readonly ActionDescriptor EmptyActionDescriptor = new ActionDescriptor(); | |
public ErrorHandlingMiddleware(RequestDelegate next) | |
{ | |
_next = next; | |
} | |
public async Task InvokeAsync(HttpContext context) | |
{ | |
try | |
{ | |
await _next(context); | |
} | |
catch (CustomValidationException ve) | |
{ | |
await WriteError(context, new { ve.Errors }); | |
} | |
} | |
private Task WriteError(HttpContext context, object error) | |
{ | |
RouteData routeData = context.GetRouteData() ?? EmptyRouteData; | |
var actionContext = new ActionContext(context, routeData, EmptyActionDescriptor); | |
var result = new ObjectResult(error) | |
{ | |
StatusCode = (int)HttpStatusCode.BadRequest | |
}; | |
return result.ExecuteResultAsync(actionContext); | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment