While doing some long-term architecture design, the task was given to work through a cloud-based application. Our business requirements have resulted in the need for a multi-tenant application built on WCF services that can share physical instances for multiple tenants. The first thing that jumps out in this framework for me is that these services must not contain state level variables that would directly relate to any single tenant. In this model, for instance, a BLL type service should not contain a static list of 'droplist' items to be consumed by the software unless the listing was tenant/customer agnostic. This breakdown gives us the ability to scale at any point in the SOA, based on load or any other need that may present itself. The gain is that you are not locked into a 1:1 tenant:service instance relationship which allows for more host cost effective scaling. In our model, based on the type of data stored, we have decided to go with a separate catalog for each tenant. This point works for our model and while I know there are other models that work effectively, this is really not the target of the current post.
Building the flow and responsibilities
The first obstacle presented was how to point a tenant at the corresponding catalog without having the SOA be inherently aware of the tenant. For our model, we have the user authenticating against a centralized database and service purposed exclusively for housing tenant information. This enables us to assume that a client will be self-aware and could be provided with their tenant information to some degree prior to engaging the full SOA of the application. The idea being that if we can have the service dynamically recognize the tenant based on the tenant self-identifying at the time of messaging, we can achieve a loosely coupled environment for services to perform with a disregard for the actual tenant entity.
Communicating tenant information
While thinking through a means to provide tenant information to the services, there are a couple of possibilities that come to mind. One could simply provide all of the required information as parameters to each service call. Would it work? Yes. Is it a good idea? No. There are many reasons this is not a good solution. One concern is the added difficulty each client application would encounter trying to consume the SOA. Plus, the SOA would be confusing, difficult to implement and not very readable as a programmer. To me, building your SOA must always consider the consuming applications and the ease with which the services can be consumed as a high priority. Those alone point us directly to the messaging interface for the service. My initial thought was to create custom incoming and outgoing message header properties to allow C# windows clients to attach the properties directly via their client proxy and then dynamically read them out in the service layer. This allows all additional difficulty to be encapsulated within the client proxy/channel factory classes. This also allows non-windows users to dynamically build message headers and attach them to the calls coming into WCF while allowing the service to leverage a single implementation to pull the information out of the message. I needed to create a proof of concept application to illustrate this. I was wrapped up in another project at the time, so ToddM took the project on and delivered an illustrative project. (Thanks, Todd). An overview of the successful prototype is as follows. First, you have to attach the outgoing behavior message header at the channel factory level in C#. This requires the creation of CustomServiceOutgoingBehvaior and CustomServiceIncomingBehvaior classes based on IEndpointBehavior and Attribute, IServiceBehavior respectively. These are then attached to the DataChannel and inspected on either side of the communication. In an effort to make this post shorter and more readable, I have removed code that doesn't directly support the main idea. To be clear, these snippets on their own are not enough.
public class DatabaseInfo { public String Catalog { get; set; } public String Instance{ get; set; } } public static class Tenant { public static String TenantID { get; set; } public static String AuthenticatedUser { get; set; } } public class CustomClientOutgoingBehvaior : IEndpointBehavior { void IEndpointBehavior.ApplyClientBehavior(ServiceEndpoint endpoint, ClientRuntime clientRuntime) { clientRuntime.MessageInspectors.Add(new CustomClientOutgoingMessageInspector()); } } public class CustomServiceOutgoingBehvaior : IEndpointBehavior { DatabaseInfo info = null; public CustomServiceOutgoingBehvaior(DatabaseInfo info) { this.info = info; } void IEndpointBehavior.ApplyClientBehavior(ServiceEndpoint endpoint, ClientRuntime clientRuntime) { clientRuntime.MessageInspectors.Add(new CustomServiceOutgoingMessageInspector(info)); } } public class CustomServiceIncomingBehavior : Attribute, IServiceBehavior { void IServiceBehavior.ApplyDispatchBehavior(ServiceDescription serviceDescription, ServiceHostBase serviceHostBase) { foreach (ChannelDispatcher channelDispatcher in serviceHostBase.ChannelDispatchers) { foreach (var endpointDispatcher in channelDispatcher.Endpoints) { endpointDispatcher.DispatchRuntime. MessageInspectors.Add(new CustomServiceIncomingMessageInspector()); } } } } public class CustomClientOutgoingMessageInspector : IClientMessageInspector { object IClientMessageInspector.BeforeSendRequest(ref Message request, IClientChannel channel) { var messageHeader = new MessageHeader<String>(Tenant.TenantID); var untypedMessageHeader = messageHeader.GetUntypedHeader("TenantID", "Namespace.Shared"); request.Headers.Add(untypedMessageHeader); messageHeader = new MessageHeader<String>(Tenant.AuthenticatedUser); untypedMessageHeader = messageHeader.GetUntypedHeader("AuthenticatedUser", "Namespace.Shared"); request.Headers.Add(untypedMessageHeader); return null; } } public class CustomServiceOutgoingMessageInspector : IClientMessageInspector { public DatabaseInfo DatabaseInfo { get; set; } public CustomServiceOutgoingMessageInspector(DatabaseInfo info) { DatabaseInfo = info; } object IClientMessageInspector.BeforeSendRequest(ref Message request, IClientChannel channel) { if (DatabaseInfo == null) return null; var messageHeader = new MessageHeader<String>(DatabaseInfo.Catalog); var untypedMessageHeader = messageHeader.GetUntypedHeader("Catalog", "Namespace.Shared"); request.Headers.Add(untypedMessageHeader); messageHeader = new MessageHeader<String>(DatabaseInfo.Instance); untypedMessageHeader = messageHeader.GetUntypedHeader("Instance", "Namespace.Shared"); request.Headers.Add(untypedMessageHeader); return null; } }
protected Tuple<DatabaseInfo, Tenent> GetInfoFromHeader() { DatabaseInfo dbInfo = new DatabaseInfo(); Tenant tenantInfo = new Tenant(); Int32 i = OperationContext.Current.IncomingMessageHeaders.FindHeader("Catalog", "Namespace.Shared"); if (i != -1) dbInfo.Catalog = OperationContext.Current.IncomingMessageHeaders.GetHeader<String>(i); i = OperationContext.Current.IncomingMessageHeaders.FindHeader("Instance", "Namespace.Shared"); if (i != -1) dbInfo.Instance = OperationContext.Current.IncomingMessageHeaders.GetHeader<String>(i); i = OperationContext.Current.IncomingMessageHeaders.FindHeader("TenantID", "Namespace.Shared"); if (i != -1) tenantInfo.TenantID = OperationContext.Current.IncomingMessageHeaders.GetHeade<String>(i); i = OperationContext.Current.IncomingMessageHeaders.FindHeader("AuthenticatedUser", "Namespace.Shared"); if (i != -1) tenantInfo.AuthenticatedUser = OperationContext.Current.IncomingMessageHeaders.GetHeader<String>(i); return new Tuple<DatabaseInfo, Tenent>(dbInfo,tenantInfo); }
michael kors outlet
ReplyDeletetrue religion jeans
louis vuitton outlet online
tiffany and co
louis vuitton
louboutin outlet
coach outlet
fitflops sale
oakley sunglasses sale
coach outlet
hzx20161221
t3g71e2b29 w1c95f6p00 l8w71g7y14 e9y24w0k64 n0e87y3q44 l0q31s1f26
ReplyDelete