RIA Services: A DomainService IS A WCF Service – Add Service Reference


I made the fairly bold statement at my PDC09 talk that a DomainService IS A WCF Service.  That is, everything you know about a WCF service should be true of a DomainService.  I didn’t have time to get into this in my talk, so I thought I’d hit the highlights here.  And in the process show how to consume a DomainService from a WinForms.    You can also see more examples at: http://code.msdn.microsoft.com/RiaServices 

You need:

 

You can download the completed solution as well.  and be sure to check out the full talk

 

1. Getting to the Service

The first thing we need to do is get at the data underlying service.  In the mainstream Silverlight case this is all handled for you by the implicit link between the Silverlight client and the ASP.NET server.  However, in the vanilla WCF case, you get the full control.  The URL to the service is of the following format:

http://[hostname]/[namespacename]-[classname].svc

so in my case that is:

http://localhost:30335/Services/MyApp-Web-DishViewDomainService.svc

Hitting that URL in the browser gives you the very familiar WCF proxy help screen:

image

And tacking on the ?wsdl gives you the WSDL for this service

http://localhost:30335/Services/MyApp-Web-DishViewDomainService.svc?wsdl

 

image

 

The rest is easy for anyone halfway familiar with WCF… Create a new WinForms project and select Add Service Reference.  Enter the URL (note discover doesn’t work for this sort of service yet)…

image

The you have a service! 

 

 

2. Querying for the Data

Now, we have a service, let’s look at actually getting data out of it.  In this case I already have a WinForms DataGridView on my form.  So getting data into it should be no problem. 

  1. private void Form1_Load(object sender, EventArgs e)
  2. {
  3.     var context = new DishViewDomainServiceClient(“BasicHttpBinding_DishViewDomainService”);
  4.     var plates = context.GetPlates(4);
  5.     this.dataGridView1.DataSource = plates.RootResults;
  6.     foreach (DataGridViewRow row in dataGridView1.Rows)
  7.     {
  8.         PlatesListOriginals.Add(ToPlate(row));
  9.     }
  10.     dataGridView1.CellEndEdit += dataGridView1_CellEndEdit;
  11.     dataGridView1.SelectionChanged += dataGridView1_SelectionChanged;
  12. }
  13.  

In line 3, we create a new instance of the web service client and point it at the right binding.    The service exposes a couple of different bindings as you can see in the app.config file for the WinForms app:

  1. <endpoint address=http://localhost:30335/Services/MyApp-Web-DishViewDomainService.svc/soap
  2.     binding=basicHttpBinding bindingConfiguration=BasicHttpBinding_DishViewDomainService
  3.     contract=ServiceReference1.DishViewDomainService name=BasicHttpBinding_DishViewDomainService />
  4. <endpoint address=http://localhost:30335/Services/MyApp-Web-DishViewDomainService.svc/binary
  5.     binding=customBinding bindingConfiguration=BinaryHttpBinding_DishViewDomainService
  6.     contract=ServiceReference1.DishViewDomainService name=BinaryHttpBinding_DishViewDomainService />
  7.  

In line 4, we call the service to get our list of Plates… in this case we are doing things synchronously.. you could of course do it async if you’d like.

In line  5, we bind the DataGridView to the results of this call.

In lines 6-9, we are saving off the “original” values..  for each item we got.. this will help us when we do updates.

In line 10, we handle the cell edit event, we will come back to look at that later.

in line 11, we sign up for the selection changed event so we can initialize the picture…

  1. void dataGridView1_SelectionChanged(object sender, EventArgs e)
  2. {
  3.      Plate currentPlate = ToPlate(dataGridView1.CurrentRow);
  4.      this.pictureBox1.ImageLocation = “http://hanselman.com/abrams/Images/Plates/” + currentPlate.ImagePath;
  5. }
  6.  

Be patient with this one… sometimes it takes a while load a picture.  it is using hanselman’s server which gets slammed sometimes 😉

image

Now we have our data, we can scroll through it and view the pretty pictures. 

 

3. Updating the Data

But how do we update the data… well, let’s take a look at CellEditEnd event handler…

  1. void dataGridView1_CellEndEdit(object sender, DataGridViewCellEventArgs e)
  2. {
  3.     var context = new DishViewDomainServiceClient(“BasicHttpBinding_DishViewDomainService”);
  4.     Plate currentPlate = ToPlate(dataGridView1.Rows[e.RowIndex]);
  5.     ChangeSetEntry[] changeSet = new[] {
  6.         new ChangeSetEntry{
  7.          OriginalEntity = PlatesListOriginals[e.RowIndex],   
  8.          Entity = currentPlate,
  9.          Operation = DomainOperation.Update
  10.         }
  11.     };
  12.     context.SubmitChanges(changeSet);
  13. }
  14.  

 

In line 3, we are creating a new context.  we could be sharing with the load method, but I thought this would be cleaner to follow.

In line 4, we save off the currently selected plate.

In lines 5-10 we are building up a changeset to send to the server. 

Notice we need to give it the original values we saved off in the load method.    Getting the original values right is the likely the hardest part here.  Keep in mind that assignment in C# (and VB) is by default by reference.  So you can’t just store off a reference, you must actually make a copy of the original values.

Then in line 12, we submit the changes. 

Make a change, tab off it..  This will call the server and post your update.   Re run the app to see that it took.

image

Notice here we are sending one item in the change set.  You could of course build up a change set on the client with many entries and then send them as a batch. 

 

I hope that helps to make it clear how a DomainService IS A WCF Service… You can download the completed solution as well.  and be sure to check out the full talk

Comments (19)

  1. Abhilashkk says:

    Is there a way to use dynamically loaded assemblies for domain services ? If so how to do that?

  2. nanhuacrab says:

    MyApp.Web project References dot find System.Web.DomainServices.EntityFramework, why?

    i use vs2010beta2, silverlight4beta.

  3. Alenas says:

    I have a question about custom properties and I can not find an answer anywhere.

    For example imagine we have a Route class, and Route class has an array of coordinates.

    I serialize that array of coordinates to DB as Binary (byte[]) and Deserialize from DB Binary to Coords[]

    I use LinqToSql for this and this works fine, but I can not find a way to send this Route class over to my Silverlight application. I tried WCF RIA and ASP.Net data services.

    I think my problem is that Coords class is not being considered as an entity (cause it is my custom class). Or maybe I need to add some attribute to Route.Points custom property, so RIA would understand that this property needs to be DataMember (but this property is NOT mapped to DB column).

    It is pretty weird, that this does not work out of the box, cause it even works with AJAX and AJAX is not such a powerful technology as RIA services. I could implement this with WCF webservice, but maybe there is a way to do this with RAI?

  4. DevKaiser says:

    Thanks for the update Mr. Abrams.

    I’m wondering if you could give us a quick lesson on how to implement Inserting and not just updating? I’ve been looking around but I haven’t figured out how to tell to my DataGrid or Details control to add a new record.

  5. Ben says:

    Is there a way to hide the WSDL so that it is not served up to prying eyes?  We have "RequiresAuthentication" decorated on many of our methods and it would be nice if users didn’t even know what the names of the methods were in the first place.  Any way to hide the WSDL or specific methods in the WSDL?

  6. BradA says:

    Good point Ben, we are looking at turning off the metadata by default.. Love to have feedback on that.  

    For now, you’ll have to plug-in a custom DomainServiceHost and override AddDefaultBehaviors:

                   base.AddDefaultBehaviors();

                   this.Description.Find<ServiceMetadataBehavior>().HttpGetEnabled = false;

  7. Ben says:

    Great, that should work for now.  My initial thought is that it would be great to have a Web.Config setting that could be toggled to expose or hide globally all public methods in the RIA / WCF services.  Maybe a setting called "isExposeAsWebServices" with a boolean value?

    That would make it really easy to toggle at least the basic security behaviour.

    Oh, while I’m on the subject of making suggestions, it would be SO COOL if we could have a similar setting for the default security setting for calling RIA / WCF methods with or without authentication.  We want all our methods secured and it is error-prone and time consuming to have to decorate all our methods with the RequresSecurity attribute.  It would be nice (and more secure by default) to have a setting that toggled all methods secure or exposed by default.  

    Then you could keep the great convenience you have with getting up and running with RIA / WCF out of the box but could avoid the accidental proplems of a developer forgetting to put the requires security attribute on a method that returns a collection of secret data and it accidently goes to protection totally open and exposed.  That’s my big worry and we are spending a lot of time code reviewing just to make sure we don’t miss this.

  8. nappisite says:

    Can you provide an example of calling a domain service from ajax?

  9. Is there a way to use the DomainDataSource as well?  Meaning over WCF.

  10. Pragati says:

    Hi,

    I’ve done the same way as per blog. But, I used the service asynchonously like –

    private CustDomainService.CustDomainServiceClient objClient = new prjTestRIAservice.CustDomainService.CustDomainServiceClient("BasicHttpBinding_CustDomainService");        

           public MainPage()

           {

               InitializeComponent();            

               objClient.GetCustomersAsync();

               objClient.GetCustomersCompleted += new EventHandler<prjTestRIAservice.CustDomainService.GetCustomersCompletedEventArgs>(objClient_GetCustomersCompleted);

           }

           void objClient_GetCustomersCompleted(object sender, prjTestRIAservice.CustDomainService.GetCustomersCompletedEventArgs e)

           {

               var lstdata = e.Result;

               grdTest.ItemsSource = lstdata.RootResults;

           }

    *******************************************

    I’m getting error like –

    ‘prjTestRIAservice.CustDomainService.QueryResultOfCustomers’ does not implement inherited abstract member ‘System.Windows.Ria.Services.QueryResult.GetIncludedResults()’

    and

    ‘prjTestRIAservice.CustDomainService.QueryResultOfCustomers’ does not implement inherited abstract member ‘System.Windows.Ria.Services.QueryResult.GetRootResults()’

  11. rjacobs says:

    I’m also running up against the same problem as pragati.  My situation requires mt tohhost the domain services in a completely seperate web site from the silverlight app.  When I try to add the domain service as a service reference to the silverlight app, I get the above mentioned errors.  The domain service work fine in a traditional asp page amd wcf service when added as a service reference.

  12. Pragati says:

    Hi rjacobs, please let me know the solution if you get any. My email id is – pragati.dukale@revalanalytics.com

    I’m still with the same problem for SL appln.

  13. Andréi TV says:

    Hi,

    Is there a way to host a domainService in a console app or a windows service.

    I’ve been through all the web to find something about this topic.

    thank you.

  14. Andréi TV says:

    Hi,

    Is there a way to host a domainService in a console app or a windows service ?

    I’ve been through all the web to find something about this topic.

    thank you.

  15. Paul P says:

    I’m getting that problem that results in this error:

    An ExceptionDetail, likely created by IncludeExceptionDetailInFaults=true, whose value is:

    System.NullReferenceException: Object reference not set to an instance of an object.

      at System.ServiceModel.Description.ServiceMetadataBehavior.MetadataExtensionInitializer.GenerateMetadata()

    According to Google, I’m not the only one.  No one has posted what they’ve done to solve that though.  Any ideas?

  16. Pragati says:

    Hi rjacobs,

    Plz chk the link & the changes done by Konkani in crossdomain file  –

    http://forums.silverlight.net/forums/p/43293/313526.aspx

    My issue has got solved with this, hope the same for you.

    Pragati

  17. Evan M says:

    Great article Brad. One quick question. When decorating the service with RequiresAuthentication, how do you authenticate a client?

    Thanks,

    Evan

  18. How is possible to create references in ChangeSetEntries in order to submit a simple hierarchy like Report (1) -> ReportEntry (n), back to server?

  19. Rodrigo A says:

    @Evan: I didn’t find a nice solution for authenticating a client, but the code below works. Basically, you call Login on the AuthenticationService, store the cookie, and send this cookie whenever you need to.

    AuthenticationServiceClient context = new AuthenticationServiceClient("BasicHttpBinding_AuthenticationService");

    using (OperationContextScope operationContext = new OperationContextScope(context.InnerChannel))

    {

       QueryResult<User> result = context.Login("testuser", "testpassword!", false, null);

       // Store cookie somewhere

       HttpResponseMessageProperty response = (HttpResponseMessageProperty)OperationContext.Current.IncomingMessageProperties[HttpResponseMessageProperty.Name];

       sharedCookie = response.Headers["Set-Cookie"];

    }

    // Later

    OtherServiceClient context = new OtherServiceClient("BasicHttpBinding_OtherService");

    using (OperationContextScope operationContext = new OperationContextScope(context.InnerChannel))

    {

       // Add stored cookie

       HttpRequestMessageProperty request = new HttpRequestMessageProperty();

       request.Headers[HttpRequestHeader.Cookie] = sharedCookie;

       OperationContext.Current.OutgoingMessageProperties[HttpRequestMessageProperty.Name] = request;

       context.SomeOperationThatRequiresAuthentication();

    }