-
Notifications
You must be signed in to change notification settings - Fork 3
Cross service validation
We may want to perform validation not only on private service data, such as not empty name when registering user, but also we may have rules that includes public data from other services, eg when placing order, collection of order items ids cannot be empty - we don't know names of these order items which are private data of some other service, but we know their public ids which are part of our validation rule.
Supose we have this service handler on some post request:
[HttpPost]
[InternalAction]
public ActionResult PostAction(ServiceModel model, IEnumerable<Guid> OtherServiceIds)
{
}
Example of this you can see in TestArea/Test/AddCustomer action
Validating on server side relies on transactions set up in main application action, so when we validate our data and return some sort of validation errors, other part of that request included that should be in the same transaction will be rollback. To signal validation error we can throw any kind of exception, so this exception will be rethrow in main application, and if not handled will rollback transaction. However validation exception is different from for example database deadlock exception, so main application should treat it differently. Therefore there is InvalidModelException defined in project, so main app can easily differentiate it from other exceptions and take appropriate actions.
[HttpPost]
[InternalAction]
public ActionResult PostAction(ServiceModel model, IEnumerable<Guid> OtherServiceIds)
{
if(!ModelState.IsValid || OtherServiceIds == null || !OtherServiceIds.Any())
throw new InvalidModelException();
//handle request if model is valid
return RequestHandled();
}
Probably the most popular way of handling invalid model state is to return the same page to a user with added validation information what should be corrected. So in this project we can also do this - when calling HandleRequestInTransaction() method in main app controller, that derive from WebController, we can specify what to do when all services handle request successfully, and what to do when InvalidModelException is thrown and transaction is rollback and all services were asked to provide view models once again for this page.
[HttpPost]
public ActionResult PostAction()
{
HandleRequestInTransaction(() => RedirectToAction("Success"), (viewModels) => View(viewModels), typeof(UIKeysController.PostAction));
//Handles request in transaction and if InvalidModelException is thrown, it will gather view models from services again for given widgets names and execute second func, passing gathered view models
}
First question is - okey, so we once again publish this request to services, but how they are supposed to return view models when they want to only handle request and eventually throw exception once again?
To solve this, we internally pass boolean parameter called isModelValid equals false to each service's internal route values, so it can be accessible in service action as for example additional action parameter. So now we can define our service action as follows:
[HttpPost]
[InternalAction]
public ActionResult PostAction(ServiceModel model, IEnumerable<Guid> OtherServiceIds, bool isModelValid = true)
{
if(!isModelValid)
{
//create view models possible using data from parameters
return ViewModels(created_view_models);
}
if(!ModelState.IsValid || OtherServiceIds == null || !OtherServiceIds.Any())
throw new InvalidModelException();
//handle request if model is valid
return RequestHandled();
}
Because when we return the same page to a user, we want to return it with filled places as it was sent to our system, we need to construct view models filling them with data from request. However this data is already here - each service has data that it wanted as action parameter, so it can fill eg inputs with appropriate values (or it has to get data from database, when in request are only ids, but either way it knows what to include). Additional thing is passing ModelState, because it is where validation errors are stored and when we invoke rendering view model from main app's view our original model state is gone long time ago. To solve this, ModelState is automatically captured when constructing ViewModel and passed as parameter when invoking Html.Action in view model rendering. If we don't specify to actually route to given action, but to render view with action's name, ModelState is automatically joined with current one, so we can have all informations while rendering view models. The same is with ViewData - it is also automatically captured, passed as parameter and union with current ViewData for given view model.
Next question is what about actions that render widgets for get request, but won't take any actions on post request?
We don't want to allow them to fire on each get and post request, because that can easily get us into confiusion what is going on, so we can decorate these actions with appropriate attribute - for example GetAndReplayPostAttribute:
[GetAndReplayPost]
[InternalAction]
public ActionResult PostAction()
{
//construct view model that will be displayed on get request and when any service throws InvalidOperationException
return ViewModel(viewModel);
}
Of course it will only work when post request goes to the same address as get action comes from, so this can be steer with action attributes.
To sum up when one action throws InvalidModelException, all actions are once again called with additional parameter and because main application can only publish request and ask for sth with parameter, it's up to each service action if it checks this parameter and reacts on it as main app wishes. In a result it's possible to do it, but should be used with caution, as all services should remember to check this parameter (or you can decorate them with some attribute, but you still should return full view models).
Examples of this you can see in ProductDrafts/Add and ProductDrafts/Edit actions
As server side validation can be helpful in some scenarios, you probably want to make aggressive client side validation and don't rely on server side validation. With this almost all request that get to the system should have correct data, and if doesn't, it can indicate that someone intentionally messed with it.
Handling client validation on our private data is very easy - we can define our scripts, or use built in. When we want to validate for example OtherServiceIds collection we must find a way to add rule to other service data. In project I'm using built in jquery validation engine.
Adding a rule to other service relies on an observation how data is binded into action's parameter - by default by input name element and data in form is submitted as pairs name=value. As a result when we define that when sending specific request there will be some data publicly available in request data, each with well defined and unique name within page context, there is a conclusion, that on this page there will be html elements with these names, because this is how data is submitted and binded. We also assume in project, that all data publicly available must be displayed with element that will display validation messages. We don't care which service holds that data, or if it's truly html element, or it's just some hidden input that changes its values in reaction to some events. All we need to do is to bind our client validator to element with given name - we know that there will be somewhere place where validation messages will be displayed and whole machanics will be taken by validation template library.
Example of adding rules from one service to another, when we know that there will be publicly available IEnumerable<Guid> OtherServiceIds parameter and we want it to not be empty, while service that contains this data is unaware of this rule and we don't introduce coupling between these services.
<script type="text/javascript">
$(function () {
$("[name='OtherServiceIds']").rules("add", { "OtherServiceIds": true, messages: { "OtherServiceIds": "Validation message here" } });
jQuery.validator.unobtrusive.adapters.addBool("OtherServiceIds");
jQuery.validator.addMethod("OtherServiceIds", function (value, element, params) {
if (value === null || value.length === 0)
return false;
return true;
});
});
</script>
-
Welcome
1.1 Project overview
1.2 Features -
Introduction
2.1 UI composition example
2.2 Service separation
2.3 Service communication
2.4 UI composition goals
2.5 Clues how to start
2.6 Potencial problems -
Identifying widgets
3.1 Naming widgets
3.2 Widgets format
3.3 Amount of widgets
3.4 Widgets and service boundaries
3.5 Widgets and caching
3.6 Grid for widgets -
Delivering view models
4.1 Publishing request
4.2 Internal routing
4.3 Data in view models
4.4 Gathering view models -
Rendering view models
5.1 External routing
5.2 Including route values
5.3 Finiding physical view files
5.4 Template views - Getting data from request
- Sharing resources
- Service api
-
Transactions
9.1 One transaction
9.2 Multiple transactions
9.3 Transaction performance - Dependency injection
- Public api changes
- [Tables] (https://github.com/padzikm/CompositeUI/wiki/Tables)
12.1 Table order
12.2 Server-side rendering
12.3 Client-side rendering
12.4 Nested tables -
Cross-service validation
13.1 Server-side validation
13.2 Client-side validation -
CRUD
14.1 Service private data
14.2 Create
14.3 Update
14.4 Delete
14.5 Preview -
Caching
15.1 View models
15.2 Widgets
15.3 Pages - Optimizing network calls
- Scalling
- Client-side communication
- Deployment
- Starting new project