Considering the cost of throwing exception, an alternative way is something like this:
public interface IValidationDictionary
{
void AddError(string key, string message);
bool IsValid { get; }
}
public class ModelStateWrapper : IValidationDictionary
{
private ModelStateDictionary _modelState;
public ModelStateWrapper(ModelStateDictionary modelState)
{
_modelState = modelState;
}
public void AddError(string key, string errorMessage)
{
_modelState.AddModelError(key, errorMessage);
}
public bool IsValid
{
get { return _modelState.IsValid; }
}
}
public interface IApplicationService
{
void Initialize(IValidationDictionary validationDictionary);
}
public interface IUserService : IApplicationService
{
Task CreateAsync(UserCreateModel model);
}
public class UserService : IUserService
{
private readonly IUnitOfWork _uow;
private IValidationDictionary _validationDictionary;
public UserService(IUnitOfWork uow)
{
_uow = uow ?? throw new ArgumentNullException(nameof(uow));
}
public void Initialize(IValidationDictionary validationDictionary)
{
_validationDictionary = validationDictionary ?? throw new ArgumentNullException(nameof(validationDictionary));
}
public Task CreateAsync(UserCreateModel model)
{
//todo: logic for create new user
if (condition)
//alternative: throw new UserFriendlyException("UserFriendlyMessage");
_validationDictionary.AddError(string.Empty, "UserFriendlyMessage");
if (other condition)
//alternative: throw new UserFriendlyException("UserFriendlyMessage");
_validationDictionary.AddError(string.Empty, "UserFriendlyMessage");
}
}
public class UsersController : Controller
{
private readonly IUserService _service;
public UsersController(IUserService service)
{
_service = service ?? throw new ArgumentNullException(nameof(service));
_service.Initialize(new ModelStateWrapper(ModelState));
}
[HttpPost]
public async Task<IActionResult> Create([FromForm]UserCreateModel model)
{
if (!ModelState.IsValid) return View(model);
await _service.CreateAsync(model);
//todo: Display ModelState's Errors
}
}
considering there is a difference between input validation like validate a DTO and business rule validation https://ayende.com/blog/2278/input-validation-vs-business-rules-validation
Input Validation for me is about validating the user input. Some people call "Name must not be empty" a business rule, I think about it as input validation. Business Rules validation is more complex, because a business rule for me is not "Name must not be empty", it is a definition of a state in the system that requires an action. Here is a definition of a business rule:
An order should be payed within 30 days, this duration can be extended, to a maximum of three times.
Is there any idea for send some error message of business rule validation that appear in between application service method's logic
public class Result
{
public bool Success { get; private set; }
public string Error { get; private set; }
public bool Failure { /* … */ }
protected Result(bool success, string error) { /* … */ }
public static Result Fail(string message) { /* … */ }
public static Result<T> Ok<T>(T value) { /* … */ }
}
public class Result<T> : Result
{
public T Value { get; set; }
protected internal Result(T value, bool success, string error)
: base(success, error)
{
/* … */
}
}
The method is a command and it can’t fail:
public void Save(Customer customer)
The method is a query and it can’t fail:
public Customer GetById(long id)
The method is a command and it can fail:
public Result Save(Customer customer)
The method is a query and it can fail
public Result<Customer> GetById(long id)