Reputation: 67948
Okay, let's assume for a minute I've got an API for an entity named Foo
and it looks something like this:
.../api/foo (GET, PUT, POST) or (SELECT, INSERT, UPDATE internally)
And this works well for a lot of consumers, especially mobile devices since it's very concise and lightweight. Now let's assume, if we were to stick with REST, that an operation named ComeForth
exists and it looks something like this:
.../api/foo/1/comeforth (POST) or (perform the come forth operation)
Okay, so we've got that, but now let's assume I need a bit more information from the consumer on that operation and so to keep it concise I'm just going to build a new resource that holds the Foo
ID and some other information, named ComeForth
and the API now looks like this:
.../api/comeforth (POST) or (perform the come forth operation)
Now, the previous API .../api/foo/1/comeforth
seems okay to me, but the second one feels like I'm trying to fit a square peg into a round hole, and just because I can create resources on a whim doesn't mean I should. So, my questions are:
ComeForth
operation?JavaScript
or mobile devices?.../api/foo/1/comeforth
would be breaking that rule wouldn't it?At any rate, I just want to make sure I'm using the right technology for the need.
Upvotes: 1
Views: 431
Reputation: 11211
In the case you are describing, the resource being operated upon is not Foo, but is instead a Transaction (based on your comments). You model a long running transaction against entities of type T (Foo) for a specific action type (ComeForth).
The controller accepts the transaction POST request for processing and returns a representation of the transaction that includes a unique identifier assigned to the transaction that can be used to track its progress.
Clients perform a GET operation to retrieve the status of the long running transaction using the unique identifier they received when the transaction was accepted for processing.
I chose use XML serialization for demo purposes but you could serialize the entity participating in the transaction as a byte array or whatever makes sense.
Example Web API:
/transactions/{id}
Web API Service Model:
[DataContract()]
public class Transaction
{
public Transaction()
{
this.Id = Guid.Empty;
}
/// <summary>
/// Gets or sets the unique identifier for this transaction.
/// </summary>
/// <value>
/// A <see cref="Guid"/> that represents the unique identifier for this transaction.
/// </value>
[DataMember()]
public Guid Id
{
get;
set;
}
/// <summary>
/// Gets or sets a value indicating if this transaction has been completed.
/// </summary>
/// <value>
/// <see langword="true"/> if this transaction has been completed; otherwise, <see langword="false"/>.
/// </value>
[DataMember()]
public bool IsComplete
{
get;
set;
}
/// <summary>
/// Gets or sets the action being performed.
/// </summary>
/// <value>The action being performed.</value>
[DataMember()]
public string Action
{
get;
set;
}
/// <summary>
/// Gets or sets the serialized representation of the entity participating in the transaction.
/// </summary>
/// <value>The serialized representation of the entity participating in the transaction.</value>
[DataMember()]
public string Entity
{
get;
set;
}
/// <summary>
/// Gets or sets the assembly qualified name of the entity participating in the transaction.
/// </summary>
/// <value>
/// The <see cref="Type.AssemblyQualifiedName"/> of the <see cref="Entity"/>.
/// </value>
[DataMember()]
public string EntityType
{
get;
set;
}
/// <summary>
/// Returns the <see cref="Entity"/> as a type of <typeparamref name="T"/>.
/// </summary>
/// <typeparam name="T">The type to project the <see cref="Entity"/> as.</typeparam>
/// <returns>
/// An object of type <typeparamref name="T"/> that represents the <see cref="Entity"/>.
/// </returns>
public T As<T>() where T : class
{
T result = default(T);
var serializer = new XmlSerializer(typeof(T));
using (var reader = XmlReader.Create(new MemoryStream(Encoding.UTF8.GetBytes(this.Entity))))
{
result = serializer.Deserialize(reader) as T;
}
return result;
}
/// <summary>
/// Serializes the specified <paramref name="entity"/>.
/// </summary>
/// <typeparam name="T">The type of entity being serialized.</typeparam>
/// <param name="entity">The entity to serialize.</param>
public static Transaction From<T>(T entity, string action = null) where T : class
{
var transaction = new Transaction();
transaction.EntityType = typeof(T).AssemblyQualifiedName;
transaction.Action = action;
var serializer = new XmlSerializer(typeof(T));
byte[] data = null;
using (var stream = new MemoryStream())
{
serializer.Serialize(stream, entity);
stream.Flush();
data = stream.ToArray();
}
transaction.Entity = Encoding.UTF8.GetString(data);
return transaction;
}
}
[DataContract()]
public class Foo
{
public Foo()
{
}
[DataMember()]
public string PropertyA
{
get;
set;
}
[DataMember()]
public int PropertyB
{
get;
set;
}
[DataMember()]
public Foo PropertyC
{
get;
set;
}
}
TransactionsController:
public class TransactionsController : ApiController
{
public TransactionsController() : base()
{
}
private static ConcurrentDictionary<Guid, Transaction> _transactions = new ConcurrentDictionary<Guid, Transaction>();
/// <summary>
/// Using to initiate the processing of a transaction
/// </summary>
/// <param name="transaction"></param>
/// <returns></returns>
[HttpPost()]
public HttpResponseMessage Post(Transaction transaction)
{
if(transaction == null)
{
return this.Request.CreateErrorResponse(HttpStatusCode.BadRequest, new HttpError("Unable to model bind request."));
}
transaction.Id = Guid.NewGuid();
// Execute asynchronous long running transaction here using the model.
_transactions.TryAdd(transaction.Id, transaction);
// Return response indicating request has been accepted fro processing
return this.Request.CreateResponse<Transaction>(HttpStatusCode.Accepted, transaction);
}
/// <summary>
/// Used to retrieve status of a pending transaction.
/// </summary>
/// <param name="id"></param>
/// <returns></returns>
[HttpGet()]
public HttpResponseMessage Get(Guid id)
{
Transaction transaction = null;
if(!_transactions.TryGetValue(id, out transaction))
{
return this.Request.CreateErrorResponse(HttpStatusCode.NotFound, new HttpError("Transaction does not exist"));
}
return this.Request.CreateResponse<Transaction>(HttpStatusCode.OK, transaction);
}
}
Example client call to transactions controller:
var foo = new Foo()
{
PropertyA = "ABC",
PropertyB = 123,
PropertyC = new Foo()
{
PropertyA = "DEF",
PropertyB = 456
}
};
var transaction = Transaction.From<Foo>(foo, "ComeForth");
Guid pendingTransactionId = Guid.Empty;
// Initiate a transaction
using(var client = new HttpClient())
{
client.BaseAddress = new Uri("http://localhost:12775/api/", UriKind.Absolute);
using (var response = client.PostAsJsonAsync<Transaction>("transactions", transaction).Result)
{
response.EnsureSuccessStatusCode();
pendingTransactionId = response.Content.ReadAsAsync<Transaction>().Result.Id;
}
}
// Retrieve status of transaction
Transaction pendingTransaction = null;
using (var client = new HttpClient())
{
client.BaseAddress = new Uri("http://localhost:12775/api/", UriKind.Absolute);
var requestUri = String.Format(null, "transactions\\{0}", pendingTransactionId.ToString());
using (var response = client.GetAsync(requestUri).Result)
{
response.EnsureSuccessStatusCode();
pendingTransaction = response.Content.ReadAsAsync<Transaction>().Result;
}
}
// Check if transaction has completed
if(pendingTransaction.IsComplete)
{
}
So you can still use REST and the ASP.NET Web API to model the initiation of a long running process, you just need to represent the operation to execute as its own separate resource. Hope this helps you in your development efforts.
Upvotes: 2
Reputation: 25803
To me this sounds like a very open ended question, and a lot of factors need to be taken into consideration.
REST is great when your calls match CRUD (Create, Retrieve, Update, Delete), take the example of Twitter, you can Create, Retrive, Update, and Delete twitter posts.
Now take into consideration a payment processor handling a transaction, you can Create one (ie pass a cc#), it will conditionally do something, and then likely return an transaction result (success or failure). You cannot really "update" a transaction, and "retreiving" one isn't really retreiving the data you sent it. You certainly can't "delete" a transaction, you may void one, or perform a refund (partial or full). For this example, REST does not make sense.
That's not to say that you couldn't have a hybrid of REST and operations. Where some entities conform to REST, but then there's additional methods (such as processing payments) where REST does not fit.
The decision to pick REST or SOAP, that should be determined by your target audience, a WCF service (which uses SOAP) is much easier to implement in .NET than REST, and probably vice versa if the consuming technology is ruby.
Upvotes: 1