Remember the 4 tenets of SOA? One of them is that Boundaries are explicit. When somebody sends data to your service it is just like when you cross an international border into another country. Just a couple of hours drive north of us in Redmond is the border crossing to Canada. When you cross into Canada or back into the United States you have to stop your car and the border agents do their job. Their job is to make sure that you have proper documentation and that you aren’t smuggling something (or someone) bad into the country.
Your service has to have a similar border checkpoint and it is at the trust boundary where data enters your “country”. At the boundary you have to validate the data before it gets down deep into your business logic or database in some invalid form. The question I want to focus on here is one of design. Where should the validation be done?
Download Sample Code – WCF Service Fault and Validation Example
For Workflow Services see endpoint.tv - WF4 Workflow Service Data Validation Design
Most of the time we tend to validate data on entry to the service method. In small applications this approach is manageable but suppose you have a number (call it a Foo) that you use in 15 different services and you always pass it as an integer. As you review the system you find that some services reject any negative Foo value while others reject any Foo value less than 1. Your refactoring instinct tells you that it would be a good idea to centralize the Foo validation logic so you don’t end up with a variety of different validation rules.
Take a look at this code. It works, but could it be better?
public bool SomeOperation(int foo)
{
if (foo < 0)
throw new FaultException("Invalid Foo");
}
return ProcessTheFoo(foo);
The problem with this code is that
Services have to consume and receive data. This data flows across the service boundary and therefore must be untrusted until validated. I’m proposing some new tenets for service orientation. These tenets describe validation rules. Validation rules are an expression that tells you if the data is valid or not.
In your system you have to validate the state of an object. The validation rules for object should be written once and only once. This makes your system more maintainable..
Service Oriented Applications consist of the service boundary and lower layers or business logic. What makes an object valid at one layer should be the same as what makes it valid at another layer. Lower layers of the system may use internal types which hold data in intermediate states that is not valid according to the rules. When this is the case you should think of these types as being fundamentally different than the type that the validation rules apply to.
If validation is called from internal service logic, validation rules should throw exceptions types appropriate for internal use such as ArgumentNullException or ArgumentOutOfRangeException. At the service boundary if you want to propagate the error to the sender these exceptions must be converted to FaultException or FaultException<TDetail>
If the sender and receiver are able and willing to accept the tighter coupling that comes from sharing assemblies, you can share validation rules between sender and receiver. If you share validation rules, the sharing should be limited to only the types exposed at the boundary
public interface IFoo { int Data { get; set; } string Text { get; set; } }
Sound complex? Sure… but it is one thing to build a simple example and quite another to show an architecture style that yields some significant benefits. Of course there are many ways to accomplish these goals – you might have a better way – if so, please share it with me.
Happy Coding!
Ron http://blogs.msdn.com/rjacobs Twitter: @ronljacobs
Hi Ron,
There are some great ideas here.
RE: Validation Rule violations should result in internal exceptions which may cause external faults
Absolutely agree with this. You should look at leveraging an IErrorHandler for exception translation. The HandleError allows you to log exceptions on a background thread while the ProvideFault allows for exception translation. ProvideFault also plays an important role by allowing you to provide exception shielding (catch all, then return generic fault) to avoid security sensitive information being returned. I have one at jabiru.codeplex.com/.../66884 for example.
I hit an edge case where I needed to use the IErrorHandler (in the previous link) to essentially manage business validation. It was where WF content correlation failed to find an existing workflow service instance for the data provided to the service. The IErrorHandler was the only place that the thrown exception could be translated into a business fault. See www.neovolve.com/.../Managing-content-correlation-failures.aspx for the details.
The IErrorHandler can be hooked up via configuration (www.neovolve.com/.../implementing-ierrorhandler.aspx) or as an attribute on the service implementation class (www.neovolve.com/.../Strict-IErrorHandler-usage.aspx). I have provided support for both these scenarios in my toolkit (neovolve.codeplex.com/.../19004).
RE: Validation Rules may be shared between sender and receiver
I like this idea, but I prefer to keep data contracts lean and for them not to have any logic. They are essentially just DTO classes. A way to support this idea would be to provide a Validate extension method that is bundled up in the service contract assembly. If you are sharing the service contract binary with clients then they can use the same validation as the service. This could be a blessing and a curse. The validation logic will be versioned along with the service contract assembly, but the client may be using an old service contract version with a newer version of the service. Validation logic may get out of sync in this case.
On a side note, a good idea is to describe business faults using combinations of code/description. Ideally, the business fault will be able to describe a collection of these code/description failures to avoid unnecessary round-trips for multiple input field failures on the client.
Failure codes are great because they are human and culture independent (prefer enums for this purpose). A client application can use the code to determine an automated action (retry) or to highlight a failure in a UI with particular input fields. Codes will also allow a client to provide its own culture aware description as required. I have an example of one of these business faults at jabiru.codeplex.com/.../66884.
Not using codes forces the client to parse fault description strings which is very risky. The only other alternative is to create a fault contract/failure type which will be very messy on the service contracts and difficult for clients to support.
I have put together an example of how to provide support for all of this with WF custom activities at www.neovolve.com/.../Custom-Workflow-activity-for-business-failure-evaluatione28093Wrap-up.aspx.
Thanks Rory - great ideas all around...
A common problem that many developers have to face is the distribution of the validation logic between the client and the service. Questions like, "Shall I run this logic on the client, service or both ?", or "How do I do to share this validation logic between the client and the service ?". The common instinct tell you that everything should be validated on the service, but then you start running in some usability issues on the client. For instance, If the service required five fields, and the client only sent two, how do I show that to the user in a friendly way ?. I can not implement the common validation method that shows the "red" boxes around the required fields without moving some validation logic to the client for that scenario. ASP.NET MVC for example introduced the model, and you can decorate it with data validation annonations so the same model (and validation logic) is shared between the view and the controller. However, the same thing does not apply quite well to services. You can not expect that clients and services will share the same data contracts with the validation logic as you might have other legacy or service stacks consuming your services.
Pablo.
@Pablo - if you provide a Validator class from the service side that can validate the individual data items as well as the entity as a whole and share the assembly that might work.
For example, a CustomerValidator might have CustomerValidator.ValidateName, ValidateAddress etc. Then it can have a ValidateCustomer(customer) which could validate as a whole.
Hi Ron and Rory,
I really like the article and Rory's additional remarks as you sum best practices regarding validation and exception handling which I use and propagate since years (DTO, Exception Shielding, Fault Contracts, Security Facades). I have the impression, that the impact of missing validation and insufficient exception handling on the maintainability of software systems is greatly underestimated.
Just some short remark to Ron's idea mentioned above: We have used this approach in the past to share simple validation logic between client and the server side.
But how does this approach fit to the tenet "Services share schema and contract, not class" and the pattern "Document Processor" (see msdn.microsoft.com/.../ms954638.aspx)? Following the "Document Processor" pattern, I would suggest to integrate the simple validation rules into the XSchema and share these rules as part of the contract.
Do you agree?
Best Regards,
Martin
Document Processor - yes I'm quite familiar with it... I came up with it.
As I said in the video, some people are not comfortable with the tight coupling that comes from sharing validation logic between client and server. That is why I said services "may" share validation rules.
I say this as a compromise because over the years I've become less dogmatic about SOA and more practical. I find that many people are creating services that are consumed by other parts of their application. And in such cases they are often already sharing code between sender and receiver so if you are comfortable with it... share the validation as well. Just recognize that sharing the validation logic does create a tighter coupling.