Posts tonen met het label RIA services. Alle posts tonen
Posts tonen met het label RIA services. Alle posts tonen

donderdag 1 september 2011

Linking an entity between web domain services in RIA services


I’m not the biggest fan of RIA services, but I was pretty surprised the other day when I saw how easily RIA handled the sharing and linking of entities trough different domain services.
So suppose we have the following table structure in our database:


As you can see we have 2 tables: Person and Function.  Then we can generate our web domain services by adding an edmx and then adding a domain service class to the solution.



So I told you nothing new at this moment.  
Now imagine evil reincarnated into a person.  We’ll call this person the customer. One day he comes to you and says: “We’ll need to split the Person table to a centralized database for reuse”.  As soon as you realize he is serious your face will probably be going this way:

But have no fear (RIA is here) because it’s actually pretty easy to accomplish this in RIA services!

At first you’ll need to create a new edmx.  The reason why is because the edmx is basically wrapped around one connection string so for each separate connection you’ll need to create a new edmx.

So the 2 databases will look like this:



And the edmx’s will look like this:



Then build the solution.  Before you can build it you’ll have to remove the person operations from the first domain context.  The reason why you have to do this is because your first domain context inherits from LinqToEntitiesDomainService<FirstDatabaseEntities>. The underlying ObjectContext won’t have any reference any more to the Person entityset that you’ve just deleted.

If you have navigation properties defined in the metadata file then you should delete these also to be able to build.

If you are receiving build errors in your Silverlight projects (which will be very likely), then just ignore these.  It is only essential that you’re able to build the web projects.

Then add a new RIA web domain service class generated from the second database and select the tables you want.  



So now you’ll need to tell RIA services that the Function class in the first entity model is linked to the Person class in the second entity model.  To do this you’ll have to create a partial class of the Function entity.  Now add a property there that will return the Person.  The property must be decorated like so:

public partial class Function
{
    [ExternalReference]
    [Association("Person_Function", "PersonId", "ID")]
    public Person Person { get; set; }
          
}

The External reference attribute will tell the current domain service that the entity this property is returning is from an external domain context.  

The Association attribute will tell the domain service how to link the entities together.  What this will actually do is generate code on the client side that will filter the appropriate entity out.

When you’re finished doing this, build the solution to trigger the code generation client side.  Then you can open the generated file and see that it generated the following code:

public global::DomainService.Web.Person Person
{
    get
    {
        if ((this._person == null))
        {
            this._person = new EntityRef<global::DomainService.Web.Person>(this, "Person", this.FilterPerson);
        }
        return this._person.Entity;
    }
}

private bool FilterPerson(global::DomainService.Web.Person entity)
{
    return (entity.ID == this.PersonId);
}

Notice the generation of the filter depending on the parameters we gave to the Association attribute.

If you’re not interested in coupling the entities server side, then you can always manually couple the entities on the client side with a partial class that has the same code as the generated file.  Either way the client side code will be practically the same.

Now we still need to tell our context object where to find the associated Person entity.  We can do this by calling the AddReference method and supply the Person type and the corresponding context in which the Person entity resides.

_context1 = new DomainService1();
_context2 = new DomainService2();

_context1.AddReference(typeof(Person),_context2);

When this is done you just need to retrieve the data from the server and tadaaaa:

var personQuery = _context2.GetPersonQuery();
_context2.Load(personQuery, PersonLoadCompleted, true);

…

void PersonLoadCompleted(LoadOperation<Person> loadOperation)
{
    var nrOfEntitiesReturned = loadOperation.AllEntities.Count();
    MessageBox.Show(nrOfEntitiesReturned.ToString());

    var functionQuery = _context1.GetFunctionQuery();
    _context1.Load(functionQuery, FunctionLoadCompleted, true);
}

void FunctionLoadCompleted(LoadOperation<Function> loadOperation)
{
    var personOfFunction = loadOperation.Entities.First().Person;
    MessageBox.Show(personOfFunction.Name);
}

Some special notes to pay attention to:
  • Name the relation the same as the entity you are returning
    I find this a very strange implementation but if you name the navigation property different then for some strange reason RIA services won’t generate the relation properly
  • Make sure you don’t have any include attributes above the external reference in you metadata class
    When you do this RIA services will generate an objectset within the service context of that type.  This will generate errors when you’re trying to couple the entity type between the two service contexts.
That’s it, till next time!

vrijdag 25 februari 2011

Adding metadata to RIA service calls

How to add metadata to RIA service calls
How often does it occur that you need to send some metadata which each operation.  This could be some authorization info, client parameters or what have you.  Adding a parameter to each operation is tedious and mind numbing work.  Good thing that there is always a lazy solution. ;)
One thing to keep in mind when programming with RIA services is that it generates a REST based service.  I hear people thinking: “what do I care”.  Well, you should care allot.  It means using SOAP headers to transport extra metadata is out of the question.   There is a solution for this though.  We can still use the HTTP header of the GET/PUT/POST requests to provide the service of more data.  In order to do this, we must realize that RIA services is nothing more than WCF with some added abstraction.  So we can use whatever techniques we are used to in WCF in RIA services. 
Getting down to business
Client side
At first we should inject the users data into the httpHeader when making a request to the service.  We can do this by extending the generated DomainContext class.  This class contains a partial method called OnCreated.  To add behavior to our context object, we will need to extend this method.  We should then access our proxy object to the service.  This is a property in our DomainContext class called DomainClient.  And once we have our proxy, we are on familiar terms and we can start extending it. Below is the code:
    public sealed partial class DomainService
    {
        private void OnCreated()
        {
            dynamic webDomainClient = (WebDomainClient<IDomainServiceContract>)DomainClient;
            ContextFlowEndpointBehavior contextFlowEndpointBehavior = new ContextFlowEndpointBehavior();
            webDomainClient.ChannelFactory.Endpoint.Behaviors.Add(contextFlowEndpointBehavior);

        }
    }
To be able to intercept every call to the service we will need to create a message inspector.  In the message inspector we can modify our request message that is going to be sent to the service.  In this case we will add some parameters to the HttpHeader:
public class ContextFlowEndpointBehavior : IEndpointBehavior
{


    public void IEndpointBehavior_Validate(ServiceEndpoint endpoint)
    {
    }
    void IEndpointBehavior.Validate(ServiceEndpoint endpoint)
    {
        IEndpointBehavior_Validate(endpoint);
    }

    public void AddBindingParameters(ServiceEndpoint endpoint, BindingParameterCollection bindingParameters)
    {
    }


    public void IEndpointBehavior_ApplyDispatchBehavior(ServiceEndpoint endpoint, EndpointDispatcher endpointDispatcher)
    {
    }
    void IEndpointBehavior.ApplyDispatchBehavior(ServiceEndpoint endpoint, EndpointDispatcher endpointDispatcher)
    {
        IEndpointBehavior_ApplyDispatchBehavior(endpoint, endpointDispatcher);
    }


    public void IEndpointBehavior_AddBindingParameters(ServiceEndpoint endpoint, BindingParameterCollection bindingParameters)
    {
    }
    void IEndpointBehavior.AddBindingParameters(ServiceEndpoint endpoint, BindingParameterCollection bindingParameters)
    {
        IEndpointBehavior_AddBindingParameters(endpoint, bindingParameters);
    }

    public void ApplyDispatchBehavior(ServiceEndpoint endpoint, System.ServiceModel.Dispatcher.EndpointDispatcher endpointDispatcher)
    {
    }

    public void IEndpointBehavior_ApplyClientBehavior(ServiceEndpoint endpoint, ClientRuntime clientRuntime)
    {
        clientRuntime.MessageInspectors.Add(new ContextFlowMessageInspector());
    }
    void IEndpointBehavior.ApplyClientBehavior(ServiceEndpoint endpoint, ClientRuntime clientRuntime)
    {
        IEndpointBehavior_ApplyClientBehavior(endpoint, clientRuntime);
    }

    public void Validate(ServiceEndpoint endpoint)
    {
    }
}

public class ContextFlowMessageInspector : IClientMessageInspector
{

    public object IClientMessageInspector_BeforeSendRequest(ref Message request, IClientChannel channel)
    {

        string myHeaderName = "foo";
        string myheaderValue = "bar";

        HttpRequestMessageProperty prop = (HttpRequestMessageProperty)request.Properties(HttpRequestMessageProperty.Name);
        prop.Headers(myHeaderName) = myheaderValue;
        return null;
    }
    object IClientMessageInspector.BeforeSendRequest(ref Message request, IClientChannel channel)
    {
        return IClientMessageInspector_BeforeSendRequest(request, channel);
    }



    public void IClientMessageInspector_AfterReceiveReply(ref Message reply, object correlationState)
    {
    }
    void IClientMessageInspector.AfterReceiveReply(ref Message reply, object correlationState)
    {
        IClientMessageInspector_AfterReceiveReply(reply, correlationState);
    }
}


Server side
Once we’re done injecting into the HttpHeader of the request client side, we have to be able to read the HttpHeader before it processed by the service.  We can do this by adding another message inspector and extracting the information like this:
public class OperationBehaviorAttribute : Attribute, IOperationBehavior
{


    public void Validate(OperationDescription operationDescription)
    {
    }

    public void ApplyDispatchBehavior(OperationDescription operationDescription, DispatchOperation dispatchOperation)
    {
        if (dispatchOperation.Parent.MessageInspectors.OfType<ClientCustomHeadersDispatchMessageInspector>.Count == 0)
        {
            ClientCustomHeadersDispatchMessageInspector inspector = new ClientCustomHeadersDispatchMessageInspector();
            dispatchOperation.Parent.MessageInspectors.Add(inspector);
        }

    }


    public void ApplyClientBehavior(OperationDescription operationDescription, ClientOperation clientOperation)
    {
    }


    public void AddBindingParameters(OperationDescription operationDescription, BindingParameterCollection bindingParameters)
    {
    }
}

   public class ClientCustomHeadersDispatchMessageInspector : IDispatchMessageInspector
    {

        public object AfterReceiveRequest(ref Message request, IClientChannel channel, InstanceContext instanceContext)
        {

            string foo = HttpContext.Current.Request.Headers("foo");
            return null;
        }

        public void BeforeSendReply(ref Message reply, object correlationState)
        {
        }
    }

Then when you have this data, whatever you want to do with it is up to you…  One of the things I like to do is add the parameter to the cache of the httpContext.  This proves to be very usefull when it is always the same data that is being sent to your service.  Eg: I had to write an application that required the user to log in with a smart card.  Legal issues prevented us from saving the data and we didn’t want to bother the user by forcing him to keep his card inserted.  We cached the card’s data and added the id of the data in the http header.
There are a few downsides to this approach though.  One problem is security.  You need to secure the channel in which your messages are being sent when dealing with sensitive data.  Everyone can read a httpHeader with fiddler.
Another problem is that some browsers (mainly firefox and chrome) encapsulate the Silverlight plugin.  This causes some issues when your Silverlight application tries to write inside the httpheader.  However there is a solution for this.  You can specify that the client handles the HTTP, this allows you finer control over the http calls. (http://msdn.microsoft.com/en-us/library/dd920295%28v=vs.95%29.aspx)
Add this in the constructor of your App.xaml:
WebRequest.RegisterPrefix("http://", System.Net.Browser.WebRequestCreator.ClientHttp)

Here are the advantages and disadvantages summed up
Advantages:
  • Transparent
  •  Eliminates redundant code
Disadvantages:
  • Security
  • Chrome / firefox issues
Hope this proves useful.
Till next time ;-)