1

好的,让我们假设我有一个名为实体的 API Foo,它看起来像这样:

.../api/foo (GET, PUT, POST) or (SELECT, INSERT, UPDATE internally)

这适用于很多消费者,尤其是移动设备,因为它非常简洁和轻便。现在让我们假设,如果我们坚持使用 REST,ComeForth存在一个名为的操作,它看起来像这样:

.../api/foo/1/comeforth (POST) or (perform the come forth operation)

好的,我们已经知道了,但是现在假设我需要从消费者那里获得更多关于该操作的信息,因此为了保持简洁,我将构建一个包含FooID 和其他一些信息的新资源,命名ComeForth和 API 现在看起来像这样:

.../api/comeforth (POST) or (perform the come forth operation)

现在,以前的 API.../api/foo/1/comeforth对我来说似乎还可以,但第二个 API 感觉就像我正在尝试将方形钉子放入圆孔中,并且仅仅因为我可以随心所欲地创建资源并不意味着我应该这样做。所以,我的问题是:

  1. 我真的应该为ComeForth操作发布一个基于 SOAP 的服务吗?
  2. 如果我确实使用 SOAP,那不会对消费者产生影响(即,对于喜欢JavaScript或移动设备的消费者来说,它的工作量要大得多?
  3. SOAP 是基于操作而 REST 是基于实体的,这真的是一个硬性规定吗?如果是这样,那么即使是 API.../api/foo/1/comeforth也会违反该规则,不是吗?

无论如何,我只是想确保我使用正确的技术来满足需求。

4

2 回答 2

2

在您描述的情况下,正在操作的资源不是 Foo,而是一个事务(基于您的评论)。您针对特定操作类型 (ComeForth) 的 T (Foo) 类型的实体对长期运行的事务进行建模。

控制器接受事务 POST 请求进行处理并返回事务的表示,其中包括分配给事务的唯一标识符,该标识符可用于跟踪其进度。

客户端执行 GET 操作以使用在接受处理事务时收到的唯一标识符来检索长时间运行的事务的状态。

我选择使用 XML 序列化来进行演示,但您可以将参与事务的实体序列化为字节数组或任何有意义的东西。

示例 Web API:

/事务/{id}

  • POST:创建一个新事务进行处理
  • GET:检索具有指定 id 的事务以验证它是否已完成

Web API 服务模型:

[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;
    }
}

交易控制器:

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);
    }
}

对事务控制器的示例客户端调用:

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)
{

}

因此,您仍然可以使用 REST 和 ASP.NET Web API 来对长时间运行的进程的启动进行建模,您只需要将要执行的操作表示为它自己的单独资源。希望这对您的开发工作有所帮助。

于 2012-09-21T19:31:14.360 回答
1

对我来说,这听起来像是一个非常开放的问题,需要考虑很多因素。

当您的调用匹配 CRUD(创建、检索、更新、删除)时,REST 非常棒,以 Twitter 为例,您可以创建、检索、更新和删除 Twitter 帖子。

现在考虑一个处理交易的支付处理器,您可以创建一个(即传递一个 cc#),它将有条件地做某事,然后可能返回一个交易结果(成功或失败)。您不能真正“更新”交易,而“检索”交易并不是真正检索您发送的数据。您当然不能“删除”交易,您可以取消交易,或执行退款(部分或全部)。对于此示例,REST 没有意义。

这并不是说您不能混合使用 REST 和操作。有些实体符合 REST,但还有其他方法(例如处理付款)不适合 REST。

选择 REST 或 SOAP 的决定应该由您的目标受众决定,WCF 服务(使用 SOAP)在 .NET 中比 REST 更容易实现,如果使用的技术是 ruby​​,反之亦然。

于 2012-09-21T15:15:38.407 回答