There is the Richardson Model of REST says REST APIs can be ranked like so:
1- Plain old XML. You serve up data in a data exchange format at HTTP endpoint. Ignores as much as possible about how HTTP was intended to work.
2- Resources have their own URL
3- Resources can be manipulated with GET, PUT, POST, DELETE
4- Resources return hypermedia, which contains links to other valid actions & acts as the state of the application.
I’ll add these:
5- Metadata. The API supports HEAD, OPTIONS, and some sort of meta data document like HAL
6- Server Side Asynch – There is support for HTTP 202 & an endpoint for checking the status of queued requests. This is not to be confused with client side asych. Server side asynch allows the server to close an HTTP connection and keep working on the request. Client side asych has to do with not blocking the browser’s UI while waiting for a response from the server.
7- Streaming – There is support for the ranges header for returning a resource in chunks of bytes. It is more like resumeable download, and not related to the chunk size when you write bytes to the Response. With ranges, the HTTP request comes to a complete end after each chunk is sent.
#5 is universally useful, but there isn’t AFAIK, a real standard.
#6 & #7 are really only needed when a request is long running or the message size is so large it needs to support resuming.
Clients should have a similar support Level system.
2 – Client Side Caches
3 – Supports chunking & maybe automatically chunks after checking the HEAD
4 – Supports streaming with byte ranges
Ideally, this boilerplate would always be available to consume on your custom classes. In practices, it is uncommon to see any of the following implemented. Why is that?
So lets simplify reality and imagine that code is of only a few types:
(I’m suffixing all of these with -like to remind you that I’m talking about things that look like these, not necessarily the class or data structure with the same name in the .NET or C# spec or BCL)
- Primative-like. Single value, appear in many domains, often formatted different in different countries. Sometimes simple, like Int32, sometimes crazy complicated like DateTime, sometimes missing, like “Money”.
- Struct-like. Really small values, appear in some domains, like Latitude/Longitude pairs.
- Datarow-like. Many properties,need to be persisted, probably stored in a relational or document database, often exchanged across machine, OS and organizational boundaries.
- Service-like. These are classes that may or may not have state depending on the programming pradigm. They are classes with methods that do something, where as all the above classes, mainly just hold data and incidentally do something. It might be domain-anemic, like create, read, update and delete or it might be domain-driven, like issue insurance policy, or cancel vacation.
- Collection-like. These used to be implemented as custom types, but with Generics, there isn’t as much motivation to implement these on a *per type* basis.
- Tree or Graph-like. These are reference values that contain complex values and collection-like values and those turn also might contain complex values and collections.
All classes may need the following features
- Equality- By value, by reference and by domain specific. The out of the box behavior is usually good enough and for reference types shouldn’t be modified. Typically if you do need to modify equality, it is to get by-value or by-primary-key behavior, which is best done in a separate class.
- Ranking- A type of sorting. This may not be as valuable as it seems now that linq exists and supports .Sort(x=>…)
- String representation- A way to represent this for usually human consumption, with features overlapping Serialization
- Serialization- A usually two way means of converting the class into string, JSON, XML for persistence or communicating off machine
- Factories, Cloning and Conversion- This covers creation (often made moot by IOC containers, which sometimes have requirements about what a class looks like), cloning, which is a mapping problem (made moot by things like automapper), and finally conversion, which is mapping similar types, such as Int to Decimal, or more like “Legacy Customer” to “Customer”
- Validation- Asking an object what is wrong, usually for human consumption
- Persistence- A way to save an object to a datastore. At the moment, this is nhibernate, EF, and maybe others.
- Metadata- For example, the .NET Type class, an XSD for the serialized format, and so on.
- Versioning- Many of the above features are affected by version, such as seralization and type conversion, where one may want to convert between types that are the same but separated by time where properties may have been added or removed. Round trip conversion without data loss is a type of a versioning feature.
- Ad hoc. Just make stuff up. Software should be hard, unpredictable and unmanageable. The real problem is too many people don’t want to read the non-existent documentation of your one-off API.
- Framework driven. Make best efforts to find existing patterns and copy them. This improves your ability to communicate how your API works to your future self and maybe to other developers.
- Interface driven. A bit old fashioned, but rampant. For example these:
//Forms of ToString(), may need additional for WebAPI
IFormattable, IFormatProvider, ICustomFormatter,
//Sort of an alternate constructor/factory pattern
IDisposable, //End of life clean up
IComparable, IComparable, //Sorting
//Competing ways to validate an object
- Attribute driven. This is now popular for seralization APIS, e.g. DataContract/DataMember and for certain Validations.
- Base Class- A universal class that all other classes derive from and implement some of the above concerns. In practice, this isn’t very practical, as most of these code snippets vary with the number of properties you have.
- In-Class- For example, just implement IFormat* on your class. If you need to support 2 or more ways of implementing an interface, you might be better off implementing several classes that depending on the class you are creating features for.
- Universal Utility Class- You can only pick one base class in C#. If you waste it on a utility class, you might preclude creating a more useful design heirarchy. A universal utility class has the same problem as a universal base class.
- Code generation. Generate source code using reflection.
- Reflection. Provide certain features by reflecting over the fields and properties.
All of these patterns entail gotchas. Someday when I’m smarter and have lots of free time, I’ll write about it.