ASP.NET MVC vs Rails Routes

Leave a comment

One thing I have always wanted to compare are the default routes you get with Rails, with the default routes you get with ASP.NET MVC.

Rails

rails-routes

ASP.NET MVC4

GET /Permits Index
GET /Permits/Create Create
POST /Permits/Create Create
GET /Permits/Details/1 Details
GET /Permits/Edit/5 Edit
POST /Permits/Edit/5 Edit
GET /Permits/Delete/5 Delete
POST /Permits/Delete/5

Pretty close. Biggest differences are Rails does

/photos/:id/edit

for editing a resource while ASP.NET MVC does

/Permits/Edit/5

And some of the action names are different (new/create, show/details, update/edit, destroy/delete).

Of course you can always change your routing logic in ASP.NET MVC if you want something closer to what they do it in Rails:

http://stackoverflow.com/questions/9332026/asp-net-mvc-4-route-news-5

Anyways. Just something I have always wanted look at and compare.

A minimalist HTTP WCF configuration

4 Comments

Wanting a simple example of how to spin up and host a WCF endpoint and proxy for integration testing, I came up with the following simple example.

IFooService.cs


using System.ServiceModel;
using System.ServiceModel.Web;

namespace Test.SomeDir
{
    [ServiceContract]
    public interface IFooService
    {
        [WebGet(UriTemplate = "/ping")]
        [OperationContract]
        string Ping();
    }
}

FooService.cs


namespace Test.SomeDir
{
    public class FooService : IFooService
    {
        public string Ping()
        {
            return "Ping";
        }
    }
}

FooServiceTest.cs


using System.ServiceModel;
using System.ServiceModel.Channels;
using System.ServiceModel.Description;
using System.ServiceModel.Web;
using System.Text;
using NUnit.Framework;
using Test.SomeDir;

namespace Test
{
    public class FooServiceTest
    {
        [TestFixture]
        public class When_spec
        {
            private ServiceHost host;
            private IFooService proxy;

            [SetUp]
            public void SetUp()
            {
                host = new WebServiceHost(typeof(FooService));
                host.Open();
                proxy = CreateProxy();
            }

            [TearDown]
            public void TearDown()
            {
                host.Close();
            }

            [Test]
            public void Should_observation()
            {
                proxy.Ping();
            }

            private IFooService CreateProxy()
            {
                EndpointAddress ep = new EndpointAddress("http://localhost:1980/abc");

                var binding = new CustomBinding(
                    new TextMessageEncodingBindingElement(MessageVersion.None, Encoding.UTF8),
                    new HttpTransportBindingElement { ManualAddressing = true });

                var factory = new ChannelFactory<IFooService>(binding, ep);
                factory.Endpoint.Behaviors.Add(new WebHttpBehavior());

                return factory.CreateChannel();
            }
        }
    }
}

The unit test above dynamically spins up a WCF WebServiceHost service which automatically sets some http bindings and behaviour configuration for us.

All we need to do now is add some information to our App.config file like this.

App.config

<?xml version="1.0" encoding="utf-8" ?>
<configuration>

  <system.serviceModel>
    <services>
      <service name="Test.SomeDir.FooService"> <!-- this needs to be the fully qualified name of the service from your test -->
        <endpoint address="abc" binding="webHttpBinding" contract="Test.SomeDir.IFooService" /> <!-- address can be anything -->
        <host>
          <baseAddresses>
            <add baseAddress="http://localhost:1980" />
          </baseAddresses>
        </host>
      </service>
    </services>
  </system.serviceModel>
  
</configuration>

And voila! You are done.

Couple things to be aware of.

1. The name in the configuration has to exactly match fully qualified name used to spin up our host.

            [SetUp]
            public void SetUp()
            {
                host = new WebServiceHost(typeof(FooService));
                host.Open();
                proxy = CreateProxy();
            }

Needs to be expanded to the following in your config.

<service name="Test.SomeDir.FooService">

Get this wrong and you will get the following error:

System.InvalidOperationException : Service ‘Test.SomeDir.FooService’ has zero application (non-infrastructure) endpoints.

2. Note the end point address ‘abc’.

<endpoint address="abc"

This need to map to the path the immediately follows the base host address in your URL;

http://localhost:1980/abc

Hope this helps. I find writing a test like this useful for testing my end points and making sure I’ve got everything setup.

A good book for learning more WCF is Learning WCF: A Hands-on Guide

How to create a simple WCF Rest service

18 Comments

Creating a Restful WCF service in .NET is pretty easy (once you’ve sorted through the myriad of confusing VS2010 options).

Here’s one way simple way to do it.

Step 1: Create a new plain old C# library.

Fire up Visual Studios 2010, create a blank solution, and add a plain old C# library called ‘Services’.

Step 2: Define your Restful service and Dto.

To create a Restful WCF service that handles a GET request, define the following interface and implementation.

IInvoiceService.cs

using System.ServiceModel;
using System.ServiceModel.Web;

namespace Services
{
    [ServiceContract]
    public interface IInvoiceService
    {
        [WebGet(UriTemplate = "/invoices")]
        [OperationContract]
        InvoiceDto[] GetAllInvoices();
    }
}

InvoiceService.cs

namespace Services
{
    public class InvoiceService : IInvoiceService
    {
        public InvoiceDto[] GetAllInvoices()
        {
            return new[] { new InvoiceDto(1, "receiver1"), new InvoiceDto(2, "receiver2") };            
        }
    }
}

To import these attributes you’ll need to add references to System.ServiceModel and System.ServiceModel.Web

Then add your Dto. Dto’s need to be marked up with DataContract and DataMember attributes so they can be serialized by WCF.

InvoiceDto.cs

using System.Runtime.Serialization;

namespace Services
{
    [DataContract(Namespace = "")]
    public class InvoiceDto
    {
        public InvoiceDto(int id, string receiver)
        {
            Id = id;
            Receiver = receiver;
        }

        [DataMember]
        public int Id { get; set; }
        [DataMember]
        public string Receiver { get; set; }
    }
}

Step 3: Host it.

There’s lots of ways to host a WCF restful service. We could create a console application and host it there. We could create a new full on ASP.NET website (WCF enabled) and host it there.

But for simplicity (and because this is how we are trying it in our project) create a ‘WCF Service Application’ and host it there in a project call ‘Host’.

Our new Host project comes with a svc file. This file just sits in the IIS (or Cassini) virtual directory and points to the service it supports. You have one svc file per service.

In our case, we don’t need the default Service1 that came with the application (so go ahead and delete those).

Instead, edit the Service1.svc file and point it too the service we just created in our plain old C# library.

Service.svc

<%@ ServiceHost Service="Services.InvoiceService" Factory="System.ServiceModel.Activation.WebServiceHostFactory"  %>

This WebServiceHostFactory is a new class introduced in WCF 3.5 purely for making WCF services restful. By using it, we don’t need any WCF configuration in our webconfig file.

In fact for the purpose of this demo we can completely gut it and leave it empty as shown below.

Host/web.config

<?xml version="1.0"?>
<configuration>
</configuration>

Step 4: Test it in the browser.

Now if everything is working, we should be able to right click on Service1.svc in our hosted project and go ‘View in browser.

This will fire up Cassini and display a blank page with the message ‘End point not found’. That’s OK.

The real test for us is when we add on the restful GET extension ‘/invoices’ to the URL. When we do that, we should get something like this:

http://localhost:49619/Service1.svc/invoices

Something to be aware of …

Now if you like WCF, and are thinking of building your next application using Restful WCF service, beware of one big gotcha.

You can’t autogenerate client side proxies using WCF.

Rest (HTTP) doesn’t knew anything about WSDL (the SOAP contract used by Visual Studios to create the proxies).

It can’t automatically create proxies for you like regular WCF. This might change in future versions of WCF (which Microsoft has indicated are going to be way more Restful). But for for now, to call this service from a unit test, you have to hand role your own proxy, or just work with basic .NET Http constructs like WebHttpRequest and WebHttpResponse as shown below.

using System.Net;
using NUnit.Framework;

namespace Test
{
    [TestFixture]
    public class When_calling_a_restful_service
    {
        [Test]
        public void Should_get_proper_HTTP_codes_and_headers()
        {

            var url = "http://localhost:49619/Service1.svc/invoices";
            var request = (HttpWebRequest)WebRequest.Create(url);
            var response = (HttpWebResponse)request.GetResponse();

            Assert.AreEqual(200, (int)response.StatusCode);
        }

    }
}

This isn’t the end of the world. On one hand your proxies are always up to sync (because there are none). On the other you just need to track URLs and figure out how to tell your client where your services live.

These are some of the things we’re learning as we spike out some Silverlight Rstful implementation stuff with WCF. Would love to hear about any more insights or gotchas as we definitely haven’t figured it all out.

Restful WCF service with Silverlight4

5 Comments

I’ve spent the last couple weeks wrestling with WCF4 and Silverlight4.

Here is some code that does a GET/POST/PUT/DELETE with a Silverlight4 client.

The Restful WCF Service

    [ServiceContract]
    public interface IInvoiceService
    {
        [WebGet(UriTemplate = "/invoices")]
        [OperationContract]
        List GetAllInvoices();

        [WebGet(UriTemplate = "/invoices/{invoiceId}")]
        [OperationContract]
        Invoice GetInvoice(string invoiceId);

        [WebInvoke(UriTemplate = "/invoices/{invoiceId}", Method = "PUT")]
        [OperationContract]
        Invoice UpdateInvoice(string invoiceId, Invoice newInvoice);

        [WebInvoke(UriTemplate = "/invoices", Method = "POST")]
        [OperationContract]
        Invoice AddNewInvoice(Invoice newInvoice);

        [WebInvoke(UriTemplate = "/invoices/{invoiceId}", Method = "DELETE")]
        [OperationContract]
        void DeleteInvoice(string invoiceId);
    }

Here is the implementation:

  public class InvoiceService : IInvoiceService
    {
        protected static List<Invoice> invoices = new List<Invoice>
        {
            new Invoice(1, "reciever1"),
            new Invoice(2, "reciever2"),
            new Invoice(3, "reciever3"),
        };

        public List<Invoice> GetAllInvoices()
        {
            return invoices;
        }

        public Invoice GetInvoice(string invoiceId)
        {
            Invoice invoice = invoices.Find(x => x.Id == int.Parse(invoiceId));

            if (invoice == null)
            {
                WebOperationContext ctx = WebOperationContext.Current;
                ctx.OutgoingResponse.StatusCode = HttpStatusCode.NotFound;
            }

            return invoice;
        }

        public Invoice UpdateInvoice(string invoiceId, Invoice newInvoice)
        {
            Invoice current = invoices.Find(x => x.Id == int.Parse(invoiceId));

            if (current == null)
            {
                WebOperationContext ctx = WebOperationContext.Current;
                ctx.OutgoingResponse.StatusCode = HttpStatusCode.NotFound;
                return null;
            }

            invoices.Remove(current);
            invoices.Add(newInvoice);

            return newInvoice;
        }

        public Invoice AddNewInvoice(Invoice newInvoice)
        {
            Debug.WriteLine("AddNewInvoice");

            Invoice existingInvoice = invoices.Find(x => x.Id == newInvoice.Id);
            WebOperationContext ctx = WebOperationContext.Current;

            if (existingInvoice != null)
            {
                ctx.OutgoingResponse.StatusCode = HttpStatusCode.Conflict;
                return null;
            }

            invoices.Add(newInvoice);

            ctx.OutgoingResponse.StatusCode = HttpStatusCode.Created;
            ctx.OutgoingResponse.Location = PluginFactory.INSTANCE.InvoiceServiceUri() + newInvoice.Id;

            return new Invoice(newInvoice.Id, newInvoice.Receiver);
        }

        public void DeleteInvoice(string invoiceId)
        {
            var id = Convert.ToInt32(invoiceId);
            invoices = invoices.FindAll(invoice => invoice.Id != id);
        }
    }

The Silverlight Client

You have two options when consuming services on the client. You can use WebClient (which is easy but weak). Or you can use the more rich HttpWebRequest which you need to consume Status Codes. This example users the later. Here is the Silverlight client.

GET

        private void GetClick(object sender, RoutedEventArgs e)
        {
            var request = (HttpWebRequest)WebRequest.Create(url + IdTextBox.Text);
            request.BeginGetResponse(GetCompleted, request);
        }

        private void GetCompleted(IAsyncResult ar)
        {
            // necessary for concurrency
            Dispatcher.BeginInvoke(delegate
            {
                try
                {
                    var request = (HttpWebRequest)ar.AsyncState;
                    var response = (HttpWebResponse)request.EndGetResponse(ar);

                    using (var streamReader = new StreamReader(response.GetResponseStream()))
                    {
                        ParseComplex(response, streamReader);
                    }
                }
                catch (WebException)
                {
                    outputTextBlock.Text = "This id does not exist.";
                }
            });
        }

POST

private void PostComplexClick(object sender, RoutedEventArgs e)
        {
            ClearTextBoxes();

            HttpWebRequest request = (HttpWebRequest)WebRequest.Create(postUrl);
            request.Method = "POST";
            request.ContentType = postContentType;
            request.BeginGetRequestStream(PostComplexRequestReady, request);
        }

        private void PostComplexRequestReady(IAsyncResult asyncResult)
        {
            HttpWebRequest request = asyncResult.AsyncState as HttpWebRequest;
            Stream postStream = request.EndGetRequestStream(asyncResult);

            Dispatcher.BeginInvoke(delegate
            {
                Invoice newInvoice = new Invoice(int.Parse(IdTextBox.Text), RandomReceiver());
                byte[] byteData = Encoding.UTF8.GetBytes(newInvoice.AsXml());
                postStream.Write(byteData, 0, byteData.Length);
                postStream.Flush();
                postStream.Close();

                request.BeginGetResponse(PostComplexResponseReady, request);
            });
        }

        private void PostComplexResponseReady(IAsyncResult asyncResult)
        {
            Dispatcher.BeginInvoke(delegate
            {
                try
                {
                    var request = asyncResult.AsyncState as HttpWebRequest;
                    var response = (HttpWebResponse)request.EndGetResponse(asyncResult);
                    using (var streamReader = new StreamReader(response.GetResponseStream()))
                    {
                        ParseComplex(response, streamReader);
                    }
                }
                catch (WebException e)
                {
                    dynamic exceptionResponse = e.Response;
                    if (exceptionResponse.StatusCode == HttpStatusCode.Conflict)
                        outputTextBlock.Text = "This user already exists.";
                    else
                        outputTextBlock.Text = e.Message;
                }
            });

        }

PUT

  private void PutClick(object sender, RoutedEventArgs e)
        {
            ClearTextBoxes();

            HttpWebRequest request = (HttpWebRequest)WebRequest.Create(url + IdTextBox.Text);
            request.Method = "PUT";
            request.ContentType = postContentType;
            request.BeginGetRequestStream(PostComplexRequestReady, request);

            // Note: PUT is very similar to POST - only difference is url
        }

DELETE

   private void DeleteClick(object sender, RoutedEventArgs e)
        {
            ClearTextBoxes();

            var request = (HttpWebRequest)WebRequest.Create(url + IdTextBox.Text);
            request.Method = "DELETE";
            request.BeginGetResponse(GetCompleted, request);

            // Note: DELETE is very similar to GET
        }

%d bloggers like this: