Upload
others
View
8
Download
1
Embed Size (px)
Citation preview
DanielElliott
GettingStartedwithASP.NETMultitenantApplications
BookRixGmbH&Co.KG81669Munich
TableOfContents
TableofContents
TheStorybehindtheSuccinctlySeriesofBooks
AbouttheAuthor
Chapter1Introduction
Chapter2Setup
Chapter3Concepts
Chapter4ASP.NETWebForms
Chapter5ASP.NETMVC
Chapter6WebServices
Chapter7Routing
Chapter8OWIN
Chapter9ApplicationServices
Chapter10Security
Chapter11DataAccess
Chapter12PuttingItAllTogether
References
DetailedTableofContents
Chapter1Introduction
Chapter1IntroductionAboutthisbookThisisabookaboutbuildingmultitenantapplicationsusingASP.NET.Wewill
bediscussingwhatmultitenancymeansforawebapplicationingeneral,andthenwe’llbelookingathowASP.NETframeworkslikeWebFormsandMVCcanhelpusimplementmultitenantsolutions.
Youcanexpecttofindhereworkingsolutionsfortheproblemsoutlined,someof which certainly can be enhanced and evolved.Whenever appropriate, I willleavereferencessothatinterestedreaderscanimproveuponwhatI’vebuiltandfurthertheirunderstandingofthesematters.
SomeknowledgeofASP.NET(WebFormsorMVC)isrequired,butyoudonothave tobeanexpert.Hopefully,bothbeginnersandexpertswill findsomethingtheycanusehere.
Throughout thebook,wewill be referring to two imaginary tenants/domains,ABC.comandXYZ.net,whichwillbothbehostedinourserver.
Whatismultitenancyandwhyshouldyoucare?
Whatismultitenancyandwhyshouldyoucare?Multitenancyisaconceptthathasgainedsomenotorietyinthelastyearsas
an alternative to multiple deployments. The concept is simple: a single webapplication can respond to clients in a way that canmake them think they aretalkingtodifferentapplications.Thedifferent“faces”ofthisapplicationarecalledtenants,becauseconceptuallytheyliveinthesamespace—thewebapplication—andcanbeaddressed individually.Tenantscanbeaddedor removed justbytheflipofaconfigurationswitch.
Why is this useful?Well, for one, it renders it unnecessary to havemultipleserversonline,withallthemaintenanceandcoststheyrequire.Ingeneral,amongotherthings,onehastoconsiderthefollowing:
Resourceisolation:Adedicatedserverisbettershieldedfromfailuresinotherinfrastructurecomponentsthanasharedsolution.Cost:Costsarehigherwhenweneedtohaveonephysicalserverperservice,application,orcustomerthanwithasharedserver.Qualityofservice:Youcanachieveahigherlevelofqualityandcustomizationifyouhaveseparateservers,becauseyouarenotconcernedwithhowitmightaffectdifferentservices,applications,orcustomers.Complexityofcustomization:Wemusttakeextracareincustomizingmultitenantsolutionsbecausewetypicallydon’twantthingstoapplytoalltenantsinthesameway.Operationandmanagement:“Hard”management,likebackups,monitoring,physicalallocation,powersupplies,cooling,etc.,aremucheasierwhenweonlyhaveoneserver.
Figure 1: Decision criteria for dedicated versus shared (multitenant)deployments
In general, a multitenant framework should help us handle the followingproblems:
Branding:Differentbrandingsfordifferenttenants;bybrandingImeanthingslikelogoimages,stylesheets,andlayoutingeneral.Authentication:Differenttenantsshouldhavedifferentuserregistrations;auserregisteredinaspecifictenantshouldnotbeabletologintoanothertenant.Workflow:Itmakessensethatdifferenttenantshandlethesamebasicthingsina(slightly)differentway;amultitenantframeworkissupposedtomakethisastransparentaspossiblewithregardtocode.DataModel:Differenttenantsmayhave(slightly)differentdataneeds;again,weshouldaccommodatethisneedwhenpossible,consideringthatwearetalkingaboutthesamesharedapplication.
Throughoutthebookwewillgivesomeattentiontotheseproblems.
Multitenancyrequirements
MultitenancyrequirementsAshigh-levelrequirements,ourapplicationframeworkshouldbeableto:
LoadanumberoftenantsandtheirinformationautomaticallyIdentify,fromeachHTTPrequest,thetenantthatitwasmeantto,oruseafallbackvalueApplyabrandingthemetoeachtenant’srequestedpagesAllowuserstologinonlytothetenanttheyareassociatedwithOffercommonapplicationservicestoalltenantsthatdon’tcollide;forexample,somethingstoredinapplicationcacheforonetenantwillnotbeoverwrittenbyanothertenant
Applicationservices
ApplicationservicesApplication services offer common functionality that .NET/ASP.NET provides
outofthebox,butnotinamultitenantway.ThesewillbediscussedinChapter9,“ApplicationServices.”
Frameworks
FrameworksASP.NET isanumbrellaunderwhichdifferent frameworks coexist:while the
venerableWebFormswasthefirstpublic implementationthatMicrosoftputout,MVChasgainedalotofmomentumsinceitsintroduction,andisnowconsideredbysomeasamoremodernapproachtowebdevelopment.SharePointisalsoastrongcontender,andevenmoresowithSharePointOnline,partoftheOffice365product.NowitseemsthatOWIN(andKatana)isthenewcoolkidontheblock,andsomeofthemostrecentframeworksthatRedmondintroducedrecentlyevendependonit(takeSignalR,forexample).
On thedataside,EntityFrameworkCodeFirst isnow thestandard fordataaccessusing theMicrosoft .NET framework, but other equally solid—and somewouldargue,evenstronger—alternativesexist,suchasNHibernate,andwewillcoverthesetoo.
Authentication has also evolved from the venerable, but outdated, providermechanism introduced in ASP.NET 2. A number of Microsoft (or otherwise)sponsored frameworks and standards (thinkOAuth) have been introduced, andthe developer community certainlywelcomes them.Herewewill talk about theIdentityframework,Microsoft’smostrecentauthenticationframework,designedtoreplacetheoldASP.NET2providersandtheSimpleMembershipAPIintroducedin MVC 4 (not covered since it has become somewhat obsolete with theintroductionofIdentity).
Finally,thegluethatwillholdthesethingstogether,plusthecodethatwewillbe producing, is Unity, Microsoft’s Inversion of Control (IoC), DependendyInjection(DI),andAspect-OrientedProgramming(AOP)framework.Thisismostlybecause I have experience with Unity, but, by all means, you are free to usewhateverIoCframeworkyou like.Unity isonlyusedforregisteringcomponents;its retrieval is done through the Common Service Locator, a standard API thatmost IoC frameworks comply to. Most of the code will work with any otherframeworkthatyouchoose,provideditoffers(oryourolloutyourown)integrationwiththeCommonServiceLocator.Exceptthecomponentregistrationcode,whichcanbetracedtoasinglebootstrapmethod,alltherestofthecodereliesontheCommonServiceLocator,notaparticularIoCframework.
Chapter2Setup
Chapter2SetupIntroductionThis chapter will walk you through setting up your development and test
environment.
VisualStudio
VisualStudioInorder tobeable to follow theconceptsandcodedescribedhere,youwill
needaworking installationofVisualStudio;anyversionfrom2012upwardswilldo,includingtheshinynewCommunityEdition.ImyselfhaveusedVisualStudio2013,but youare free touseadifferent version; basically, you’ll needaVisualStudioversionthatcanhandle.NET4.xandNuGetpackages.
NuGet
NuGetNuGetisVisualStudio’snativepackagingsystem.Mostsoftwarecompanies,
includingMicrosoft,aswellasindependentdevelopersandcommunities(suchastheNHibernatecommunity)providetheirlibrariesandframeworksthroughNuGet.It’saneasywaytofetchsoftwarepackagesandtheirdependencies.
For the purpose of the examples in this book, you will need the followingpackages:
OWINSelfHost
OWINWebAPI
EntityFrameworkCodeFirst
NHibernate
Unity
UnityBootstrapperforASP.NETMVC
EnterpriseLibraryLoggingApplicationBlock
ASP.NETIdentity(EntityFramework)
ASP.NETIdentity(NHibernate)
OWINIdentity
AspNet.Identity.EntityFramework.Multitenant
WatiN
Addressmapping
AddressmappingIfyouwanttofollowtheexamplesdescribedinthisbook,youwillneedtoset
up different host names for you development machine. This is so that we canmakeuseofthehostheadertenantidentificationpattern,describedinChapter3.If you have administrative access to the DNS server that serves your localconnection (workorhome), youcanadddifferentaliases toastatic IPaddressandthenusethatIPaddressinsteadofusingaDHCP-assignedone.
Another option is to usea free service suchas xip.io,which translates hostnames in domain xip.io into IP addresses; for example, 127.0.0.1.xip.io willautomaticallytranslateto127.0.0.1,192.168.1.1.xip.iobecomes192.168.1.1,andsoon.Thisissowecanusethestrategyforidentifyingtenantsbasedonthehostheadermentionedearlier,butitonlyworkswithIPaddresses,nothostnames.
Another option is to configure static IP-name mappings through the%WINDIR%System32DriversEtchostsfile.Thisfilecontainslinesintheformat:
CodeSample1
IPaddresshostnamealias1…aliasn
Forexample,youcanaddthefollowingentry:
CodeSample2
127.0.0.1localhostabc.comxyz.net
This will tell the IP stack on Windows that the hosts named abc.com andxyz.netwillbothtranslatetotheloopbackaddress127.0.0.1,withouttheneedforaDNSlookup.
IIS
IISSettingupamultitenant site in Internet InformationServices (IIS) is easy:
justopentheIISManagerappletandselectSitesAddwebsite.Thenaddahostnamethatyouwishtohandle:
Figure2:SettingupamultitenantsiteinIIS
Nowaddahostnameforall theadditional tenants,select thesite,andclickBindings.
Figure3:Addingadditionalhostnamestoasite
Figure4:Allthesite’shostnames
Alsoimportantistheapplicationpool:youcanhavedifferentapplicationpoolsserving each of the different tenants (the site bindings). This provides betterisolation, in the sense that if something goes wrong for one of the tenants—perhapsanunhandledexceptionthatrendersthesiteunusable—itwillnotaffectthe others. Also, you can have the application pools running under differentidentities,whichisniceifyouwishtoaccessdifferentdatabases,orhavedifferentaccesslevels.JustcreateasmanyapplicationpoolsasyoulikeinIISManager:
Figure5:Creatinganapplicationpoolforatenant
IISExpress
IISExpressIncaseyouwillbeusingIISExpress,besidesDNSorhostsfile,youwillalso
need to configure the website’s bindings to allow for other host names.Unfortunately,youwillhavetodoitbyhand,sinceIISExpressdoesnotofferanygraphical tool for that purpose; the site’s configuration file is located at:%HOMEPATH%DocumentsIISExpressConfigApplicationHost.config. Open itandlocatethesiteentrythatyouwishtochange;itshouldlooklikethefollowing(minusname,id,physicalPath,andbindingInformation):
CodeSample3
sites
sitename=“Multitenant”id=“1”
applicationpath=”/”applicationPool=“Clr4IntegratedAppPool”virtualDirectorypath=”/”
physicalPath=“C:InetPubMultitenant”/
/application
bindings
bindingprotocol=“http”bindingInformation=”*:80:localhost”/
/bindings
/site
/sites
Youwillprobablyseesomesitesalreadyconfigured.Inthatcase,youhavetofindtheoneyou’re interestedin,maybethroughthephysicalPathattribute,andaddabindingelementwithabindingInformationattributepointingtotheproperhostnameandport.
CodeSample4
bindings
bindingprotocol=“http”bindingInformation=”*:80:localhost”/
bindingprotocol=“http”bindingInformation=”*:80:abc.com”/
bindingprotocol=“http”bindingInformation=”*:80:xyz.net”/
/bindings
This instructs IIS Express to also accept requests for hosts abc.com andxyz.net,togetherwithlocalhost.
Chapter3Concepts
Chapter3ConceptsWhodoyouwanttotalkto?As we’ve seen, the main premise of multitenancy is that we can respond
differently to different tenants. You might have asked yourself: how does theASP.NETknowwhichtenant’scontents itshouldbeserving?That is,whoistheclienttryingtoreach?HowcanASP.NETfindout?
Theremaybeseveralanswerstothisquestion:
Fromtherequestedhostname;forexample,ifthebrowseristryingtoreachabc.comorxyz.net,asstatedintherequestURLFromaquerystringparameter,likehttp://host.com?tenant=abc.comFromtheoriginating(client)hostIPaddressFromtheoriginatingclient’sdomainname
Probably themost typical (anduseful) use case is the first one; youhaveasinglewebserver(orserverfarm)whichhasseveralDNSrecords(AorCNAME)assigned to it, and itwill responddifferently dependingonhow itwas reached,say,abc.comandxyz.net.Beingdevelopers,let’strytodefineagenericcontractthatcangiveananswertothisquestion.Considerthefollowingmethodsignature:
CodeSample5
StringGetCurrentTenant(RequestContextcontext)
We can interpret it as: “given some request, give me the name of thecorrespondingtenant.”
Note: If you want to know thedifferencebetweenDNSAandCNAMErecords,youcanfindagoodexplanationhere.
TheRequestContextclassispartoftheSystem.Web.Routingnamespace,anditencapsulatesallofarequest’spropertiesandcontext.ItsHttpContextpropertyallowseasyaccess to thecommonHTTP requestURL, server variables,querystringparameters,cookiesandheadersandRouteData to routing information, ifavailable.Ichosethisclassfortherequestinsteadofwhatmightbemoreobviousones—HttpContextandHttpContextBase—preciselybecause it easesaccess toroutedata,incaseweneedit.
Note:HttpContextBasewasintroducedin .NET3.5 toalloweasymocking,because it isnotsealed,andoutsideof theASP.NETpipeline.ItbasicallymimicsthepropertiesandbehaviorofHttpContext,whichissealed.
Asforthereturntype,itwillbethenameofatenant.Moreonthatlater.
Inthe.NETworld,wehavetwomainoptionsifwearetoreusesuchamethodsignature:
DefineamethoddelegateDefineaninterfaceoranabstractbaseclass
Inourcase,we’llgowithaninterface,enterITenantIdentifierStrategy:CodeSample6
publicinterfaceITenantIdentifierStrategy
{
StringGetCurrentTenant(RequestContextcontext);
}
Hostheaderstrategy
Figure6:Hostheaderstrategy
AsimpleimplementationofITenantIdentifierStrategyforusingtherequestedhostname(HostHTTPheader)asthetenantidentifierisprobablytheoneyou’llusemoreofteninpublic-facingsites.AsingleserverwithasingleIPaddressandmultiplehostanddomainnameswilldifferentiatetenantsbytherequestedhost,intheHTTPrequest:
Table1:MappingofHTTPHostheaderstotenants
HTTPRequestHeaders
Tenant
GET/Default.aspxHTTP/1.1
Host:abc.com
abc.com
GET/Default.aspxHTTP/1.1
Host:xyz.net
xyz.net
Note:FormoreinformationontheHostHTTPheader,seeRFC2616,HTTPHeaderFieldDefinitions.
A class for using the host header as the tenant name might look like thefollowing:
CodeSample7
publicclassHostHeaderTenantIdentifierStrategy:ITenantIdentifierStrategy
{
publicStringGetCurrentTenant(RequestContextcontext)
{
returncontext.HttpContext.Request.Url.Host.ToLower();
}
}
Tip: This code ismeant for demo purposes only; it does not have
any kind of validation and just returns the requested host name in lowercase.Real-lifecodeshouldbeslightlymorecomplex.
Querystringstrategy
Figure7:Querystringstrategy
We might want to use a query string parameter to differentiate betweentenants,likehost.com?tenant=abc.com:
Table2:MappingofHTTPquerystringstotenants
HTTPRequestURL
Tenant
http://host.com?Tenant=abc.com
abc.com
http://host.com?Tenant=xyz.net
xyz.net
This strategymakes it really easy to test with different tenants; no need toconfigureanything—justpassaquerystringparameterintheURL.
We can use a class like the following, which picks theTenant query stringparameterandassignsitasthetenantname:
publicclassQueryStringTenantIdentifierStrategy:ITenantIdentifierStrategy
{
publicStringGetCurrentTenant(RequestContextcontext)
{
return (context.HttpContext.Request.QueryString[“Tenant”] ?? String.Empty).ToLower();
}
}
Tip:Evenifthistechniquemayseeminterestingatfirst,itreallyisn’tappropriateforreal-lifescenarios.JustconsiderthatforallyourinternallinksandpostbacksyouwillhavetomakesuretoaddtheTenantquerystringparameter;ifforwhateverreasonitislost,youaredroppedoutofthedesiredtenant.
Note: A variation of this pattern coulduse theHTTPvariablePATH_INFO insteadofQUERY_STRING,but thiswouldhaveimpacts,namely,withMVC.
SourceIPaddressstrategy
Figure8:SourceIPstrategy
Nowsupposewewanttodeterminethetenant’snamefromtheIPaddressof
the originating request. Say a user located at a network whose address is200.200.200.0/24willbeassignedtenantabc.comandanotheroneusingastaticIPof160.160.160.160willgetxyz.net.Itgetsslightlytrickier,becauseweneedtomanuallyregistertheseassignments,andweneedtodosomemathtofindoutifarequestmatchesalistofregisterednetworkaddresses.
Wehavetwopossibilitiesforassociatinganetworkaddresstoatenantname:
WeuseasingleIPaddressWeuseanIPnetworkandasubnetmask.
Say,forexample:
Table3:MappingofsourceIPaddressestotenants
SourceNetwork/IPTenant200.200.200.0/24(200.200.200.1-200.200.200.254)
abc.com
160.160.160.1
xyz.net
The .NET Base Class Library does not offer an out-of-the-box API for IPnetworkaddressoperations,sowehavetobuildourown.Considerthefollowinghelpermethods:
CodeSample8
publicstaticclassSubnetMask
{
publicstaticIPAddressCreateByHostBitLength(Int32hostPartLength)
{
varbinaryMask=newByte[4];
varnetPartLength=32-hostPartLength;
if(netPartLength2)
{
thrownewArgumentException
(“NumberofhostsistoolargeforIPv4.”);
}
for(vari=0;i4;i++)
{
if(i*8+8=netPartLength)
{
binaryMask[i]=(Byte)255;
}
elseif(i*8netPartLength)
{
binaryMask[i]=(Byte)0;
}
else
{
varoneLength=netPartLength-i*8;
varbinaryDigit=String.Empty
.PadLeft(oneLength,‘1’).PadRight(8,‘0’);
binaryMask[i]=Convert.ToByte(binaryDigit,2);
}
}
returnnewIPAddress(binaryMask);
}
publicstaticIPAddressCreateByNetBitLength(Int32netPartLength)
{
varhostPartLength=32-netPartLength;
returnCreateByHostBitLength(hostPartLength);
}
publicstaticIPAddressCreateByHostNumber(Int32numberOfHosts)
{
varmaxNumber=numberOfHosts+1;
varb=Convert.ToString(maxNumber,2);
returnCreateByHostBitLength(b.Length);
}
}
publicstaticclassIPAddressExtensions
{
publicstaticIPAddress[]ParseIPAddressAndSubnetMask(StringipAddress)
{
varipParts=ipAddress.Split(‘/’);
varparts=newIPAddress[]{ParseIPAddress(ipParts[0]),
ParseSubnetMask(ipParts[1])};
returnparts;
}
publicstaticIPAddressParseIPAddress(StringipAddress)
{
returnIPAddress.Parse(ipAddress.Split(‘/’).First());
}
publicstaticIPAddressParseSubnetMask(StringipAddress)
{
varsubnetMask=ipAddress.Split(‘/’).Last();
varsubnetMaskNumber=0;
if(!Int32.TryParse(subnetMask,outsubnetMaskNumber))
{
returnIPAddress.Parse(subnetMask);
}
else
{
returnSubnetMask.CreateByNetBitLength(subnetMaskNumber);
}
}
publicstaticIPAddressGetBroadcastAddress(thisIPAddressaddress,
IPAddresssubnetMask)
{
varipAdressBytes=address.GetAddressBytes();
varsubnetMaskBytes=subnetMask.GetAddressBytes();
if(ipAdressBytes.Length!=subnetMaskBytes.Length)
{
thrownewArgumentException
(“LengthsofIPaddressandsubnetmaskdonotmatch.”);
}
varbroadcastAddress=newByte[ipAdressBytes.Length];
for(vari=0;ibroadcastAddress.Length;i++)
{
broadcastAddress[i]=(Byte)(ipAdressBytes[i]|
(subnetMaskBytes[i]^255));
}
returnnewIPAddress(broadcastAddress);
}
publicstaticIPAddressGetNetworkAddress(thisIPAddressaddress,
IPAddresssubnetMask)
{
varipAdressBytes=address.GetAddressBytes();
varsubnetMaskBytes=subnetMask.GetAddressBytes();
if(ipAdressBytes.Length!=subnetMaskBytes.Length)
{
thrownewArgumentException
(“LengthsofIPaddressandsubnetmaskdonotmatch.”);
}
varbroadcastAddress=newByte[ipAdressBytes.Length];
for(vari=0;ibroadcastAddress.Length;i++)
{
broadcastAddress[i]=(Byte)(ipAdressBytes[i]
(subnetMaskBytes[i]));
}
returnnewIPAddress(broadcastAddress);
}
publicstaticBooleanIsInSameSubnet(thisIPAddressaddress2,
IPAddressaddress,Int32hostPartLength)
{
returnIsInSameSubnet(address2,address,SubnetMask
.CreateByHostBitLength(hostPartLength));
}
publicstaticBooleanIsInSameSubnet(thisIPAddressaddress2,
IPAddressaddress,IPAddresssubnetMask)
{
varnetwork1=address.GetNetworkAddress(subnetMask);
varnetwork2=address2.GetNetworkAddress(subnetMask);
returnnetwork1.Equals(network2);
}
}
Note: This code is based in codepubliclyavailablehere(andslightlymodified).
NowwecanwriteanimplementationofITenantIdentifierStrategythatallowsustomapIPaddressestotenantnames:
CodeSample9
publicclassSourceIPTenantIdentifierStrategy:ITenantIdentifierStrategy
{
privatereadonlyDictionaryTupleIPAddress,IPAddress,Stringnetworks=newDictionaryTupleIPAddress,IPAddress,String();
publicIPTenantIdentifierStrategyAdd(IPAddressipAddress,
Int32netmaskBits,Stringname)
{
returnthis.Add(ipAddress,SubnetMask.CreateByNetBitLength(
netmaskBits),name);
}
publicIPTenantIdentifierStrategyAdd(IPAddressipAddress,
IPAddressnetmaskAddress,Stringname)
{
this.networks
[newTupleIPAddress,IPAddress(ipAddress,netmaskAddress)]=name.ToLower();
returnthis;
}
publicIPTenantIdentifierStrategyAdd(IPAddressipAddress,Stringname)
{
returnthis.Add(ipAddress,null,name);
}
publicStringGetCurrentTenant(RequestContextcontext)
{
varip=IPAddress.Parse(context.HttpContext.Request
.UserHostAddress);
foreach(varentryinthis.networks)
{
if(entry.Key.Item2==null)
{
if(ip.Equals(entry.Key.Item1))
{
returnentry.Value.ToLower();
}
}
else
{
if(ip.IsInSameSubnet(entry.Key.Item1,
entry.Key.Item2))
{
returnentry.Value;
}
}
}
returnnull;
}
}
Notice that this class is not thread safe; if you wish to make it so, onepossibilitywouldbetouseaConcurrentDictionaryTKey,TValueinsteadofaplainDictionaryTKey,TValue.
Before we can use IPTenantIdentifierStrategy, we need to register somemappings:
CodeSample10
vars=newSourceIPTenantIdentifierStrategy();
s.Add(IPAddress.Parse(“200.200.200.0”,24),“abc.com”);
s.Add(IPAddress.Parse(“160.160.160.1”),“xyz.net”);
Inthisexampleweseethattenantxyz.netismappedtoasingleIPaddress,160.160.160.1,while tenantabc.com ismapped toanetworkof200.200.200.0with a 24-bit network mask, meaning all hosts ranging from 200.200.200.1 to200.200.200.254willbeincluded.
Sourcedomainstrategy
Figure9:Sourcedomainstrategy
Wemaynotknowabout IPaddresses,but instead,domainnames;henceastrategy based on client domain names comes in order. We want to get thetenant’snamefromthedomainnamepartoftherequestinghost,somethinglike:
Table4:Mappingsourcedomainnamestotenants
SourceDomain
Tenant
*.some.domain
abc.com
*.xyz.net
xyz.net
Subdomains should also be included. Here’s a possible implementation ofsuchastrategy:
CodeSample11
publicclassSourceDomainTenantIdentifierStrategy:ITenantIdentifierStrategy
{
privatereadonlyDictionaryString,Stringdomains=new
DictionaryString,String(StringComparer.OrdinalIgnoreCase);
publicDomainTenantIdentifierStrategyAdd(Stringdomain,Stringname)
{
this.domains[domain]=name;
returnthis;
}
publicDomainTenantIdentifierStrategyAdd(Stringdomain)
{
returnthis.Add(domain,domain);
}
publicStringGetCurrentTenant(RequestContextcontext)
{
varhostName=context.HttpContext.Request.UserHostName;
vardomainName=String.Join(“.”,hostName.Split(‘.’)
.Skip(1)).ToLower();
returnthis.domains.Where(domain=domain.Key==domainName)
.Select(domain=domain.Value).FirstOrDefault();
}
}
ForDomainTenantIdentifierStrategy,ofcourse,wealsoneedtoentersomemappings:
CodeSample12
vars=newSourceDomainTenantIdentifierStrategy();
s.Add(“some.domain”,“abc.com”);
s.Add(“xyz.net”);
Thefirstentrymapsallclientrequestscomingfromthesome.domaindomain(orasubdomainof) toa tenantnamedabc.com.Theseconddoesan identicaloperation for thexyz.net domain, wherewe skip the tenant’s name because itshouldbeidenticaltothedomainname.
Asyoucansee,twoofthepreviousstrategies’implementations—hostheaderandquerystringparameter—arebasicallystatelessandimmutable,soinsteadofcreatingnewinstanceseverytime,wecaninsteadhavestaticinstancesofeachinawell-knownlocation.Let’screateastructureforthatpurpose:
CodeSample13
publicstaticclassTenantsConfiguration
{
publicstaticclassIdentifiers
{
publicstaticreadonlyHostHeaderTenantIdentifierStrategy
HostHeader=newHostHeaderTenantIdentifierStrategy();
publicstaticreadonlyQueryStringTenantIdentifierStrategy
QueryString=newQueryStringTenantIdentifierStrategy();
}
Tip:NoticetheDefaultTenantproperty.Thisiswhatwillbeusedifthetenantidentificationstrategyisunabletomaparequesttoatenant.
Two other strategies—by source IP address and by domain name—requireconfiguration,soweshouldn’thavethemasconstantinstances,but,toallowfor
easy finding, let’s add some static factories to theTenantsConfiguration classintroducedjustnow:
CodeSample14
publicstaticclassTenantsConfiguration
{
//restgoeshere
publicstaticclassIdentifiers
{
//restgoeshere
publicstaticSourceDomainTenantIdentifierStrategySourceDomain()
{
returnnewSourceDomainTenantIdentifierStrategy();
}
publicstaticSourceIPTenantIdentifierStrategySourceIP()
{
returnnewSourceIPTenantIdentifierStrategy();
}
}
}
Note: In Chapter 9, “Putting It AllTogether,”wewillseehowallthesestrategiesarerelated.
GettingthecurrenttenantWe’ve looked at some strategies for obtaining the tenant’s name from the
request;now,wehavetopickone,andstoreitsomewherewhereitcanbeeasilyfound.
StaticpropertyOneoption is tostore thisasastaticproperty in theTenantsConfiguration
class:
CodeSample15
publicstaticclassTenantsConfiguration
{
publicstaticITenantIdentifierStrategyTenantIdentifier{get;set;}
//restgoeshere
}
Now,we can choosewhatever strategywewant, probably pickingone fromthe TenantsConfiguration’s static members. Also, we need to set theDefaultTenant property, so that if the current strategy is unable to identify thetenanttouse,wehaveafallback:
CodeSample16
TenantsConfiguration.TenantIdentifier =TenantsConfiguration.Identifiers.HostHeader;
TenantsConfiguration.DefaultTenant=“abc.com”;
UnityandtheCommonServiceLocatorAnother option is to use an Inversion of Control (IoC) framework to store a
singletonreference to the tenant identifier instanceofourchoice.Betteryet,wecanusetheCommonServiceLocatortoabstractawaytheIoCthatwe’reusing.Thisway,wearenottiedtoaparticularimplementation,andcanevenchangetheonetousewithoutimpactingthecode(except,ofcourse,somebootstrapcode).There are several IoC containers for the .NET framework. Next, we’ll see anexample using awell-known IoC framework,MicrosoftUnity, part ofMicrosoft’sEnterpriseLibrary.
CodeSample17
//setupUnity
varunity=newUnityContainer();
//registerinstances
unity.RegisterInstanceITenantIdentifierStrategy(TenantsConfiguration.Identifiers.HostHeader);
unity.RegisterInstanceString(“DefaultTenant”,“abc.com”);
//setupCommonServiceLocatorwiththeUnityinstance
ServiceLocator.SetLocatorProvider(()=newUnityServiceLocator(unity));
//resolvethetenantidentifierstrategyandthedefaulttenant
varidentifier=ServiceLocator.Current.GetInstanceITenantIdentifierStrategy();
vardefaultTenant=ServiceLocator.Current.GetInstanceString(“DefaultTenant”);
This approach has the advantage that we can register new strategiesdynamically throughcode(or throughtheWeb.config file)withoutchanginganycode, shouldwe need to do so.Wewill be using this approach throughout thebook.TheServiceLocatorsectionofChapter9,“ApplicationServices,”alsotalksaboutUnityandhowtouseittoreturncomponentsspecifictothecurrenttenant.
Note:Unity is just one among tens ofIoCcontainers,offeringsimilarfunctionalityaswellasmorespecificones.Notallhave adapters for the Common Service Locator, but it is generally easy toimplement one. For a more in-depth discussion of IoC, the Common ServiceLocator,andUnity,pleaseseeMicrosoftUnitySuccinctly.
What’sinaname?Nowthatwehaveanabstractionthatcangiveusatenant’sname,let’sthink
foramomentwhatelseatenantneeds.
Wemightneedathemeaswell,somethingthataggregatesthings likecolorschemesandfonts.Differenttenantsmightwantdifferentlooks.
IntheASP.NETworld,thetwomajorframeworks,MVCandWebForms,offerthe concept of master pages or layout pages (inMVC terminology), which areusedtodefinethegloballayoutofanHTMLpage.Withthis,itiseasytoenforceaconsistent layout throughout a site, so, let’s consider amaster page (or layoutpage,inMVCterms)property.
Itwon’thurthavingacollectionofkey/valuepairsthatarespecifictoatenant,regardlessofhavingamoreadvancedconfigurationfunctionality,sowenowhave
ageneral-purposepropertybag.Windows offers a general purpose, operating-system-supported mechanism
formonitoringapplications:performancecounters.Performancecountersallowusto monitor in real time certain aspects of our application, and even reactautomaticallytoconditions.Weshallexposeacollectionofcounterinstancestobecreatedautomaticallyinassociationwithatenant.
Itmightbeusefultoofferageneral-purposeextensionmechanism;beforethedaysIoCbecamepopular,.NETalreadyincludedagenericinterfaceforresolvingacomponentfromatypeintheformoftheIServiceProviderinterface.Let’salsoconsideraserviceresolutionmechanismusingthisinterface.
Finally, itmakessense tohavea tenant initialize itselfwhen it is registeringwithourframework.Thisisnotdata,butbehavior.
So, based on what we’ve talked about, our tenant definition interface,ITenantConfiguration,willlooklikethis:
CodeSample18
publicinterfaceITenantConfiguration
{
StringName{get;}
StringTheme{get;}
StringMasterPage{get;}
IServiceProviderServiceProvider{get;}
IDictionaryString,ObjectProperties{get;}
IEnumerableStringCounters{get;}
voidInitialize();
}
Forexample,atenantcalledxyz.netmighthavethefollowingconfiguration:CodeSample19
publicsealedclassXyzNetTenantConfiguration:ITenantConfiguration
{
publicXyzNetTenantConfiguration()
{
//dosomethingproductive
this.Properties=newDictionaryString,Object();
this.Counters=newListString{“C”,“D”,“E”};
}
publicvoidInitialize()
{
//dosomethingproductive
}
publicStringName{get{return“xyz.net”;}}
publicStringMasterPage{get{returnthis.Name;}}
publicStringTheme{get{returnthis.Name;}}
publicIServiceProviderServiceProvider{get;privateset;}
publicIDictionaryString,ObjectProperties{get;privateset;}
publicIEnumerableStringCounters{get;privateset;}
}
In this example, we are returning a MasterPage and a Theme that areidenticaltothetenant’sName,andarenotreturninganythingreallyusefulintheCounters,PropertiesandServiceProviderproperties,butinreallife,youwouldprobablydosomethingelse.Anycounternamesyoureturnwillbeautomaticallycreatedasnumericperformancecounterinstances.
Note:Therearelotsofotheroptionsforprovidingthiskindofinformation,likewithattributes,forexample.
Findingtenants
FindingtenantsTenantlocationstrategiesWhat’sahousewithoutsomeonetoinhabitit?
Now, we have to figure out a strategy for finding tenants. Two basicapproachescometomind:
ManuallyconfiguringindividualtenantsexplicitlyAutomaticallyfindingandregisteringtenants
Again, let’s abstract this functionality in a nice interface,ITenantLocationStrategy:
CodeSample20
publicinterfaceITenantLocationStrategy
{
IDictionaryString,TypeGetTenants();
}
This interfacereturnsacollectionofnamesandtypes,wherethetypes areinstancesofsomenon-abstractclassthatimplementsITenantConfigurationandthenamesareuniquetenantidentifiers.
We’ll also keep a static property in TenantsConfiguration, DefaultTenant,wherewecanstorethenameofthedefaulttenant,asafallbackifonecannotbeidentifiedautomatically:
CodeSample21
publicstaticclassTenantsConfiguration
{
publicstaticStringDefaultTenant{get;set;}
//restgoeshere
}
Herearethestrategiesforlocatingandidentifyingtenants:
CodeSample22
publicstaticclassTenantsConfiguration
{
publicstaticStringDefaultTenant{get;set;}
publicstaticITenantIdentifierStrategyTenantIdentifier{get;set;}
publicstaticITenantLocationStrategyTenantLocation{get;set;}
//restgoeshere
}
Next,we’llseesomewaystoregistertenants.
ManuallyRegisteringTenantsPerhaps themostobviousway to register tenants is tohaveaconfiguration
section in theWeb.config file where we can list all the types that implementtenant configurations.Wewould like to have a simple structure, something likethis:
CodeSample23
tenantsdefault=“abc.com”
addname=“abc.com”type=“MyNamespace.AbcComTenantConfiguration,MyAssembly”/
addname=“xyz.net”type=“MyNamespace.XyzNetTenantConfiguration,MyAssembly”/
/tenants
Inside the tenants section, we have a number of elements containing thefollowingattributes:
Name:theuniquetenantidentifier;inthiscase,itwillbethesameasthedomainnamethatwewishtoanswerto(seeHostHeaderStrategy)Type:thefullyqualifiedtypenameofaclassthatimplementsITenantConfigurationDefault:thedefaulttenant,ifnotenantcanbeidentifiedfromthecurrenttenantidentifierstrategy
Thecorrespondingconfigurationclassesin.NETare:
CodeSample24
[Serializable]
publicclassTenantsSection:ConfigurationSection
{publicstaticreadonlyTenantsSectionSection=ConfigurationManager
.GetSection(“tenants”)asTenantsSection;
[ConfigurationProperty(””,IsDefaultCollection=true,IsRequired=true)]
publicTenantsElementCollectionTenants
{
get
{
returnbase[String.Empty]asTenantsElementCollection;
}
}
}
[Serializable]
publicclassTenantsElementCollection:ConfigurationElementCollection,
IEnumerableTenantElement
{
protectedoverrideStringElementName{get{returnString.Empty;}}
protectedoverrideConfigurationElementCreateNewElement()
{
returnnewTenantElement();
}
protectedoverrideObjectGetElementKey(ConfigurationElementelement)
{
varelm=elementasTenantElement;
returnString.Concat(elm.Name,”:”,elm.Type);
}
IEnumeratorTenantElementIEnumerableTenantElement.GetEnumerator()
{
foreach(varelminthis.OfTypeTenantElement())
{
yieldreturnelm;
}
}
}
[Serializable]
publicclassTenantElement:ConfigurationElement
{
[ConfigurationProperty(“name”,IsKey=true,IsRequired=true)]
[StringValidator(MinLength=2)]
publicStringName
{
get{returnthis[“name”]asString;}
set{this[“name”]=value;}
}
[ConfigurationProperty(“type”,IsKey=true,IsRequired=true)]
[TypeConverter(typeof(TypeTypeConverter))]
publicTypeType
{
get{returnthis[“type”]asType;}
set{this[“type”]=value;}
}
[ConfigurationProperty(“default”,IsKey=false,IsRequired=false,
DefaultValue=false)]
publicBooleanDefault
{
get{return(Boolean)(this[“default”]??false);}
set{this[“default”]=value;}
}
}
So, our tenant location strategy (ITenantLocationStrategy) implementationmightresemblethefollowing:
CodeSample25
publicsealedclassXmlTenantLocationStrategy:ITenantLocationStrategy
{
public static readonly ITenantLocationStrategy Instance = new
XmlTenantLocationStrategy();
publicIDictionaryString,TypeGetTenants()
{
vartenants=TenantsSection.Section.Tenants
.ToDictionary(x=x.Name,x=x.Type);
foreach(vartenantinTenantsSection.Section.Tenants
.OfTypeTenantElement())
{
if(tenant.Default)
{
if(String.IsNullOrWhiteSpace
(TenantsConfiguration.DefaultTenant))
{
TenantsConfiguration.DefaultTenant=
tenant.Name;
}
}
}
returntenants;
}
}
Youmighthavenoticedthe Instance field;becausethere isn’tmuchpoint inhavingseveral instancesofthisclass,sinceallpointtothesame.configfile,wecanhaveasinglestaticinstanceofit,andalwaysuseitwhennecessary.Now,allwehavetodoissetthisstrategyastheonetouseinTenantsConfiguration. IfwearetousetheCommonServiceLocatorstrategy(seeUnityandtheCommonService Locator), we need to add the following line in our Unity registrationmethodandTenantsConfigurationstaticclass:
CodeSample26
publicstaticclassTenantsConfiguration
{
//restgoeshere
publicstaticclassLocations
{
publicstaticXmlTenantLocationStrategyXml()
{
returnXmlTenantLocationStrategy.Instance;
}
//restgoeshere
}
}
container.RegisterInstanceITenantLocationStrategy(TenantsConfiguration.Locations.Xml());
BykeepingafactoryofallstrategiesintheTenantsConfigurationclass,itiseasier for developers to find the strategy they need, from the set of the onesprovidedoutofthebox.
FindingtenantsautomaticallyAsforfindingtenantsautomatically,thereareseveralalternatives,butIopted
forusingMicrosoftExtensibilityFramework(MEF).This isa framework includedwith.NETthatoffersmechanismsforautomaticallylocatingplug-insfrom,amongothers,thefilesystem.Itsconceptsinclude:
ContractType:Theabstractbaseclassorinterfacethatdescribesthefunctionalitytoimport(theplug-inAPI)ContractName:Afree-formnamethatisusedtodifferentiatebetweenmultiplepartsofthesamecontracttypePart:AconcreteclassthatexportsaspecificContractTypeandNameandcanbefoundbyaCatalogCatalog:AnMEFclassthatimplementsastrategyforfindingparts
Figure10:MEFarchitecture
Figure11:MEFparts
Wewon’tgointoMEFindepth;we’lljustseehowwecanuseittofindtenantconfigurationclassesautomaticallyinourcode.Wejustneedtodecorateplug-inclasses—inourcase,tenantconfigurations—withacoupleofattributes,chooseastrategytouse,andMEFwillfindandoptionallyinstantiatethemforus.Let’sseeanexampleofusingMEFattributeswithatenantconfigurationclass:
CodeSample27
[ExportMetadata(“Default”,true)]
[PartCreationPolicy(CreationPolicy.Shared)]
[Export(“xyz.net”,typeof(ITenantConfiguration))]
publicsealedclassXyzNetTenantConfiguration:ITenantConfiguration
{
publicXyzNetTenantConfiguration()
{
//dosomethingproductive
this.Properties=newDictionaryString,Object();
this.Counters=newListString{“C”,“D”,“E”};
}
publicvoidInitialize()
{
//dosomethingproductive
}
publicStringName{get{return“xyz.net”;}}
publicStringMasterPage{get{returnthis.Name;}}
publicStringTheme{get{returnthis.Name;}}
publicIServiceProviderServiceProvider{get;privateset;}
publicIDictionaryString,ObjectProperties{get;privateset;}
publicIEnumerableStringCounters{get;privateset;}
}
Thisclass isbasically identical toonepresentedincodesample18,withtheaddition of the ExportMetadataAttribute, PartCreationPolicyAttribute, andExportAttributeattributes.ThesearepartoftheMEFframeworkandtheirpurposeis:
ExportMetadataAttribute:Allowsaddingcustomattributestoaregistration;youcanaddasmanyoftheseasyouwant.Theseattributesdon’thaveanyspecialmeaningtoMEF,andwillonlybemeaningfultoclassesimportingplug-ins.PartCreationPolicyAttribute:Howtheplug-inisgoingtobecreatedbyMEF;currently,twooptionsexist:Shared(forsingletons)orNonShared(fortransientinstances,thedefault)ExportAttribute:Marksatypeforexportingasapart.Thisistheonlyrequiredattribute.
Now, our implementation of a tenant location strategy usingMEF goes likethis:
CodeSample28
publicsealedclassMefTenantLocationStrategy:ITenantLocationStrategy
{
privatereadonlyComposablePartCatalogcatalog;
publicMefTenantLocationStrategy(paramsString[]paths)
{
this.catalog=newAggregateCatalog(paths.Select(
path=newDirectoryCatalog(path)));
}
publicMefTenantLocationStrategy(paramsAssembly[]assemblies)
{
this.catalog=newAggregateCatalog(assemblies
.Select(asm=newAssemblyCatalog(asm)));
}
publicIDictionaryString,TypeGetTenants()
{
//getthedefaulttenant
vartenants=this.catalog.GetExports(
newImportDefinition(a=true,null,
ImportCardinality.ZeroOrMore,false,false))
.ToList();
vardefaultTenant=tenants.SingleOrDefault(x=x.Item2.Metadata
.ContainsKey(“Default”));
if(defaultTenant!=null)
{
varisDefault=Convert.ToBoolean(defaultTenant.Item2
.Metadata[“Default”]);
if(isDefault)
{
if(String.IsNullOrWhiteSpace(
TenantsConfiguration.DefaultTenant))
{
TenantsConfiguration.DefaultTenant=
defaultTenant.Item2.ContractName;
}
}
}
returnthis.catalog.GetExportedTypesITenantConfiguration();
}
}
Thiscodewill lookuppartsfromeitherasetofassembliesorasetofpaths.Thenitwilltrytosetatenantasthedefaultone,ifnodefaultisset.Let’saddittotheTenantsConfigurationclass:
CodeSample29
publicstaticclassTenantsConfiguration
{
//restgoeshere
publicstaticclassLocations
{
publicstaticXmlTenantLocationStrategyXml()
{
returnXmlTenantLocationStrategy.Instance;
}
publicstaticMefTenantLocationStrategyMef
(paramsAssembly[]assemblies)
{
returnnewMefTenantLocationStrategy(assemblies);
}
publicstaticMefTenantLocationStrategyMef(paramsString[]paths)
{
returnnewMefTenantLocationStrategy(paths);
}
//restgoeshere
}
}
container.RegisterInstanceITenantLocationStrategy(TenantsConfiguration.Locations.
Mef(“some”,“path”);
At the end, we register this implementation as the default tenant locationstrategy—remember,therecanbeonlyone.ThiswillbetheinstancereturnedbytheCommonServiceLocatorfortheITenantLocationStrategytype.
Note:Again,refertoChapter9foracomparisonofthesestrategies.
BootstrappingtenantsWeneedtorunthebootstrappingcodeatthestartofthewebapplication.We
haveacoupleofoptions:
AttheApplication_StarteventoftheHttpApplication,normallydefinedintheGlobal.asax.csUsingthePreApplicationStartMethodAttribute,toruncodebeforeApplication_Start
Inanycase,weneedtosetthestrategiesandgetthetenantslist:
CodeSample30
TenantsConfiguration.DefaultTenant=“abc.com”;
TenantsConfiguration.TenantIdentifier=TenantsConfiguration.Identifiers
.HostHeader;
TenantsConfiguration.TenantLocation=TenantsConfiguration.Locations.Mef();
TenantsConfiguration.Initialize();
Here’sanupdatedTenantsConfigurationclass:CodeSample31
publicstaticclassTenantsConfiguration
{
publicstaticStringDefaultTenant{get;set;}
publicstaticITenantIdentifierStrategyTenantIdentifierStrategy
{get;set;}
publicstaticITenantLocationStrategyTenantLocationStrategy{get;set;}
publicstaticvoidInitialize()
{
vartenants=GetTenants();
InitializeTenants(tenants);
CreateLogFactories(tenants);
CreatePerformanceCounters(tenants);
}
privatestaticvoidInitializeTenants
(IEnumerableITenantConfigurationtenants)
{
foreach(vartenantintenants)
{
tenant.Initialize();
}
}
privatestaticvoidCreatePerformanceCounters(
IEnumerableITenantConfigurationtenants)
{
if(PerformanceCounterCategory.Exists(“Tenants”)==false)
{
varcol=newCounterCreationDataCollection(tenants
.Select(t=newCounterCreationData(t.Name,
String.Empty,PerformanceCounterType.NumberOfItems32))
.ToArray());
varcategory=PerformanceCounterCategory
.Create(“Tenants”,
“TenantsPerformanceCounters”,
PerformanceCounterCategoryType
.MultiInstance,
col);
foreach(vartenantintenants)
{
foreach(varinstanceNameintenant.Counters)
{
using(varpc=newPerformanceCounter(
category.CategoryName,
tenant.Name,
String.Concat(tenant.Name,
“:”,
instanceName),false))
{
pc.RawValue=0;
}
}
}
}
}
}
By leveraging the ServiceProvider property, we can add our own tenant-specific services. For instance, consider this implementation that registers aprivateUnitycontainerandaddsacoupleofservicestoit:
CodeSample32
publicsealedclassXyzNetTenantConfiguration:ITenantConfiguration
{
privatereadonlyIUnityContainercontainer=newUnityContainer();
publicvoidInitialize()
{
this.container.RegisterTypeIMyService,MyServiceImpl();
this.container.RegisterInstanceIMyOtherService(newMyOtherServiceImpl());
}
publicIServiceProviderServiceProvider{get{returnthis.container;}}
//restgoeshere
}
Then, we can get the actual services from the current tenant, if they areavailable:
CodeSample33
vartenant=TenantsConfiguration.GetCurrentTenant();
var myService = tenant.ServiceProvider.GetService(typeof(IMyService)) asIMyService;
There’sasmallgotchahere:Unitywillthrowanexceptionincasethereisnoregistrationby thegiven type. Inorder togetaround this,wecanuse thisniceextensionmethodoverIServiceProvider:
CodeSample34
publicstaticclassServiceProviderExtensions
{
publicstaticTGetServiceT(thisIServiceProviderserviceProvider)
{
varservice=default(T);
try
{
service=(T)serviceProvider.GetService(typeof(T));
}
catch
{
}
returnservice;
}
}
Asyoucansee,besidesperformingacast, italsoreturnsthedefault typeofthe generic parameter (likely null, in the case of interfaces) if no registrationexists.
Chapter4ASP.NETWebForms
Chapter4ASP.NETWebFormsIntroductionASP.NETWebForms is theoriginal framework introducedwith .NETforweb
development. Probably the reason for its success lies in how easy it is toimplement simple scenarios including data binding, AJAX functionality, keepingcontrol state between postbacks, and performing user authentication andauthorization.Itsdetractorspointoutthatallthemagiccomeswithacostintermsof performance, excessive complexity, error-proneness of the page, and controllifecycle;andhavebeenpushingnewertechnologiessuchasMVC.Thetruth,inmy opinion, is that some complex requirements, such as those addressed bySharePoint(which isbasedonWebForms)aretoodifficult, ifnot impossible, toimplementwithMVC.
In this chapterwewill seewhichmechanismsASP.NETWeb Forms has toofferwhenitcomestowritingmultitenantapplications—namely,whenitcomestobranding.
Note: Even though it may seem asthoughMVCistakingover,makenomistake—WebFormswillstillbearoundforalong time. It is true that ASP.NET 5will not supportWebForms, at least in itsinitial version, but the 4.x series will coexist with version 5 and will continuedevelopment of Web Forms. For pointers and discussions on MVC vs WebForms,checkoutthisMicrosoftCurahandDinoEsposito’sviewonthesubject.
Table5:Brandingconcepts
Concept
API
Branding
MasterPages
ThemesandSkins
Authentication
ASP.NETMembership/ASP.NETIdentity
Workflow
Unity/CommonServiceLocator
DataModel
EntityFrameworkCodeFirstorNHibernate
Branding
BrandingASP.NETWebFormsoffersprimarilytwotechniquesforbranding:
MasterPages:Asharedlayout(andpossiblyfunctionality)thatdefinesthegloballookandfeeltowhichindividualpagescanaddcontentsto;itworksbydefiningareas(contentplaceholders),possiblywithdefaultcontents,whichpagescanoverride,whilekeepingthegloballayout.ThemesandSkins:AnamedcollectionofcontrolpropertysettingsandCSSstylesheetfilesthatapplytoallpagesautomatically.All.cssfilesinathemefolderunderApp_Themesareautomaticallyaddedtoallpages;allcontrolpropertiesareappliedtoallcontrolsdeclaredinthemarkup,unlessexplicitlyoverriddenthere.
MasterpagesIn the master page, we can add the same markup—HTML and ASP.NET
controldeclarations—aswewouldinaregularpage,butwecanalsoaddcontentplaceholders:
CodeSample35
%@MasterLanguage=“C#”CodeBehind=“Site.Master.cs”Inherits=“WebForms.Site”%
!DOCTYPEhtml
html
headrunat=“server”
title
asp:ContentPlaceHolderID=“title”runat=“server”/
/title
asp:ContentPlaceHolderID=“head”runat=“server”
DefaultHeadcontent
/asp:ContentPlaceHolder
/head
body
formrunat=“server”
asp:ContentPlaceHolderID=“header”runat=“server”/
div
asp:ContentPlaceHolderID=“body”runat=“server”/
/div
asp:ContentPlaceHolderID=“footer”runat=“server”/
/form
/body
/html
ContentPlaceholder elements represents the “extensible” areas, or holes, inthemasterpage.PagesmayincludeContentelementsthatsupplycontent,thusoverriding what is defined in the master page. The master page is normallyassignedinthemarkupofapage:
CodeSample36
%@PageLanguage=“C#”MasterPageFile=”~/Site.Master”CodeBehind=“Default.aspx.cs”Inherits=“WebForms.Default”%
asp:ContentContentPlaceHolderID=“title”runat=“server”
Thisisthetitle
/asp:Content
asp:ContentContentPlaceHolderID=“head”runat=“server”
Thisistheoverriddenhead
/asp:Content
asp:ContentContentPlaceHolderID=“body”runat=“server”
Bodycontentgoeshere
/asp:Content
Youcanseethatnotallcontentplaceholdersdefinedinthemasterpagearebeingusedinthepage.Also,thecontentfortheheadplaceholderisoverriddeninthepage,whichisperfectlynormal.
AcontentplaceholdertakesthespaceofitscontainingHTMLelement,so,forexample,twomasterpagescouldbedefinedas:
CodeSample37
!—masterpage1—
table
tr
tdasp:ContentPlaceHolderID=“first”runat=“server”//td
tdasp:ContentPlaceHolderID=“second”runat=“server”//td
/tr
/table
And:
CodeSample38
!—masterpage2—
table
tr
tdasp:ContentPlaceHolderID=“first”runat=“server”//td
/tr
tr
tdasp:ContentPlaceHolderID=“second”runat=“server”//td
/tr
/table
I’mnotsayingthatyoushoulduseHTMLtablesforlayout;thisisjusttomakeapoint:contentwillappeardifferentlywhethermasterpage1or2isused.
Ifyourecall,thetenantconfigurationinterfacethatwespecifiedinChapter3,ITenantConfiguration, included aMasterPage property. We will leverage thisproperty in order to set automatically the master page to use for our pages,dependingonthecurrenttenant.
Therearetwowaysbywhichamasterpagecanbeset:
Declarativelyinthepage’smarkup,usingtheMasterPageFileattributeofthe@PagedirectiveProgrammatically,usingthePage.MasterPageFileproperty
A master page can only be set programmatically before or during thePage.PreIniteventofthepage’slifecycle;afterthat,anyattempttochangeitwillresultinanexceptionbeingthrown.Ifwewanttosetmasterpagesautomatically,without imposinga basepage class,we shouldwrite a custommodule for thatpurpose:
CodeSample39
publicsealedclassMultiTenancyModule:IHttpModule
{
publicvoidDispose(){}
publicvoidInit(HttpApplicationcontext)
{
context.PreRequestHandlerExecute+=OnPreRequestHandlerExecute;
}
publicstaticStringMasterPagesPath{get;set;}
privatevoidOnPreRequestHandlerExecute(Objectsender,EventArgse)
{
vartenant=TenantsConfiguration.GetCurrentTenant();
varapp=senderasHttpApplication;
if((app!=null)(app.Context!=null))
{
varpage=app.Context.CurrentHandlerasPage;
if(page!=null)
{
page.PreInit+=(s,args)=
{
varp=sasPage;
if(!String.IsNullOrWhiteSpace
(tenant.MasterPage))
{
//setthemasterpage
p.MasterPageFile=
String.Format(“{0}/{1}.Master”,
MasterPagesPath,
tenant.MasterPage);
}
};
}
}
}
}
The MasterPagesPath static property exists so that we can specify analternative locationforourmasterpages, incasewedon’twant themlocatedatthesite’sroot.It’ssafetoleaveitblankifyourmasterpagesarelocatedthere.
Thatthismodulehooksuptothecurrentpage’sPreIniteventand,insidetheevent handler, checks if theMasterPage property for the current tenant is set,and,ifso,setsitasthemasterpageofthecurrentpage.
ThemesandskinsFor applying a theme or a skin, we need to create a folder under
App_Themes:
Figure12:Themefolders
Wecanhaveanynumberof folders,butonlyonecanbesetas thecurrenttheme.
ThemesA theme consists of one ormore .css files located inside the theme folder;
even inside other folders, ASP.NET makes sure all of them are added to thepages.Therearethreewaystosetatheme:
ThroughthestyleSheetThemeattributeofthepageselementofWeb.configBysettingtheStyleSheetThemeattributeinsidethePagedirectiveofthe.aspxfileAssigningavaluetotheStyleSheetThemepropertyofthePageclass
The first two don’t really play well with dynamic contents, but the final onedoes.Italsotakesprecedenceovertheothertwo:
TheWeb.configsettingisthefirstonethatwillbeappliedtoallpages,unlessspecifiedotherwiseinotherlocation.Thencomesthe.aspxattribute,whichonlyappliestothatspecificpage.Finally,thepageproperty,ifsetuntilthePage.PreInitevent,istheonetouse.
SkinsA skin consists of one or more .skin files, also under a folder beneath
App_Themes.Eachfilecontainsmultiplecontroldeclarations,suchasthosewewouldfindinan.aspx,.ascx,or.mastermarkupfile,withvaluesforsomeofthe
control’sproperties:
CodeSample40
asp:TextBoxrunat=“server”SkinID=“Email”placeholder=“Emailaddress”/
asp:TextBoxrunat=“server”Text=“entervalue”/
asp:Buttonrunat=“server”SkinID=“Dark”BackColor=“DarkGray”ForeColor=“Black”/
asp:Buttonrunat=“server”SkinID=“Light”BackColor=“Cyan”ForeColor=“Green”/
asp:Imagerunat=“server”onerror=“this.src=‘missing.png’”/
OnlypropertieshavingtheThemeableAttributewithavalueoftrue(defaultifnoattributeisset)andlocatedinacontrolalsohavingThemeableAttributesettotrue (ornoattributeatall), orplainattributes thatdonothavea correspondingproperty (like placeholder in the first TextBox or onerror in the ImagedeclarationinCodeSample37)canbesetthrougha.skinfile.
Herewecanseeafewdifferentoptions:
AnyTextBoxwiththeSkinIDpropertysettoEmailwillgetanHTML5placeholderattribute,whichisbasicallyawatermark;noticethatthisisnotapropertyoftheTextBoxcontrol,oranyofitsbaseclasses;instead,itwillbetranslatedtoanidenticallynamedattributeinthegeneratedHTMLtag.AllTextBoxcontrolswithoutaSkinIDsetwillhavetext“entervalue”ButtoncontrolswithaSkinIDof“Dark”willgetadarkeraspectthanthosewithaSkinIDof“Light”AnyImagecontrolthatraisesaJavaScriptonerroreventwillhaveanalternativeimagesetthroughaJavaScripteventhandler.
Note: I chose these examples sothat it isclear that themedpropertiesdon’tnecessarilymeanonlyuser interfacesettings.
TheSkinIDpropertyisoptional;ifset,ASP.NETwilltrytofindanydeclarationsonthecurrenttheme’s.skinfilesthatmatchitsvalue.Otherwise,itwillfallbacktocontroldeclarationsthatdonotcontainaSkinIDattribute.
Likewiththemes,therearethreewaystosetaskinfolder:
ThroughtheThemeattributeofthePageselementofWeb.configBysettingtheThemeattributeinsidethePagedirectiveofthe.ASPXfileAssigningavaluetotheThemepropertyofthePageclass.
Skinsandstylesheetthemes,althoughdifferentthings,arecloselyrelated,soitisagoodideatousethesamethemefolderforbothbysettingjusttheThemeproperty.Ifyoudo,ASP.NETwillparseallthe.skinfilesandalsoincludeany.cssfilesfoundinsidethethemefolder.
SettingthemesautomaticallyKnowing this, let’schangeourMultiTenancyModuleso that,besidessetting
themasterpage,italsosetsthethemeforthecurrentpage.Let’screateathemeforeachtenant,underitsname,andsetitautomatically:
CodeSample41
publicsealedclassMultiTenancyModule:IHttpModule
{
//restgoeshere
privatevoidOnPreRequestHandlerExecute(Objectsender,EventArgse)
{
vartenant=TenantsConfiguration.GetCurrentTenant();
varapp=senderasHttpApplication;
if((app!=null)(app.Context!=null))
{
varpage=app.Context.CurrentHandlerasPage;
if(page!=null)
{
page.PreInit+=(s,args)=
{
varp=sasPage;
if(!String.IsNullOrWhiteSpace(tenant))
{
//setthetheme
p.Theme=tenant.Theme;
p.MasterPageFile=
String.Format(“{0}/{1}.Master”,
MasterPagesPath,p.MasterPage);
}
};
}
}
}
}
Tenant-specificimagesYou canalso keepother contents, suchas images, inside the theme folder.
This is nice if wewant to have different images for each tenantwith the samename. The problem is that you cannot reference those images directly in yourpages,becauseyoudon’tknowbeforehandwhichtheme—meaning,whichtenant—will be accessing the page. For example, shall you point the image to“~/App_Themes/abc.com/logo.png”or“~/App_Themes/xyz.net/logo.png”?
Fortunately, ASP.NETWeb Forms offers an extensibility mechanism by thename of Expression Builders, which comes in handy here. If you have usedresourcesinWebFormspages,youhavealreadyusedtheResourceexpressionbuilder. In a nutshell, an expression builder picks up a string passed as aparameter, tries to make sense of it, and then returns some content to thepropertytowhichitisbound(anexpressionbuilderalwaysrunsinthecontextofapropertyofaserver-sidecontrol).Howyouparsetheparameterandwhatyoudowithitisuptoyou.
Let’swriteanexpressionbuilderthattakesapartialURLandmakesitrelativetothecurrenttheme.PleaseconsidertheThemeFileUrlexpressionbuilder:
CodeSample42
[ExpressionPrefix(“ThemeFileUrl”)]
publicclassThemeFileUrlExpressionBuilder:ExpressionBuilder
{
publicoverrideObjectEvaluateExpression(Objecttarget,
BoundPropertyEntryentry,ObjectparsedData,
ExpressionBuilderContextcontext)
{
if(String.IsNullOrWhiteSpace(entry.Expression))
{
returnbase.EvaluateExpression(target,entry,parsedData,
context);
}
else
{
returnGetThemeUrl(entry.Expression);
}
}
publicoverrideBooleanSupportsEvaluate{get{returntrue;}}
publicoverrideCodeExpressionGetCodeExpression(BoundPropertyEntryentry,ObjectparsedData,ExpressionBuilderContextcontext)
{
if(String.IsNullOrWhiteSpace(entry.Expression))
{
returnnewCodePrimitiveExpression(String.Empty);
}
else
{
returnnewCodeMethodInvokeExpression(
newCodeMethodReferenceExpression(
newCodeTypeReferenceExpression(this.GetType()),
“GetThemeUrl”),
newCodePrimitiveExpression(entry.Expression));
}
}
publicstaticStringGetThemeUrl(StringfileName)
{
varpage=HttpContext.Current.HandlerasPage;
//we can use the Page.Theme property because, at this point, theMultiTenancyModulehasalreadyrun,andhassetitproperly
vartheme=page.Theme;
varpath=(page!=null)?String.Concat(“/App_Themes/”,
theme,”/”,fileName):String.Empty;
returnpath;
}
}
Before we can use an expression builder, we need to register in theWeb.configfileinsectionsystem.web/compilation/expressionBuilders(doreplace“MyNamespace”and“MyAssembly”withthepropervalues):
CodeSample43
system.web
compilationdebug=“true”targetFramework=“4.5”
expressionBuilders
addexpressionPrefix=“ThemeFileUrl”type=“MyNamespace
.ThemeFileUrlExpressionBuilder,MyAssembly”/
/expressionBuilders
/compilation
/system.web
Now,wecanuseitinourpagesas:
CodeSample44
asp:Imagerunat=“server”ImageUrl=”%$ThemeFileUrl:/logo.jpg%”/
The ThemeFileUrl expression builder will make sure that the right theme-specificpathisused.
Showingorhidingtenant-specificcontentsAnotheruseofanexpressionbuilderistoshoworhidecontentsdirectedtoa
specific tenant without writing code. We create the MatchTenant expressionbuilder:
CodeSample45
[ExpressionPrefix(“MatchTenant”)]
publicsealedclassMatchTenantExpressionBuilder:ExpressionBuilder
{
publicoverrideObjectEvaluateExpression(Objecttarget,
BoundPropertyEntryentry,ObjectparsedData,
ExpressionBuilderContextcontext)
{
if(String.IsNullOrWhiteSpace(entry.Expression))
{
returnbase.EvaluateExpression(target,entry,parsedData,
context);
}
else
{
returnMatchTenant(entry.Expression);
}
}
publicoverrideBooleanSupportsEvaluate{get{returntrue;}}
publicoverrideCodeExpressionGetCodeExpression(BoundPropertyEntryentry,ObjectparsedData,ExpressionBuilderContextcontext)
{
if(String.IsNullOrWhiteSpace(entry.Expression))
{
returnnewCodePrimitiveExpression(String.Empty);
}
else
{
returnnewCodeMethodInvokeExpression(
newCodeMethodReferenceExpression(
newCodeTypeReferenceExpression(
this.GetType()),“MatchTenant”),
newCodePrimitiveExpression(entry.Expression));
}
}
publicstaticBooleanMatchTenant(Stringtenant)
{
varcurrentTenant=TenantsConfiguration.GetCurrentTenant();
if(tenant==currentTenant.Name)
{
returntrue;
}
if(tenant.StartsWith(“!”))
{
if(tenant.Substring(1)!=currentTenant.Name)
{
returnfalse;
}
}
returnfalse;
}
}
AndweregisteritintheWeb.configfile:CodeSample46
system.web
compilationdebug=“true”targetFramework=“4.5”
expressionBuilders
addexpressionPrefix=“ThemeFileUrl”type=“MyNamespace
.ThemeFileUrlExpressionBuilder,MyAssembly”/
addexpressionPrefix=“MatchTenant”type=“MyNamespace
.MatchTenantExpressionBuilder,MyAssembly”/
/expressionBuilders
/compilation
/system.web
Twosampleusages:
CodeSample47
asp:Labelrunat=“server”Text=“abc.comonly”Visible=”%$MatchTenant:abc.com%”/
asp:Label runat=“server” Text=“Anything butabc.com”Visible=”%$MatchTenant:!abc.com%”/
Byaddingthe“!”symboltothetenantname,wenegatetherule.
Security
SecurityAuthorizationAuthorization in ASP.NET Web Forms is dealt with by the
FileAuthorizationModule and the UrlAuthorizationModule modules. Thesemodulesweren’treallydesignedwithextensibilityinmind,soitisn’texactlyeasytomakethemdowhatwewant.
It isbasedaroundtwoconcepts:authenticatedorunauthenticatedusersanduserroles.Thebuilt-inauthorizationmechanismsallowus todefine,perpath,apageorafilesystemfolderunderourwebapplicationroot,iftheresourceshallbeaccessedby:
Anonymous(notauthenticated)usersSpecificusernames(e.g.,“peter.jackson”,“johndoe”,etc.)Oneofaspecificsetofroles(e.g.,“Admins”,“Developers”,etc.)Everyone(thedefault)
Asyoucansee,thereisnoobviousmappingbetweentheseconceptsandthatof tenants,butwecanuse rolesas tenants’names to restrictaccess tocertainresources.WedosointheWeb.configfile,inthelocationsections:
CodeSample48
locationpath=“AbcComFolder”
system.webServer
security
authorization
addaccessType=“Deny”users=”?”/
addaccessType=“Allow”roles=“abc.com”/
/authorization
/security
/system.webServer
/location
Thisexampleusesaroleof“abc.com”,whichisalsoatenant’sname.Thingsgeta littlebit tricky, though, ifweneed touseboth rolesand tenants inaccessrules.Anotheroptionistorestrictcallingcertainmethodsbythecurrenttenant.Anexampleusing.NET’sbuilt-indeclarativesecuritymightbe:
CodeSample49
[Serializable]
publicsealedclassTenantPermission:IPermission
{
publicTenantPermission(paramsString[]tenants)
{
this.Tenants=tenants;
}
publicIEnumerableStringTenants{get;privateset;}
publicIPermissionCopy()
{
returnnewTenantPermission(this.Tenants.ToArray());
}
publicvoidDemand()
{
vartenant=TenantsConfiguration.GetCurrentTenant();
if(!this.Tenants.Any(t=tenant.Name()==t))
{
thrownewSecurityException
(“Currenttenantisnotallowedtoaccessthisresource.”);
}
}
publicIPermissionIntersect(IPermissiontarget)
{
varother=targetasTenantPermission;
if(other==null)
{
thrownewArgumentException
(“Invalidpermission.”,“target”);
}
returnnewTenantPermission
(this.Tenants.Intersect(other.Tenants).ToArray());
}
publicBooleanIsSubsetOf(IPermissiontarget)
{
varother=targetasTenantPermission;
if(other==null)
{
thrownewArgumentException
(“Invalidpermission.”,“target”);
}
returnthis.Tenants.All(t=other.Tenants.Contains(t));
}
publicIPermissionUnion(IPermissiontarget)
{
varother=targetasTenantPermission;
if(other==null)
{
thrownewArgumentException
(“Invalidpermission.”,“target”);
}
returnnewTenantPermission
(this.Tenants.Concat(other.Tenants).Distinct()
.ToArray());
}
publicvoidFromXml(SecurityElemente)
{
if(e==null)
{
thrownewArgumentNullException(“e”);
}
vartenantTag=e.SearchForChildByTag(“Tenants”);
if(tenantTag==null)
{
thrownewArgumentException
(“Elementdoesnotcontainanytenants.”,“e”);
}
vartenants=tenantTag.Text.Split(‘,’).Select(t=t.Trim());
this.Tenants=tenants;
}
publicSecurityElementToXml()
{
varxml=String
.Concat(“IPermissionclass=””,
this.GetType().AssemblyQualifiedName,
””version=“1”unrestricted=“false”Tenants”,
String.Join(“,”,this.Tenants),
“/Tenants/IPermission”);
returnSecurityElement.FromString(xml);
}
}
[Serializable]
[AttributeUsage(AttributeTargets.Method|AttributeTargets.Class,
AllowMultiple=true,Inherited=false)]
publicsealedclassRequiredTenantPermissionAttribute:CodeAccessSecurityAttribute
{
publicRequiredTenantPermissionAttribute(SecurityActionaction):
base(action){}
publicoverrideIPermissionCreatePermission()
{
returnnewTenantPermission(this.Tenants.Split(‘,’)
.Select(t=t.Trim()).ToArray());
}
publicStringTenants{get;set;}
}
The RequiredTenantPermissionAttribute attribute, when applied to amethod, does a runtime check, which in this case checks the current tenantagainstalistofallowedtenants(theTenantsproperty).Ifthere’snomatch,thenanexceptionisthrown.Anexample:
CodeSample50
[RequiredTenantPermission(SecurityAction.Demand,Tenants=“abc.com”)]
publicoverridevoidOnLoad(EventArgse)
{
//nobodyotherthanabc.comcanaccessthismethod
base.OnLoad(e);
}
In Chapter 6, we will look at another technique that can also be used forrestrictingaccesses.
Unittesting
UnittestingUnittestingwithWebFormsisverydifficultbecauseit’shardtoreproducethe
eventmodel that pages and controls use. It isn’t impossible, though, and toolssuchasWatiNmakeitpossible.WatiNallowsinstrumentingwebsitesusingourbrowserofchoice. Iwon’tcover it indetailhere,buthavea lookat thissamplecode,andIthinkyou’llgetthepicture:
CodeSample51
using(varbrowser=newIE(“http://abc.com”))
{
Assert.IsTrue(browser.ContainsText(“ABC”));
browser.Button(Find.ByName(“btnGo”)).Click();
}
InordertouseWatiN,justaddareferencetoitusingNuGet:
Figure13:AddingtheWatiNNuGetpackage
Chapter5ASP.NETMVC
Chapter5ASP.NETMVCIntroductionASP.NETMVC introduces a very different developmentmodel.Most people
would agree that it ismore testable, promotes a separation of responsibilities -viewsforuserinterface,controllersforbusinesslogic–andismuchclosertotheHTTPprotocol,avoidingsomemagic thatWebFormsemploys,which,althoughuseful,canresultinincreasedcomplexityanddecreaseofperformance.
Note: Nick Harrison wrote MVCSuccinctly for theSuccinctlyseries;besure to read it foragood introduction toMVC.
Branding
BrandingHerewewillexplorethreebrandingmechanismsofferedbyMVC:
Pagelayouts:TheequivalenttoWebForms’masterpages,whereweshallhaveadifferentoneforeachtenant.Viewlocations:Eachtenantwillhaveitsviewsstoredinaspecificfolder.CSSbundles:EachtenantwillhaveitsownCSSbundle(acollectionoftenant-specific.cssfiles).
PagelayoutsMVC’sviewshaveamechanismsimilartoWebForms’masterpages,bythe
nameofPageLayouts.ApagelayoutspecifiestheglobalstructureoftheHTMLcontent,leaving“holes”tobefilledbypagesthatuseit.Bydefault,ASP.NETMVCuses the “~/Views/Shared/_Layout.cshtml” layout for C# views, and“_Layout.vbhtml”forVisualBasicviews.
Theonlyway touseapage layout is toexplicitlyset it inaview, like in thisexample,usingRazor:
CodeSample52
@{
Layout=”~/Views/Shared/Layout.cshtml”;
}
Inordertoavoidrepeatingcodeoverandoverjusttosetthepagelayout,wecanuseoneofMVC’sextensibilitypoints, theViewEngine.BecausewewillbeusingRazorastheviewengineofchoice,weneedtosubclassRazorViewEngine,inordertoinjectourtenant-specificpagelayout.
Ourimplementationwilllooklikethis:
CodeSample53
publicsealedclassMultitenantRazorViewEngine:RazorViewEngine
{
publicoverrideViewEngineResultFindView(ControllerContextctx,
StringviewName,StringmasterName,BooleanuseCache)
{
vartenant=TenantsConfiguration.GetCurrentTenant();
//thethirdparametertoFindViewisthepagelayout
returnbase.FindView(controllerContext,viewName,
tenant.MasterPage,useCache);
}
}
Inordertouseournewviewengine,weneedtoreplacetheexistingoneintheViewEngines.Engines collection; this should be done in somemethod spawnedfromApplication_Start:
CodeSample54
ViewEngines.Engines.Clear();
ViewEngines.Engines.Add(newMultitenantRazorViewEngine());
Finally,wemustmake sureour views set the page layout (Layout property)from ViewBag.Layout, which is mapped to whatever is returned in the thirdparameterofFindView:
CodeSample55
@{
Layout=ViewBag.Layout;
}
ViewlocationsBydefault, theviewenginewill look forviews(.cshtmlor .vbhtml) inapre-
definedcollectionofvirtualpaths.Whatwewouldliketodoislookforaviewinatenant-specific path first, and if it’s not found there, fall back to the defaultlocations.Whywouldwewant that?Well, thisway,wecandesignourviews tohaveamoretenant-specific look,morethanwecoulddo justbychangingpagelayouts.
Wewillmakeuseof thesameviewengine introduced in thepreviouspage,andwilladaptitforthispurpose:
CodeSample56
publicsealedclassMultitenantRazorViewEngine:RazorViewEngine
{
privateBooleanpathsSet=false;
publicMultitenantRazorViewEngine():this(false){}
publicMultitenantRazorViewEngine(BooleanusePhysicalPathsPerTenant)
{
this.UsePhysicalPathsPerTenant=usePhysicalPathsPerTenant;
}
publicBooleanUsePhysicalPathsPerTenant{get;privateset;}
privatevoidSetPaths(ITenantConfigurationtenant)
{
if(this.UsePhysicalPathsPerTenant)
{
if(!this.pathsSet)
{
this.ViewLocationFormats=newString[]
{
String.Concat(“~/Views/”,
tenant.Name,”/{1}/{0}.cshtml”),
String.Concat(“~/Views/”,
tenant.Name,”/{1}/{0}.vbhtml”)
}.Concat(this.ViewLocationFormats).ToArray();
this.pathsSet=true;
}
}
}
publicoverrideViewEngineResultFindView(ControllerContextctx,
StringviewName,StringmasterName,BooleanuseCache)
{
vartenant=TenantsConfiguration.GetCurrentTenant();
//thethirdparametertoFindViewisthepagelayout
returnbase.FindView(controllerContext,viewName,
tenant.MasterPage,useCache);
}
}
This requires that the view engine is configured with theusePhysicalPathsPerTenantset:
CodeSample57
ViewEngines.Engines.Add(newMultitenantRazorViewEngine(true));
CSSBundlesCSS Bundling and Minification is integrated in the two major flavors of
ASP.NET: Web Forms and MVC. In Web Forms, because it has the themesmechanism(seeThemesandSkins), it isprobablynotmuchused forbranding,butMVCdoesnothaveananalogousmechanism.
ACSSbundleconsistsofanameandasetof.cssfileslocatedinanynumberoffolders.Wewillcreate,foreachoftheregisteredtenants,anidentically-namedbundlecontainingallofthefilesinafoldernamedfromthetenantconfiguration’sThemepropertyunderthefolder~/Content.Here’show:
CodeSample58
publicstaticvoidRegisterBundles(BundleCollectionbundles)
{
foreach(vartenantinTenantsConfiguration.GetTenants())
{
varvirtualPath=String.Format(“~/{0}”,tenant.Name);
varphysicalPath=String.Format(“~/Content/{0}”,
tenant.Theme);
if(!BundleTable.Bundles.Any(b=b.Path==virtualPath))
{
varbundle=newStyleBundle(virtualPath)
.IncludeDirectory(physicalPath,”*.css”);
BundleTable.Bundles.Add(bundle);
}
}
}
Thismethodneedstobecalledafterthetenantsareregistered:
CodeSample59
RegisterBundles(BundleTable.Bundles);
Tenant“abc.com”will get aCSSbundle called“~/abc.com” containing all.cssfilesunder“~/Content/abc.com”.
Thisisnotall,however;inordertoactuallyaddtheCSSbundletoaview,weneedtoaddanexplicitcallontheview,likethisone:
CodeSample60
@Styles.Render(“~/abc.com”)
However,ifwearetohardcodethetenant’sname,thiswon’tscale.Thiswouldbeokayfortenant-specificlayoutpages,butnotforgenericviews.Whatweneedisamechanismtoautomaticallysupply,oneachrequest,therightbundlename.Fortunately,wecanachievethiswithanactionfilter:
CodeSample61
publicsealedclassMultitenantActionFilter:IActionFilter
{
voidIActionFilter.OnActionExecuted(ActionExecutedContextctx){}
voidIActionFilter.OnActionExecuting(ActionExecutingContextctx)
{
vartenant=TenantsConfiguration.GetCurrentTenant();
ctx.Controller.ViewBag.Tenant=tenant.Name;
ctx.Controller.ViewBag.TenantCSS=String.Concat(“~/”,
filterContext.Controller.ViewBag.Tenant);
}
}
ThisactionfilterimplementstheIActionFilterinterface,andallitdoesisinjecttwo values in the ViewBag collection, the tenant name (Tenant), and the CSSbundle’sname(TenantCSS).Actionfilterscanberegisteredinanumberofways,oneofwhichistheGlobalFilters.Filterscollection:
CodeSample62
GlobalFilters.Filters.Add(newMultitenantActionFilter());
Withthison,allweneedtoaddcustomtenant-specificbundlesonaviewis:
CodeSample63
@Styles.Render(ViewBag.TenantCSS)
Security
SecurityWemightwanttorestrictsomecontrolleractionsforsometenant.MVCoffers
ahookcalledanauthorizationfilterthatcanbeusedtoallowordenyaccesstoagiven controller or action method. There’s a base implementation,AuthorizeAttribute,whichgrantsaccessonly if thecurrentuser isauthenticated.Theimplementationallowsextensibility,andthat’swhatwearegoingtodo:
CodeSample64
[Serializable]
[AttributeUsage(AttributeTargets.Class|AttributeTargets.Method,
AllowMultiple=false,Inherited=true)]
publicsealedclassAllowedTenantsAttribute:AuthorizeAttribute
{
publicAllowedTenantsAttribute(paramsString[]tenants)
{
this.Tenants=tenants;
}
publicIEnumerableStringTenants{get;privateset;}
protectedoverrideBooleanAuthorizeCore(HttpContextBasectx)
{
vartenant=TenantsConfiguration.GetCurrentTenant();
returnthis.Tenants.Any(x=x==tenant.Name);
}
}
When applied to a controller method or class with one or more tenants asparameters,itwillonlygrantaccesstothespecifiedtenants:
CodeSample65
publicclassSomeController:Controller
{
[AllowedTenants(“abc.com”)]
publicActionResultSomeThing()
{
//thiscanonlybeaccessedbyabc.com
//returnsomething
}
[AllowedTenants(“xyz.net”,“abc.com”)]
publicActionResultOtherThing()
{
//thiscanbeaccessedbyabc.comandxyz.net
//returnsomething
}
}
The next chapter describes another technique for restricting access toresourceswithouttheneedforcode.
UnitTesting
UnitTestingTesting MVC controllers and actions is straightforward. In the context of
multitenantapplications,aswehavebeentalkinginthisbook,wejustneedtosetupourtestbenchaccordingtotherighttenantidentificationstrategy.Forexample,ifyouareusingNUnit,youwoulddo itbeforeeach test, inamethoddecoratedwiththeSetUpFixtureAttribute:
CodeSample66
[SetUpFixture]
publicstaticclassMultitenantSetup
{
[SetUp]
publicstaticvoidSetup()
{
varreq=newHttpRequest(
filename:String.Empty,
url:“http://abc.com/”,
queryString:String.Empty);
req.Headers[“HTTP_HOST”]=“abc.com”;
//addothersasneeded
varresponse=newStringBuilder();
varres=newHttpResponse(newStringWriter(response));
varctx=newHttpContext(req,res);
varprincipal=newGenericPrincipal(
newGenericIdentity(“Username”),new[]{“Role”});
varculture=newCultureInfo(“pt-PT”);
Thread.CurrentThread.CurrentCulture=culture;
Thread.CurrentThread.CurrentUICulture=culture;
HttpContext.Current=ctx;
HttpContext.Current.User=principal;
}
}
Chapter6WebServices
Chapter6WebServicesIntroductionWebservices,ofcourse,arealsocapableofhandlingmultitenantrequests.In
the .NET world, there are basically two APIs for implementing web services:Windows Communication Foundation (WCF) and the new Web API. Althoughtheysharesomeoverlapping features, theirarchitectureandbasicconceptsarequitedifferent.WhileathoroughdiscussionoftheseAPIsisoutsidethescopeofthisbook,let’sseesomeofthekeypointsthatarerelevanttomultitenancy.
WCF
WCFWCF,bydefault,doesnotmakeuseof theASP.NETpipeline,whichmeans
that HttpContext.Current is not available. This is so that WCF has a morestreamlined, focused model where the classic pipeline is not needed (but it ispossibletoenableit).
If we do not want to use the ASP.NET pipeline, we need to change ourimplementationofthetenantidentificationstrategy.InsideaWCFmethodcall,itisalways possible to access the current request context through theOperationContext.Current or WebOperationContext.Current (for REST webservices) properties. So, we need to write one implementation of the TenantIdentificationstrategythatknowshowtofetchthisinformation:
CodeSample67
publicclassWcfHostHeaderTenantIdentification:ITenantIdentifierStrategy
{
publicStringGetCurrentTenant(RequestContextcontext)
{
varrequest=WebOperationContext.Current.IncomingRequest;
varhost=request.Headers[“Host”];
returnhost.Split(‘:’).First().ToLower();
}
}
Ontheotherside,ifASP.NETpipelineisanoption,wejustneedtoenableitthroughXMLconfiguration,throughtheaspNetCompatibilityEnabledattribute:
CodeSample68
system.serviceModel
serviceHostingEnvironmentaspNetCompatibilityEnabled=“true”/
/system.serviceModel
Or,wecanenableitthroughanattributeincode:
CodeSample69
[AspNetCompatibilityRequirements(
RequirementsMode=AspNetCompatibilityRequirementsMode.Required)]
publicclassDateService:IDateService
{
//restgoeshere
}
Note: For a good discussion of theASP.NETcompatibilitymode,checkoutthisarticle.
WebAPI
WebAPIASP.NETWebAPIisthenewAPIforwritingRESTwebserviceswiththe.NET
framework. It closely follows the ASP.NET MVC model, and is thus based oncontrollersandactions.
Currently, it canbehosted in either IIS (the traditionalway) or in anOWIN-supported host. In the first case, all of the classic ASP.NET APIs(System.Web.DLL)areavailable,includingHttpContext.Current,soyoucanusethesamestrategyimplementationsthatweredescribedforMVCandWebForms.ForOWIN,readChapter8forsometips.
UnitTesting
UnitTestingWhenever the ASP.NET classic APIs are available, we can follow the
prescriptiondescribedintheUnitTestingsectionofChapter5.ForOWIN,jumptoChapter8.
Chapter7Routing
Chapter7RoutingIntroductionRouting is a powerful mechanism for managing the URLs of your web
applications. It allows us to have nice, friendly URLs that can provide a usefulinsight on what they aremeant for. Other than that, it also offers security andextensibilityfeaturesthatshouldn’tbediscarded.
Routing
RoutingRoutingisanessentialpartofMVC,butwasactuallyintroducedtoWebForms
inASP.NET3.5SP1.ItdependsonanASP.NETmodule(UrlRoutingModule)thatis configured in theglobalWeb.config file, sowenormally don’t need toworryaboutit,andalsoonaroutingtable,whichisconfiguredthroughcode.Wewon’tbe looking extensively at ASP.NET routing, but we will explore two of itsextensibilitypoints:routehandlersandrouteconstraints.
RoutehandlersA route handler is a class that is actually responsible for processing the
request, and consists of an implementation of the IRouteHandler interface.ASP.NETincludesoneimplementationforMVC(MvcRouteHandler),butyoucaneasily rolloutyourown forWebForms.So,what is ituseful for?Well,youcanaddyourowncodelogictoitandrestassuredthat itwillonlyapplytotheroutespecified.Let’slookatanexample:
CodeSample70
varcustomRoute=newRoute(“{controller}/{action}/{id}”,
newRouteValueDictionary(new{controller=“Home”,action=“Index”,
id=UrlParameter.Optional}),newMyRouteHandler());
RouteTable.Routes.Add(customRoute);
We are registering an ordinary route, with the sole difference that we arepassingacustom routehandler,MyRouteHandler, a sample implementationofwhichispresentednext:
CodeSample71
classMyRouteHandler:MvcRouteHandler
{
protectedoverrideIHttpHandlerGetHttpHandler(RequestContext
requestContext)
{
vartenant=TenantsConfiguration.GetCurrentTenant();
//dosomethingwiththecurrenttenant
returnbase.GetHttpHandler(requestContext);
}
}
This handler will apply to all requests matching the given URL template of“{controller}/{action}/{id}”, regardless of the current tenant. By calling its baseimplementation,everythingwillworkasexpected.Wecan,however, restrictourroutesbasedonsomecondition—enterrouteconstraints.
Beforewegotothat,itisimportanttonoticethattheseexamplesareforMVC,butwecandosomethingsimilar forWebForms; theonlydifference is thebaseclass,PageRouteHandler,inthiscase:
CodeSample72
classPageRouteHandler:PageRouteHandler
{
publicWebFormsRouteHandler(StringvirtualPath):base(virtualPath){}
publicoverrideIHttpHandlerGetHttpHandler(RequestContext
requestContext)
{
vartenant=TenantsConfiguration.GetCurrentTenant();
//dosomethingwiththecurrenttenant
returnbase.GetHttpHandler(requestContext);
}
}
Also,registrationisslightlydifferent:
CodeSample73
varcustomRoute=newRoute(String.Empty,newWebFormsRouteHandler
(“~/Default.aspx”));
RouteTable.Routes.Add(customRoute);
RouteconstraintsA route constraint needs to implement the framework interface
IRouteConstraint. When such an implementation is applied to a route, theUrlRoutingModule will check first with it to see if the current request can bematchedtoaroute.Here’showwecanspecifyone(ormore)routeconstraints:
CodeSample74
varcustomRoute=newRoute(“{controller}/{action}/{id}”,
newRouteValueDictionary(new{controller=“Home”,action=“Index”,
id=UrlParameter.Optional}),newRouteValueDictionary(
newDictionaryString,Object{{“my”,newMyRouteConstraint()}}),
newMyRouteHandler());
RouteTable.Routes.Add(customRoute);
And,finally,aconstraintexamplethattakesintoaccountthecurrenttenant:
CodeSample75
classMyRouteConstraint:IRouteConstraint
{
publicBooleanMatch(HttpContextBasehttpContext,Routeroute,
StringparameterName,RouteValueDictionaryvalues,
RouteDirectionrouteDirection)
{
vartenant=TenantsConfiguration.GetCurrentTenant();
//dosomecheckwiththecurrenttenantandreturntrueorfalse
returntrue;
}
}
IISRewriteModule
IISRewriteModuleAnotheroptionforrestrictingaccessbasedonthecurrenttenantliesintheIIS
RewriteModule.ThismoduleisafreedownloadfromMicrosoft,anditallowsusto specify rules for controlling access and redirecting requests, all from theWeb.configfile.Essentially,arewriteruleconsistsof:
Figure14:IISRewriteModulerulecomponents
Theruleelementdefinesthename,iftheruleshouldbeenabledornot,andthe pattern matching syntax used in all fuzzy expression attributes(patternSyntax),whichcanbe:
Wildcard:Noregularexpression,butjustwildcard(*)matchingECMAScript:ECMAScriptstandardregularexpressionsExactMatch:Anexactmatch
CodeSample76
rewrite
rules
ruleenabled=“true”patternSyntax=“ECMAScript”
name=“ABC.COMonly”
!—restgoeshere—
/rule
/rules
/rewrite
Then there’s thematch element. In itwespecifyaURLpattern (url),whichcan be a specific page, if thematch should be done in a case-insensitiveway
(ignoreCase), and even if the matching should be reverted (negate), that is,acceptURLsthatdon’tmatchthepattern:
CodeSample77
rewrite
rules
ruleenabled=“true”patternSyntax=“ECMAScript”
name=“ABC.COMonly”
matchurl=”/abc.com/.*”ignoreCase=“true”/
!—restgoeshere—
/rule
/rules
/rewrite
Wecanthenaddanynumberofconditions; thesearenormallyconstructedby checking Server Variables (input) against one of the following match types(matchType):
Pattern:patterns,thedefaultIsFile:checkingifafileexistsIsDirectory:checkingifadirectoryexists
Conditionscanbeevaluateddifferently(logicalGrouping):
MatchAll:Allrulesneedtobeevaluatedpositively(thedefault).MatchAny:Atleastoneruleispositive.
Conditionscanalsobenegated,asshowinthisexample:CodeSample78
rewrite
rules
ruleenabled=“true”patternSyntax=“ECMAScript”
name=“ABC.COMonly”
matchurl=”/abc.com/.*”ignoreCase=“true”/
conditions
addinput=”{HTTP_HOST}”matchType=“Pattern”
pattern=“abc.com”negate=“true”/
/conditions
!—restgoeshere—
/rule
/rules
/rewrite
Server variables are more or less standard across web servers; they aredefinedinRFC3875,andinclude:
HTTP_HOST:theserverhostname,assentintherequestHTTP_REFERER:theURLthatthebrowsercamefromHTTP_USER_AGENT:thebrowserbeingusedtoaccessthepageHTTPS:whethertherequestisusingHTTPSornotPATH_INFO:apathfollowingtheactualserverresourceQUERY_STRING:thequerystringoftherequestREMOTE_ADDR:theclient’saddressREMOTE_HOST:theclient’shostnameREMOTE_USER:theauthenticateduser
Finally,wespecifyanactiontobetriggeredifthematchandconditionsaresuccessfullyevaluated.Anactioncanbeoneof:
AbortRequest:stopsthecurrentrequest;usefulfordenyingaccessNone:doesnotdoanythingRewrite:internallyrewritestherequesttosomethingdifferentRedirect:redirectsthebrowsertoadifferentresourceCustomResponse:sendscustomHTTPheaders,astatuscode,andsubcode
This example denies access to everyone except the abc.com tenant toeverythingunder/abc.com:
CodeSample79
rewrite
rules
ruleenabled=“true”patternSyntax=“ECMAScript”
name=“ABC.COMonly”
matchurl=”/abc.com/.*”ignoreCase=“true”/
conditions
addinput=”{HTTP_HOST}”matchType=“Pattern”
pattern=“abc.com”negate=“true”/
/conditions
actiontype=“AbortRequest”statusCode=“401”
statusDescription=“DeniedforothersthanABC.COM”/
/rule
/rules
/rewrite
Finally,wecanspecifyoneormoreservervariables(serverVariables).Thesecan be used either for being referenced in conditions, or to pass some kind ofinformationtothehandlersthatwillbeprocessingtherequest.Forexample,ifweweretocapturethetenantusingtheHostHeaderStrategy,wecouldhave:
CodeSample80
rewrite
rules
ruleenabled=“true”patternSyntax=“ECMAScript”
name=“ABC.COMonly”
serverVariables
setname=“TENANT”value=”{HTTP_HOST}”/
/serverVariables
matchurl=”/abc.com/.*”ignoreCase=“true”/
conditions
addinput=”{TENANT}”matchType=“Pattern”
pattern=“abc.com”negate=“true”/
/conditions
actiontype=“AbortRequest”statusCode=“401”
statusDescription=“DeniedforothersthanABC.COM”/
/rule
/rules
/rewrite
TheIISRewriteModulealsofeaturesacapturemechanismthatallowsustoretrievepartsofmatchingregularexpressions.Forexample,ifweweretousetheQueryStringStrategyfordeterminingthetenant,wecoulduse:
CodeSample81
conditions
addinput=”{QUERY_STRING}”pattern=“amp;?tenant=(.+)”/
!—thetenantpassedinthequerystringwillbein{C:1}—
/conditions
actiontype=“Rewrite”url=”/tenant/{C:1}”/
Wewoulduse{R:n}forreferencescapturedinthematchelementand{C:n}forthosecapturedinconditions,where0wouldreturnthewholematchingstringand 1, …, n, each of the captured elements, inside “()”. This simple exampleredirectsall requests for “tenant=something” to“/tenant/something”. It canbeuseful,forexample,ifyouwanttoredirectallrequestsfromacertaintenantfromone resource to another, namely, for resources that shouldn’t be accessed bysomecustomer.
Chapter8OWIN
Chapter8OWINIntroductionThe Open Web Interface for .NET (OWIN) is a Microsoft specification that
definesacontractfor.NETapplicationstointeractwithwebservers.Thisincludesthe pipeline for execution ofHTTP requests and contracts for services to sit inbetween(middleware).ItalsohappenstobethebasisfortheupcomingASP.NET5.
OWINallowsyoutodecoupleyourserviceapplicationfromanyparticularwebserver,suchasIIS.AlthoughitcanuseIIS,itcanalsobeself-hosted(likeWCF)throughOWIN’swebserverimplementation,OwinHttpListener.
You will need to add a reference to the Microsoft.Owin.Host.HttpListenerNuGetpackage:
Figure15:InstallingtheHttpListenerNuGetpackage
Why should we care with OWIN? Well, for once, it gives you a consistentpipelinethatismorealignedwiththewayASP.NETwillbeinthenearfuture:nomoreWebForms—MVCwillbeconfiguredthroughOWIN!
Note: For a good introduction toOWIN, check out the book OWIN Succinctly, by Ugo Lattanzi and SimoneChiaretta,alsointheSuccinctlycollection.
Registeringservices
RegisteringservicesTheOWINpipeline startswitha call to amethodcalledConfiguration of a
classcalledStartup.Thisisbyconvention;thereisnobaseclassorinterfacethatdictates this, and both the class andmethod can even be static. If wewant tochangethisbootstrapmethod,wecandosobyaddinganOwinStartupAttributeattheassemblylevelora“owin:appStartup”entrytotheWeb.config’sappSettingssection.Hereweregisteramiddlewarecomponent that inreturn,will registerallourservices(liketenantidentificationandregistration):
CodeSample82
publicstaticclassStartup
{
publicstaticvoidConfiguration(IAppBuilderbuilder)
{
builder.UseStaticFiles();
builder.UseDefaultFiles();
//restgoeshere
builder.UseMultitenancyMiddleware();
//restgoeshere
}
}
ThemiddlewareclassinheritsfromOwinMiddleware—thereareotheroptions,butthisismyfavoriteone.Amiddlewareclassreceivesinitsconstructorapointerto the next middleware in the pipeline, and should return the result of itsinvocationinitsInvokemethod.
Anexample:
CodeSample83
publicclassMultitenancyMiddleware:OwinMiddleware
{
publicMultitenancyMiddleware(OwinMiddlewarenext):base(next){}
publicoverrideTaskInvoke(IOwinContextcontext)
{
//servicesregistration
context.SetITenantLocationStrategy(
typeof(ITenantLocationStrategy).FullName,
newMefTenantLocationStrategy(
typeof(Common.ContextRepository).Assembly));
context.SetITenantIdentifierStrategy(
typeof(ITenantIdentifierStrategy).FullName,
newHostHeaderTenantIdentifierStrategy());
context.SetIConfiguration(
typeof(IConfiguration).FullName,
newAppSettingsConfiguration());
//restgoeshere
returnthis.Next.Invoke(context);
}
}
Note:Rightnow,youcannotuseOWINwith ASP.NET MVC or Web Forms, because these depend on theSystem.Web.DLL,thecoreoftheASP.NETframework,butASP.NETMVC6willworkontopofOWIN.
So, if you are to useOWIN, and you need to resolve one of our bootstrapservices, you can either stick with your choice of IoC container and CommonServiceLocator,oryoucanuseOWIN’sinternalimplementation:
CodeSample84
publicoverrideTaskInvoke(IOwinContextcontext)
{
vartls=context.GetITenantLocationStrategy(
typeof(ITenantLocationStrategy).FullName);
returnthis.Next.Invoke(context);
}
OWIN also supports ASP.NET Identity for authentication. Just add NuGetpackage Microsoft.AspNet.Identity.Owin and make any changes to the added
classes:
Figure16:ASP.NETIdentitypackageforOWIN
WebAPI
WebAPIUnlikeMVC,WebAPIcanbeusedwithOWIN.Youwill need to registeran
implementation, such as the one provided by the Microsoft NuGet packageMicrosoft.AspNet.WebApi.Owin:
Figure17:WebAPIOWINpackage
Then you will need to register the Web API service in the aforementionedConfigurationmethod,usingtheUseWebApiextensionmethod:
CodeSample85
publicstaticclassStartup
{
publicstaticvoidConfiguration(IAppBuilderbuilder)
{
builder.UseWebApi(newHttpConfiguration());
//restgoeshere
}
}
ReplacingSystem.Web
ReplacingSystem.WebOWIN came into being because of the desire to replace the System.Web
architecture.OnekeyclassinSystem.WebisHttpContext,whichnolongerexistsinOWIN.Thisposesaproblem:ourinterfacesthatwereintroducedinChapter3,namely,ITenantIdentifierStrategy,relyonclassesthatarepartofSystem.Web,sowe need to figure out howwe can achieve the same resultswithout it. TheOWINpipelineissignificantlysimplerthanASP.NET;itfollowsapatternknownasChainofResponsibility,whereeachstage(calledmiddleware)inthepipelinecallsthenextone,eventuallydoingsomethingbeforeandafterthat.So,ifwewanttomakeavailablesomeservicestotheother,weneedtodoitinthefirststage:
CodeSample86
publicclassOwinHostHeaderTenantIdentifierStrategy:OwinMiddleware
{
publicOwinHostHeaderTenantIdentifierStrategy(OwinMiddlewarenext):
base(next){}
publicoverrideTaskInvoke(IOwinContextcontext)
{
context.Request.Environment[“Tenant”] = TenantsConfiguration.GetTenants().Single(x = x.Name ==context.Request.Host.Value.Split(‘:’)
.First().ToLower());
returnthis.Next.Invoke(context);
}
}
The equivalent of the HttpContext in OWIN is the implementation of theIOwinContext interface: it hasRequest andResponseobjects, plus a couple ofotherusefulmethodsandproperties. In thisexample,wearestoringthecurrenttenant, as identified by the host header, in an environment variable (theEnvironmentcollection).OWINdoesnotdictatehowtodoit,sopleasefeelfreetodoitanywayyouprefer.Theimportantthingisthatthismiddlewareneedstobeinsertedonthepipelinebeforeanythingelsethatmightneedit:
CodeSample87
publicstaticclassStartup
{
publicstaticvoidConfiguration(IAppBuilderbuilder)
{
builder.UseWebApi(newHttpConfiguration());
builder.UseStaticFiles();
builder.UseDefaultFiles();
//restgoeshere
builder.UseMultitenancyMiddleware();
builder.UseOwinHostHeaderTenantIdentifierStrategy();
//restgoeshere
}
}
Unittesting
UnittestingIn order to unit test an OWIN setup, we may need to inject a couple of
properties, such as the current host, so that it can be caught by ourimplementationoftheHostHeaderStrategy:
CodeSample88
publicstaticvoidSetup(thisIOwinContextcontext)
{
context.Request.Host=newHostString(“abc.com”);
}
Otherstrategiesdon’tneedanyparticularsetup.
Chapter9ApplicationServices
Chapter9ApplicationServicesIntroductionAny application that does something at least slightly elaborate depends on
someservices.Callthemcommonconcerns,applicationservices,middleware,orwhatever.Thechallengehere is thatwecannot just use theseserviceswithoutproviding them with some context, namely, the current tenant. For example,consider cache: you probably wouldn’t want something cached for a specifictenant tobeaccessiblebyother tenants.Herewewill seesome techniques forpreventing this by leveraging what ASP.NET and the .NET framework alreadyoffer.
InversionofControl
InversionofControlThroughout the code, I have been using the Common Service Locator to
retrieveservicesregardlessoftheInversionofControlcontainerusedtoactuallyregister them,but in theend,weareusingUnity.Whenwedoactually registerthem,wehaveacoupleofoptions:
RegisterstaticinstancesRegisterclassimplementationsRegisterinjectionfactories
For those services that need context (knowing the current tenant), injectionfactoriesareourfriends,becausethisinformationcanonlybeobtainedwhentheservice isactually requested.UsingUnity,we register themusingcodesuchasthis:
CodeSample89
varcontainer=UnityConfig.GetConfiguredContainer();
container.RegisterTypeIContextfulService(newPerRequestLifetimeManager(),
newInjectionFactory(x=
{
vartenant=TenantsConfiguration.GetCurrentTenant();
returnnewContextfulServiceImpl(tenant.Name);
}));
Noteworthy:
ThePerRequestLifetimeManagerisaUnitylifetimemanagerimplementationthatprovidesaper-requestlifetimeforregisteredcomponents,meaningcomponentswillonlybecreatedinthescopeofthecurrentHTTPrequestiftheyweren’talreadycreated.Otherwise,thesameinstancewillalwaysbereturned.Disposablecomponentsaredealtwithappropriatelyattheendoftherequest.InjectionFactorytakesadelegatethatjustreturnsapre-createdinstance.Inthisexample,wearebuildingabogusserviceimplementation,ContextfulServiceImpl,whichtakesaconstructorparameterthatisthecurrenttenantname.
Tip:Itdoesn’tmakemuchsenseusingInjectionFactorywithlifetimemanagersotherthanPerRequestLifetimeManager.
Note: Like I said previously, youarenottiedtoUnity—youcanusewhateverIoCcontaineryoulike,providedthat(forthepurposeoftheexamplesinthisbook)itoffersanadapterfortheCommonServiceLocator.
Remember the interface that represents a tenant’s configuration,ITenantConfiguration? If you do, you know that it features a general-purpose,indexedcollection,Properties.Wecanimplementacustomlifetimemanagerthatstoresitemsinthecurrenttenant’sPropertiescollectioninatransparentway:
CodeSample90
publicsealedclassPerTenantLifetimeManager:LifetimeManager
{
privatereadonlyGuidid=Guid.NewGuid();
privatestaticreadonlyConcurrentDictionaryString,
ConcurrentDictionaryGuid,Objectitems=
newConcurrentDictionaryString,
ConcurrentDictionaryGuid,Object();
publicoverrideObjectGetValue()
{
vartenant=TenantsConfiguration.GetCurrentTenant();
ConcurrentDictionaryGuid,Objectregistrations=null;
Objectvalue=null;
if(items.TryGetValue(tenant.Name,outregistrations))
{
registrations.TryGetValue(this.id,outvalue);
}
returnvalue;
}
publicoverridevoidRemoveValue()
{
vartenant=TenantsConfiguration.GetCurrentTenant();
ConcurrentDictionaryGuid,Objectregistrations=null;
if(items.TryGetValue(tenant.Name,outregistrations))
{
Objectvalue;
registrations.TryRemove(this.id,outvalue);
}
}
publicoverridevoidSetValue(ObjectnewValue)
{
vartenant=TenantsConfiguration.GetCurrentTenant();
varregistrations=items.GetOrAdd(tenant.Name,
newConcurrentDictionaryGuid,Object());
registrations[this.id]=newValue;
}
}
Caching
CachingCaching is one of those techniques that can dramatically improve the
performanceofanapplication,becauseitkeepsinmemorydatathatispotentiallycostlytoacquire.Weneedtobeabletostoredataagainstakey,whereseveraltenantscanuse thesamekey.The trickhere is to,behind thescenes, join thesupplied key with a tenant-specific identifier. For instance, a typical cachingserviceinterfacemightbe:
CodeSample91
publicinterfaceICache
{
voidRemove(Stringkey,StringregionName=null);
ObjectGet(Stringkey,StringregionName=null);
voidAdd(Stringkey,Objectvalue,DateTimeabsoluteExpiration,
StringregionName=null);
voidAdd(Stringkey,Objectvalue,TimeSpanslidingExpiration,
StringregionName=null);
}
AndanimplementationusingtheASP.NETbuilt-incache:
CodeSample92
publicsealedclassAspNetMultitenantCache:ICache,ITenantAwareService
{
publicstaticreadonlyICacheInstance=newAspNetMultitenantCache();
privateAspNetMultitenantCache()
{
//privateconstructorsincethisismeanttobeusedasasingleton
}
privateStringGetTenantKey(Stringkey,StringregionName)
{
vartenant=TenantsConfiguration.GetCurrentTenant().Name;
key=String.Concat(tenant,”:”,regionName,”:”,key);
returnkey;
}
publicvoidRemove(Stringkey,StringregionName=null)
{
HttpContext.Current.Cache.Remove(this.GetTenantKey(key,
regionName));
}
publicObjectGet(Stringkey,StringregionName=null)
{
returnHttpContext.Current.Cache.Get(this.GetTenantKey(key,
regionName));
}
publicvoidAdd(Stringkey,Objectvalue,DateTimeabsoluteExpiration,
StringregionName=null)
{
HttpContext.Current.Cache.Add(this.GetTenantKey(key,regionName),
value,null,absoluteExpiration,Cache.NoSlidingExpiration,
CacheItemPriority.Default,null);
}
publicvoidAdd(Stringkey,Objectvalue,TimeSpanslidingExpiration,
StringregionName=null)
{
HttpContext.Current.Cache.Add(this.GetTenantKey(key,regionName),
value,null,Cache.NoAbsoluteExpiration,slidingExpiration,
CacheItemPriority.Default,null);
}
}
Note: Do not worry aboutITenantAwareService; it is just a marker interface I use for those services thatknowaboutthecurrenttenant.
Nothing too fancyhere—we justwrap theuser-suppliedkeywith thecurrenttenant’sIDandregionname.Thisisimplementedasasingleton,becausethere’sonly one ASP.NET cache, and there’s no point in having multiple instances ofAspNetMultitenantCache,withoutanystateandallpointingtothesamecache.That’s what the ITenantAwareService interface states: it knows who we areaddressing,sothere’snoneedtohavedifferentinstancespertenant.
Note:Notice theAspNet prefix on theclassname.ItisanindicationthatthisclassrequiresASP.NETtowork.
Registering the cache service is easy, and we don’t need to useInjectionFactory,sincewewillberegisteringasingleton:
CodeSample93
container.RegisterInstanceICache(AspNetMultitenantCache.Instance);
Configuration
ConfigurationI am not aware of any mid-sized application that does not require some
configurationofanykind.Theproblemhereisthesame:twotenantsmightsharethesameconfigurationkeys,whileat thesametimetheywouldexpectdifferentvalues.Let’sdefineacommoninterfaceforthebasicconfigurationfeatures:
CodeSample94
publicinterfaceIConfiguration
{
ObjectGetValue(Stringkey);
voidSetValue(Stringkey,Objectvalue);
ObjectRemoveValue(Stringkey);
}
Let’salsodefineamultitenantimplementationthatusesappSettingsinaper-tenantconfigurationfileasthebackingstore:
CodeSample95
publicclassAppSettingsConfiguration:IConfiguration,ITenantAwareService
{
publicstaticreadonlyIConfigurationInstance=
newAppSettingsConfiguration();
privateAppSettingsConfiguration()
{
}
privatevoidPersist(Configurationconfiguration)
{
configuration.Save();
}
privateConfigurationConfiguration
{
get
{
vartenant=TenantsConfiguration.GetCurrentTenant();
varconfigMap=newExeConfigurationFileMap();
configMap.ExeConfigFilename = String.Format( AppDomain.CurrentDomain.BaseDirectory,
tenant.Name,”.config”);
varconfiguration=ConfigurationManager
.OpenMappedExeConfiguration(configMap,
ConfigurationUserLevel.None);
returnconfiguration;
}
}
publicObjectGetValue(Stringkey)
{
varentry=this.Configuration.AppSettings.Settings[key];
return(entry!=null)?entry.Value:null;
}
publicvoidSetValue(Stringkey,Objectvalue)
{
if(value==null)
{
this.RemoveValue(key);
}
else
{
varconfiguration=this.Configuration;
configuration.AppSettings.Settings
.Add(key,value.ToString());
this.Persist(configuration);
}
}
publicObjectRemoveValue(Stringkey)
{
varconfiguration=this.Configuration;
varentry=configuration.AppSettings.Settings[key];
if(entry!=null)
{
configuration.AppSettings.Settings.Remove(key);
this.Persist(configuration);
returnentry.Value;
}
returnnull;
}
}
Note:Thisclassistotallyagnosticastoweb or non-web; it can be used in any kind of project. It also implementsITenantAwareService,forthesamereasonasbefore.
With this implementation, we can have one file per tenant, say,abc.com.config,orxyz.net.config,andthesyntaxisgoingtobeidenticaltothatoftheordinary.NETconfigurationfiles:
CodeSample96
configuration
appSettings
addkey=“Key”value=“Value”/
/appSettings
configuration
Thisapproach isnicebecausewecanevenchangethefilesatruntime,andwe won’t cause the ASP.NET application to restart, which would happen if weweretochangetheWeb.configfile.
We’llusethesamepatternforregistration:
CodeSample97
container.RegisterInstanceIConfiguration(AppSettingsConfiguration.Instance);
Logging
LoggingIt would be cumbersome and rather silly to implement another logging
framework,whentherearesomanygoodonestochoosefrom.For thisbook, Ihavechosen theEnterpriseLibraryLoggingApplicationBlock (ELLAB).YouwillprobablywanttoaddsupportforitthroughNuGet:
Figure18:InstallingtheEnterpriseLibraryLoggingApplicationBlock
ELLAB offers a single API that you can use to send logs to a number ofsources,asoutlinedinthefollowingpicture:
Figure19:EnterpriseLibraryLoggingApplicationBlockarchitecture
Thesesourcesinclude:
Textfiles(RollingFlatFileTraceListener,FlatFileTraceListener,XmlTraceListener)WindowsEventLog(FormattedEventLogTraceListener)MSMQ(MsmqEventTraceListener)Email(EmailTraceListener)Database(FormattedDatabaseTraceListener)WMI(WmiTraceListener)
Now,ourrequirementistosendtheoutputforeachtenanttoitsownfile.Forexample, output for tenant “abc.com”will go to “abc.com.log”, and so on.Butfirstthingsfirst—here’sourbasiccontractforlogging:
CodeSample98
publicinterfaceILog
{
voidWrite(Objectmessage,Int32priority,TraceEventTypeseverity,
Int32eventId=0);
}
I kept it simple, but, by all means, do add any auxiliary methods you findappropriate.
TheELLABcanlog:
AnarbitrarymessageAstringcategory:wewilluseitforthetenantname,sowewillnotexposeitintheAPIAnintegerpriorityTheeventseverityAneventidentifier,whichisoptional(eventId)
TheimplementationofitontopoftheELLABcouldbe:
CodeSample99
publicsealedclassEntLibLog:ILog,ITenantAwareService
{
publicstaticreadonlyILogInstance=newEntLibLog();
privateEntLibLog()
{
//privateconstructorsincethisismeanttobeusedasasingleton
}
publicvoidWrite(Objectmessage,Int32priority,TraceEventTypeseverity,Int32eventId=0)
{
vartenant=TenantsConfiguration.GetCurrentTenant();
Logger.Write(message,tenant.Name,priority,eventId,severity);
}
}
Theregistrationfollowsthesamepatternasbefore:
CodeSample100
container.RegisterInstanceILog(EntLibLog.Instance);
Thistime,wealsoneedtoaccountfortheELLABconfiguration.Becauseweneed to setupeach tenant individually,wehave to run the following codeuponstartup:
CodeSample101
privatevoidCreateLogFactories(IEnumerableITenantConfigurationtenants)
{
foreach(vartenantintenants)
{
try
{
varconfigurationSource=
newFileConfigurationSource(tenant.Name+
“.config”);
varlogWriterFactory=newLogWriterFactory(
configurationSource);
Logger.SetLogWriter(logWriterFactory.Create());
}
catch{}
}
}
vartenants=TenantsConfiguration.GetTenants();
CreateLogFactories(tenants);
Tip:Thetry…catchblocksurroundingtheinitializationcodeistheresothatifwedonotwanttohaveaconfigurationfileforaspecifictenant,itdoesn’tthrow.
TheTenantsConfiguration.GetTenantsmethodwasintroducedinchapteronFinding Tenants. This code, specifically the FileConfigurationSource class,requiresthatalltenantshavetheirconfigurationinanindividualfile,namedafterthe tenant (tenant.config). Next is an example of a logging configuration that
usesarollingflatfilethatrotateseveryweek:
CodeSample102
configuration
configSections
sectionname=“loggingConfiguration”
type=“Microsoft.Practices.EnterpriseLibrary.Logging.
Configuration.LoggingSettings,Microsoft.Practices.EnterpriseLibrary.Logging”/
/configSections
loggingConfigurationname=“loggingConfiguration”tracingEnabled=“true”
defaultCategory=””logWarningsWhenNoCategoriesMatch=“true”
listeners
addname=“RollingFlatFileTraceListener”
type=“Microsoft.Practices.EnterpriseLibrary
.Logging.TraceListeners.RollingFlatFileTraceListener,
Microsoft.Practices.EnterpriseLibrary.Logging” listenerDataType=“Microsoft.Practices.EnterpriseLibrary
.Logging.Configuration.RollingFlatFileTraceListenerData,
Microsoft.Practices.EnterpriseLibrary.Logging”
fileName=“abc.com.log”
footer=”–––––––––”
formatter=“TextFormatter”
header=”–––––––––”
rollFileExistsBehavior=“Increment”
rollInterval=“Week”
timeStampPattern=“yyyy-MM-ddhh:mm:ss”
traceOutputOptions=“LogicalOperationStack,DateTime,
Timestamp,ProcessId,ThreadId,Callstack”
filter=“All”/
/listeners
formatters
addtype=“Microsoft.Practices.EnterpriseLibrary.Logging.
Formatters.TextFormatter,
Microsoft.Practices.EnterpriseLibrary.Logging”
template=“Timestamp:{timestamp}#xA;
Message:{message}#xA;Category:{category}#xA;
Priority:{priority}#xA;EventId:{eventid}#xA;
Severity:{severity}#xA;Title:{title}#xA;
Machine:{machine}#xA;ProcessId:{processId}#xA;
ProcessName:{processName}#xA;”
name=“TextFormatter”/
/formatters
categorySources
addswitchValue=“All”name=“General”
listeners
addname=
“RollingFlatFileTraceListener”/
/listeners
/add
/categorySources
specialSources
allEventsswitchValue=“All”name=“AllEvents”
listeners
addname=
“RollingFlatFileTraceListener”/
/listeners
/allEvents
notProcessedswitchValue=“All”
name=“UnprocessedCategory”
listeners
addname=
“RollingFlatFileTraceListener”/
/listeners
/notProcessed
errorsswitchValue=“All”
name=“LoggingErrorsamp;Warnings”
listeners
addname=
“RollingFlatFileTraceListener”/
/listeners
/errors
/specialSources
/loggingConfiguration
/configuration
Note: For more information on the Enterprise Library Logging ApplicationBlock,checkouttheEnterpriseLibrarysite.Forloggingtoadatabase,youneedtoinstalltheLoggingApplicationBlockDatabaseProviderNuGetpackage.
Monitoring
MonitoringThe classic APIs offered by ASP.NET and .NET applications for diagnostics
andmonitoringdonotplaywellwithmultitenancy,namely:
TracingPerformancecountersWebevents(HealthMonitoringProvider)
In this chapter, we will look at some techniques for making these APIsmultitenant.Beforethat,let’sdefineaunifyingcontractforallthesedifferentAPIs:
CodeSample103
publicinterfaceIDiagnostics
{ Int64IncrementCounterInstance(StringinstanceName,Int32value=1);
GuidRaiseWebEvent(Int32eventCode,Stringmessage,Objectdata,
Int32eventDetailCode=0);
voidTrace(Objectvalue,Stringcategory);
}
Anexplanationofeachofthesemethodsisrequired:
Trace:writestotheregisteredtracelistenersIncrementCounterInstance:incrementsaperformancecounterspecifictothecurrenttenantRaiseWebEvent:raisesaWebEventintheASP.NETHealthMonitoringinfrastructure
Inthefollowingsections,we’llseeeachinmoredetail.
TracingASP.NETTracingTheASP.NET tracing service is very useful for profiling your ASP.NETWeb
Formspages,anditcanevenhelpinsortingoutsomeproblems.
Beforeyoucanusetracing,itneedstobegloballyenabled,whichisdoneontheWeb.configfile,throughatraceelement:
CodeSample104
system.web
traceenabled=“true”localOnly=“true”writeToDiagnosticsTrace=“true”
pageOutput=“true”traceMode=“SortByTime”requestLimit=“20”/
/system.web
Whatthisdeclarationsaysis:
Tracingisenabled.Traceoutputisonlypresenttolocalusers(localOnly).Standarddiagnosticstracinglistenersarenotified(writeToDiagnosticsTrace).OutputissenttothebottomofeachpageinsteadofbeingdisplayedonthetracehandlerURL(pageOutput).Traceeventsaresortedbytheirtimestamp(traceMode).Upto20requestsarestored(requestLimit).
Youalsoneedtohaveitenabledonapage-by-pagebasis(thedefaultistobedisabled):
CodeSample105
%@PageLanguage=“C#”CodeBehind=“Default.aspx.cs”Inherits=“WebForms.Default”
Trace=“true”%
Wewon’tgointodetailsonASP.NETtracing.Instead,let’sseeasampletrace,forASP.NETWebForms:
Figure20:PagetraceforWebForms
InMVC, theonlydifference is that theControlTree table isempty,which isobvious,onceyouthinkofit.
This trace isbeingdisplayedon thepage itself,asdictatedbypageOutput;the other option is to have traces only showing on the trace handler page,Trace.axd,andleavethepagealone.Eitherway,theoutputisthesame.
A tracingentrycorresponds toa requesthandledby theserver.Wecanaddourown tracemessages to theentrybycallingoneof the tracemethods in the
TraceContextclass,convenientlyavailableasPage.TraceinWebForms:
CodeSample106
protectedoverridevoidOnLoad(EventArgse)
{
this.Trace.Write(“OnPage.Load”);
base.OnLoad(e);
}
Orasstaticmethodsof theTraceclass (forMVCor ingeneral),whichevenhasanoverloadforpassingacondition:
CodeSample107
publicActionResultIndex()
{
Trace.WriteLine(“Beforepresentingaview”);
vartenant=TenantsConfiguration.GetCurrentTenant();
Trace.WriteLineIf(tenant.Name!=“abc.com”,“Notabc.com”);
returnthis.View();
}
Now,thetracingproviderkeepsanumberoftraces,uptothevaluespecifiedbyrequestLimit—thedefault being 10, and themaximum, 10,000. Thismeansthatrequestsforallourtenantswillbehandledthesameway,soifwegototheTrace.axdURL,wehavenowayofknowingwhichtenantfortherequestwasfor.ButifyoulookcloselytotheMessagecolumnoftheTraceInformationtableinFigure 20, youwill notice a prefix that corresponds to the tenant for which therequest was issued. In order to have that, we need to register a customdiagnosticslistenerontheWeb.configfile,inasystem.diagnosticssection:
CodeSample108
system.diagnostics
traceautoflush=“true”
listeners
addname=“MultitenantTrace”
type=“WebForms.MultitenantTraceListener,
WebForms”/
/listeners
/trace
/system.diagnostics
ThecodeforMultitenantTraceListenerispresentedbelow:CodeSample109
publicsealedclassMultitenantTraceListener:WebPageTraceListener
{
privatestaticreadonlyMethodInfoGetDataMethod=typeof(TraceContext)
.GetMethod(“GetData”,BindingFlags.NonPublic|BindingFlags.Instance);
publicoverridevoidWriteLine(Stringmessage,Stringcategory)
{
vards=GetDataMethod.Invoke(HttpContext.Current.Trace,null)
asDataSet;
vardt=ds.Tables[“Trace_Trace_Information”];
vardr=dt.Rows[dt.Rows.Count-1];
vartenant=TenantsConfiguration.GetCurrentTenant();
dr[“Trace_Message”]=String.Concat(tenant.Name,”:”,
dr[“Trace_Message”]);
base.WriteLine(message,category);
}
}
Whatthisdoesis,withabitofreflectionmagic,getsareferencetothecurrentdataset containing the last traces, and, on the last of them—the one for thecurrent request—adds a prefix that is the current tenant’s name (for example,abc.com).
Web.config is not the only way by which a diagnostics listener can beregistered;there’salsocode:Trace.Listeners.Usingthismechanism,youcanaddcustomlistenersthatwilldoallkindsofthingswhenatracecallisissued:
CodeSample110
protectedvoidApplication_Start()
{
//unconditionallyaddingalistener
Trace.Listeners.Add(newCustomTraceListener());
}
protectedvoidApplication_BeginRequest()
{
//conditionallyaddingalistener
vartenant=TenantsConfiguration.GetCurrentTenant();
if(tenant.Name==“abc.com”)
{
Trace.Listeners.Add(newAbcComTraceListener());
}
}
TracingProvidersOthertracingprovidersexistinthe.NETbaseclasslibrary:
EventLogTraceListener:writestotheWindowsEventLogWebPageTraceListener:ASP.NETtracingIisTraceListener:IIStraceEventProviderTraceListener:EventTracingforWindows(ETW)ConsoleTraceListener:consoleDefaultTraceListener:outputwindowinVisualStudioTextWriterTraceListener:textfileDelimitedListTraceListener:textfilewithcustomfielddelimiterEventSchemaTraceListener:XMLtextfileXmlWriterTraceListener:XMLtextfileFileLogTraceListener:textfile
Allofthesecanberegisteredinsystem.diagnosticsorTrace.Listeners.Wewillhave awrapper class for the Trace staticmethods, inwhichwe implement theIDiagnosticsinterface’sTracemethod:
CodeSample111
publicsealedclassMultitenantDiagnostics:IDiagnostics,ITenantAwareService
{
publicvoidTrace(Objectvalue,Stringcategory)
{
vartenant=TenantsConfiguration.GetCurrentTenant();
System.Diagnostics.Trace.AutoFlush=true;
System.Diagnostics.Trace.WriteLine(String.Concat(tenant.Name,
“:“,value),category);
}
}
PerformanceCountersPerformance Counters are a Windows feature that can be used to provide
insightsonrunningapplicationsandservices.ItisevenpossibletohaveWindowsreactautomaticallytothesituationwhereaperformancecounter’svalueexceedsa given threshold. If we so desire, we can use performance counters tocommunicatetointerestedpartiesaspectsofthestateofourapplications,wherethisstateconsistsofintegervalues.
PerformanceCountersareorganizedin:
Categories:anameCounters:anameandatype(let’sforgetaboutthetypefornow)Instances:anameandavalueofagiventype(we’llassumealonginteger)
Figure21:Performancecountersbasicconcepts
IntheITenantConfigurationinterface,weaddedaCountersproperty,which,when implemented, is used to automatically create performance counterinstances.Wewillfollowthisapproach:
Table6:Mappingperformancecounterconceptsformultitenancy
Concept
Content
Category
“Tenants”
Counter
Thetenantname(eg,abc.comorxyz.net)Instance
Atenant-specificname,comingfromITenantConfiguration.CountersThecodeforautomaticallycreatingeachperformancecounterandinstancein
theTenantsConfigurationclassisasfollows:CodeSample112
publicsealedclassTenantsConfiguration:IDiagnostics,ITenantAwareService
{
privatestaticvoidCreatePerformanceCounters(
IEnumerableITenantConfigurationtenants)
{
if(PerformanceCounterCategory.Exists(“Tenants”))
{
PerformanceCounterCategory.Delete(“Tenants”);
}
varcounterCreationDataCollection=
newCounterCreationDataCollection(
tenants.Select(tenant=
newCounterCreationData(
tenant.Name,
String.Empty,
PerformanceCounterType.NumberOfItems32))
.ToArray());
varcategory=PerformanceCounterCategory.Create(“Tenants”,
“Tenantsperformancecounters”,
PerformanceCounterCategoryType.MultiInstance,
counterCreationDataCollection);
foreach(vartenantintenants)
{
foreach(varinstanceintenant.Counters)
{
varcounter=newPerformanceCounter(
category.CategoryName,
tenant.Name,
String.Concat(tenant.Name,
“:”,
instance.InstanceName),false);
}
}
}
}
Ontheotherside,thecodeforincrementingaperformancecounterinstanceisdefinedintheIDiagnostics interface(IncrementCounterInstance),andcanwecanimplementitas:
CodeSample113
public sealed class MultitenantDiagnostics :IDiagnostics,ITenantAwareService
{
publicInt64IncrementCounterInstance(StringinstanceName,
Int32value=1)
{
vartenant=TenantsConfiguration.GetCurrentTenant();
using(varpc=newPerformanceCounter(“Tenants”,tenant.Name,
String.Concat(tenant.Name,”:”,instanceName),false))
{
pc.RawValue+=value;
returnpc.RawValue;
}
}
}
When the application is running, we can observe in real-time the values ofcounterinstancesthroughthePerformanceMonitorapplication:
Figure22:PerformanceMonitorapplicationdisplayingperformancecounters
Wejustneedtoaddsomecounterstothedisplay;ourswillbeavailableunderTenants-tenantname-instancename:
Figure23:Addingperformancecounters
Inthisexample,itisperceivablethatbothtenants,abc.comandxyz.net,haveidenticallynamedcounterinstances,butitdoesn’thavetobethecase.
There are other built-in ASP.NET-related performance counters that can beusedtomonitoryourapplications.Forexample,inPerformanceMonitor,addanewcounterandselectASP.NETApplications:
Figure24:ASP.NETApplicationsperformancecounters
You will notice that you have several instances, one for each site that isrunning. These instances are automatically named, but each number can betraced to an application. For instance, if you are using IIS Express, open theApplicationHost.config file (C:UsersusernameDocumentsIISExpressConfig)andgo to thesites section,where youwill find something like this (in case, ofcourse,youareservingtenantsabc.comandxyz.net):
CodeSample114
sites
sitename=“abc.com”id=“1”
applicationpath=”/”applicationPool=“Clr4IntegratedAppPool”
virtualDirectorypath=”/”
physicalPath=“C:InetPubMultitenant”/
/application
bindings
bindingprotocol=“http”bindingInformation=”*:80:abc.com”/
/bindings
/site
sitename=“xyz.net”id=“2”
applicationpath=”/”applicationPool=“Clr4IntegratedAppPool”
virtualDirectorypath=”/”
physicalPath=“C:InetPubMultitenant”/
/application
bindings
bindingprotocol=“http”bindingInformation=”*:80:xyz.net”/
/bindings
/site
/sites
OrusingIIS:
Figure25:Creatingaseparatesiteforabc.com
Figure26:Creatingaseparatesiteforxyz.net
Inordertohavemorecontrol,weseparatedthetwosites;thisisrequiredformoreaccuratelycontrollingeach.Thecodebaseremainsthesame,though.
In thenameofeach instance (_LM_W3SVC_n_ROOT),nwillmatchoneofthesenumbers.
If,on theotherhand,youwant tousethefull IIS, theappcmdcommandwillgiveyouthisinformation:
CodeSample115
C:WindowsSystem32inetsrvappcmdlistsite
SITE“abc.com”(id:1,bindings:http/abc.com:80:,state:Started)
SITE“xyz.net”(id:2,bindings:http/xyz.net:80:,state:Started)
C:WindowsSystem32inetsrvappcmdlistapppool
APPPOOL “DefaultAppPool”(MgdVersion:v4.0,MgdMode:Integrated,state:Started)
APPPOOL “Classic .NET AppPool”(MgdVersion:v2.0,MgdMode:Classic,state:Started)
APPPOOL “.NET v2.0 Classic”(MgdVersion:v2.0,MgdMode:Classic,state:Started)
APPPOOL“.NETv2.0”(MgdVersion:v2.0,MgdMode:Integrated,state:Started)
APPPOOL “.NET v4.5 Classic”(MgdVersion:v4.0,MgdMode:Classic,state:Started)
APPPOOL“.NETv4.5”(MgdVersion:v4.0,MgdMode:Integrated,state:Started)
APPPOOL“abc.com”(MgdVersion:v4.0,MgdMode:Integrated,state:Started)
APPPOOL“xyz.net”(MgdVersion:v4.0,MgdMode:Integrated,state:Started)
Iamlistingallsites(firstlist)andthenallapplicationpools.
HealthMonitoringTheHealthMonitoringfeatureofASP.NETenablestheconfigurationofrulesto
respond to certain events that take place in an ASP.NET application. It uses aprovided model to decide what to do when the rule conditions are met. Anexamplemightbesendingoutanotificationmailwhenthenumberoffailedloginattemptsreachesthreeinaminute’stime.So,verysimply,theHealthMonitoringfeatureallowsusto:
RegisteranumberofprovidersthatwillperformactionsAddnamedrulesboundtospecificprovidersMapeventcodestothecreatedrules
Thereareanumberofout-of-theboxeventsthatareraisedbytheASP.NETAPIsinternally,butwecandefineourownaswell:
Figure27:IncludedHealthMonitoringevents
Eventsaregroupedinclassesandsubclasses.Therearespecificclassesfordealing with authentication events (WebAuditEvent, WebFailureAuditEvent,WebSuccessAuditEvent), request (WebRequestEvent, WebRequestErrorEvent)and application lifetime events (WebApplicationLifetimeEvent), view state failureevents (WebViewStateFailureEvent), and others. Each of these events (andclasses) is assigned a unique numeric identifier, which is listed inWebEventCodes fields. Custom events should start with the value inWebEventCodes.WebExtendedBase+1.
A number of providers—for executing actionswhen rules aremet—are also
included, of course. We can implement our own too, by inheriting fromWebEventProvideroroneofitssubclasses:
Figure28:IncludedHealthMonitoringproviders
Includedproviderscoveranumberoftypicalscenarios:
Writingtoadatabase(SQLWebEventProvider)WritingtoWMI(WMIWebEventProvider)WritingtotheEventLog(EventLogWebEventProvider)Sendingmail(SimpleMailWebEventProvider)
Before we go into writing some rules, let’s look at our custom provider,MultitenantEventProvider, and its related classesMultitenantWebEvent andMultitenantEventArgs:
CodeSample116
publicsealedclassMultitenantEventProvider:WebEventProvider
{
privatestaticreadonlyIDictionaryString,MultiTenantEventProvider
providers=
newConcurrentDictionaryString,MultiTenantEventProvider();
privateconstStringTenantKey=“tenant”;
publicStringTenant{get;privateset;}
publiceventEventHandlerMultiTenantEventArgsEvent;
publicstaticvoidRegisterEvent(StringtenantId,
EventHandlerMultiTenantEventArgshandler)
{
varprovider=FindProvider(tenantId);
if(provider!=null)
{
provider.Event+=handler;
}
}
publicstaticMultiTenantEventProviderFindProvider(StringtenantId)
{
varprovider=nullasMultiTenantEventProvider;
providers.TryGetValue(tenantId,outprovider);
returnprovider;
}
publicoverridevoidInitialize(Stringname,NameValueCollectionconfig)
{
vartenant=config.Get(TenantKey);
if(String.IsNullOrWhiteSpace(tenant))
{
thrownewInvalidOperationException(
“Missingtenantname.”);
}
config.Remove(TenantKey);
this.Tenant=tenant;
providers[tenant]=this;
base.Initialize(name,config);
}
publicoverridevoidFlush()
{
}
publicoverridevoidProcessEvent(WebBaseEventraisedEvent)
{
varevt=raisedEventasMultitenantWebEvent;
if(evt!=null)
{
vartenant=TenantsConfiguration.GetCurrentTenant();
if(tenant.Name==evt.Tenant)
{
varhandler=this.Event;
if(handler!=null)
{handler(this,
newMultitenantEventArgs(
this,evt));
}
}
}
}
publicoverridevoidShutdown()
{
}
}
[Serializable]
publicsealedclassMultitenantEventArgs:EventArgs
{
publicMultitenantEventArgs(MultitenantEventProviderprovider,
MultitenantWebEventevt)
{
this.Provider=provider;
this.Event=evt;
}
publicMultitenantEventProviderProvider{get;privateset;}
publicMultitenantWebEventEvent{get;privateset;}
}
publicclassMultitenantWebEvent:WebBaseEvent
{
publicMultitenantWebEvent(Stringmessage,ObjecteventSource, Int32eventCode,Objectdata):
this(message,eventSource,eventCode,data,0){}
publicMultitenantWebEvent(Stringmessage,ObjecteventSource,
Int32eventCode,Objectdata,Int32eventDetailCode):
base(message,eventSource,eventCode,eventDetailCode)
{
vartenant=TenantsConfiguration.GetCurrentTenant();
this.Tenant=tenant.Name;
this.Data=data;
}
publicStringTenant{get;privateset;}
publicObjectData{get;privateset;}
}
So,wehavea provider class (MultitenantEventProvider), a provider eventargument (MultitenantEventArgs), and a provider event(MultitenantWebEvent).Wecanalwaysfindtheproviderthatwasregisteredforthe current tenant by calling the static method FindProvider, and from thisproviderwecan registereventhandlers to theEvent event. In amoment,we’llsee how we can wire this, but first, here is a possible implementation of theIDiagnosticsinterfaceRaiseWebEventmethod:
CodeSample117
publicsealedclassMultitenantDiagnostics:IDiagnostics,ITenantAwareService
{
publicGuidRaiseWebEvent(Int32eventCode,Stringmessage,Objectdata,
Int32eventDetailCode=0)
{
vartenant=TenantsConfiguration.GetCurrentTenant();
varevt=newMultitenantWebEvent(message,tenant,eventCode,
data,eventDetailCode);
evt.Raise();
returnevt.EventID;
}
}
Now, let’s add some rules and see thingsmoving. First, we need to add acoupleofelementstotheWeb.configfile’shealthMonitoringsection:
CodeSample118
configuration
system.web
healthMonitoringenabled=“true”heartbeatInterval=“0”
providers
addname=“abc.com” type=“MultitenantEventProvider,MyAssembly”
tenant=“abc.com”/
addname=“xyz.net”
type=“MultiTenantEventProvider,MyAssembly”
tenant=“xyz.net”/
/providers
rules
addname=“abc.comCustomEvent”
eventName=“abc.comCustomEvent”
provider=“abc.com”
minInterval=“00:01:00”
minInstances=“1”maxLimit=“1”/
addname=“xyz.netCustomEvent”
eventName=“xyz.netCustomEvent”
provider=“xyz.net”
minInterval=“00:01:00”
minInstances=“2”maxLimit=“2”/
/rules
eventMappings
addname=“abc.comCustomEvent”
startEventCode=“100001”
endEventCode=“100001”
type=“MultiTenantWebEvent,MyAssembly”/
addname=“xyz.netCustomEvent”
startEventCode=“200001”
endEventCode=“200001”
type=“MultiTenantWebEvent,MyAssembly”/
/eventMappings
/healthMonitoring
/system.web
/configuration
So,whatwehavehereis:
Twoprovidersregisteredofthesameclass(MultitenantEventProvider),oneforeachtenant,withanattributetenantthatstatesitTworules,eachwhichraisesacustomeventwhenanamedevent(eventName)israisedanumberoftimes(minInstances,maxLimit)inacertainperiodoftime(minInterval),foragivenprovider;Twoeventmappings(eventMappings)thattranslateeventidintervals(startEventCode,endEventCode)toacertaineventclass(type).
We’ll also need to add an event handler for theMultitenantEventProviderclassoftherighttenant,maybeinApplication_Start:
CodeSample119
protectedvoidApplication_Start()
{
MultiTenantEventProvider.RegisterEvent(“abc.com”,(s,e)=
{
//dosomethingwhentheeventisraised
});
}
So, in theend, ifanumberofwebevents israised in theperiodspecified inanyoftherules,aneventisraisedandhopefullysomethinghappens.
Analytics
AnalyticsHands down, Google Analytics is the de facto standard when it comes to
analyzingthetrafficonyourwebsite.NootherservicethatIamawareofoffersthesameamountofinformationandfeatures,soit,makessensetouseit,alsoformultitenantsites.
If youdon’t haveaGoogleAnalyticsaccount, go createone, theprocess isprettystraightforward(yes,youdoneedaGoogleaccountforthat).
Afteryouhaveone,gototheAdminpageandcreateasmanypropertiesasyourtenants(Propertydropdownlist):
Figure29:CreatingGoogleAnalyticsproperties
EachtenantwillbeassignedauniquekeyintheformUA-nnnnnnnn-n,wheren are numbers. If you followed the previous topic on theConfiguration service,youwillhaveaconfigurationfilepertenant(abc.com.config)attherootofyourwebsite,andthisiswhereyouwillstorethiskey:
CodeSample120
configuration
appSettings
addkey=“GoogleAnalyticsKey”value=“UA-nnnnnnnn-n”/
/appSettings
/configuration
Ofcourse,doreplaceUA-nnnnnnnn-nwiththerightkey!Now,weneedtoaddsomeJavaScripttothepagewherewementionthiskey
andtenantname.ThisscriptiswheretheactualcalltotheGoogleAnalyticsAPIisdone,anditlookslikethis:
CodeSample121
scripttype=“text/javascript”//![CDATA[
(function(i,s,o,g,r,a,m){i[‘GoogleAnalyticsObject’]=r;i[r]=i[r]||function(){
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*newDate();a=s.createElement(o),
m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
})(window,document,‘script’,’//www.google-analytics.com/analytics.js’,‘ga’);
ga(‘create’,’{0}’,‘auto’);
ga(‘send’,‘pageview’);
//]]/script
Ifyouarecuriousaboutwherethiscamefrom,GoogleAnalytics’Adminpagehasthisinformationreadyforyoutopickup—justclickonTrackingInfoforyourproperty(tenant)ofchoice.
You might have noticed the {0} token—this is a standard .NET stringformattingplaceholder.What itmeans is, itneedstobereplacedwiththeactualtenant key (UA-nnnnnnnn-n). Each tenant will have this key stored in itsconfiguration,say,underthekeyGoogleAnalyticsKey.ThiswaywecanalwaysretrieveitthroughtheIConfigurationinterfacepresentedawhileago.
IfwewillbeusingASP.NETWebForms,anicewrapper for thismightbeacontrol:
CodeSample122
publicsealedclassGoogleAnalytics:Control
{
privateconstStringScript=“scripttype=“text/javascript”//![CDATA[“+
“(function(i,s,o,g,r,a,m){{i[‘GoogleAnalyticsObject’]=r;i[r]=i[r]||function(){{“+
“(i[r].q=i[r].q||[]).push(arguments)}},i[r].l=1*newDate();a=s.createElement(o),”+
“ m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)”+
“}})(window,document,‘script’,’//www.google-analytics.com/analytics.js’,‘ga’);”+
“ga(‘create’,’{0}’,‘auto’);”+
“ga(‘send’,‘pageview’);”+
“//]]/script”;
protectedoverridevoidRender(HtmlTextWriterwriter)
{
varconfig=ServiceLocator.Current
.GetInstanceIConfiguration();
varkey=config.GetValue(“GoogleAnalyticsKey”);
writer.Write(Script,key);
}
}
Hereisasampledeclarationonapageormasterpage:
CodeSample123
%@RegisterAssembly=“Multitenancy.WebForms”Namespace=“Multitenancy.WebForms”
TagPrefix=“mt”%
mt:GoogleAnalyticsrunat=“server”/
Otherwise, for MVC, the right place would be an extension method overHtmlHelper:
CodeSample124
publicstaticclassHtmlHelperExtensions
{ private const String Script = “script type=“text/javascript”// ![CDATA[“ + “(function(i,s,o,g,r,a,m){{i[‘GoogleAnalyticsObject’]=r;i[r]=i[r]||function(){{“+
“(i[r].q=i[r].q||[]).push(arguments)}},i[r].l=1*newDate();a=s.createElement(o),”+
“ m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)”+
“}})(window,document,‘script’,’//www.google-analytics.com/analytics.js’,‘ga’);”+
“ga(‘create’,’{0}’,‘auto’);”+
“ga(‘send’,‘pageview’);”+
“//]]/script”;
publicstaticvoidGoogleAnalytics(thisHtmlHelperhtml)
{
varconfig=ServiceLocator.Current
.GetInstanceIConfiguration();
varkey=config.GetValue(“GoogleAnalyticsKey”);
html.Raw(String.Format(Script,key));
}
}
Itwouldthenbeinvokedasthis,onalocalorsharedview:
CodeSample125
@Html.GoogleAnalytics()
Andthat’s it!Eachtenantwillget itsownpropertypageonGoogleAnalytics,andyoucanstartmonitoringthem.
Chapter10Security
Chapter10SecurityIntroductionAll applications need security enforcing. Multitenant applications have
additional requirements, in the sense that something that is allowed for sometenantmaybeshouldn’tbeforothertenants.Thischaptertalksaboutsomeofthesecuritymechanismstoconsiderwhenwritingmultitenantapplications.
Authentication
AuthenticationTheauthenticationmechanismthatwillbeusedshouldbecapableof:
LogginginusersassociatedwiththecurrenttenantDenyinglogintouserscomingfromatenantwithwhichtheyarenotassociated
Let’sseehowwecanimplementtheserequirements.
ASP.NETMembershipandRoleProvidersThe venerable ASP.NET Membership and Role providers are part of the
extensibleprovidermodel introduced inASP.NET2.0.TheMembershipprovidertakes care of authenticating and managing users, and the Role providerassociates theseusers togroups.This frameworkhasagedover theyears,butbecauseitmightbenecessarytosupportit(brownfielddevelopment),Idecideditwasworthmentioning.
Theframeworkincludestwoimplementationsoftheseproviders:oneforusingWindows facilities for authentication and groupmembership, and one for usingSQLServer-basedobjects.
Youcan imaginethat theSQLServerversionsaremoreflexible, in that theydonotforceustouseWindows(orActiveDirectory)accountsforourusersandgroups.Theschematheyuseisthis:
Figure30:ASP.NETMembershipandRoleproviderschema
Keytounderstandingthisschemaarethesetables:
Table7:ASP.NETMembershipandRoleprovidertablesandtheirpurposes
Table
Purpose
aspnet_Applications
StoresalltheapplicationsthattheMembershipandRoleprovidersknowof
aspnet_Users
Storesalltheregisteredusers,associatedwithanapplication
aspnet_Roles
Storesalloftheroles
aspnet_Membership
Additionalinformationaboutregisteredusers
aspnet_UsersInRoles
Associationofuserstoroles
Both the users tables and the roles tables are dependant of the applicationtable,meaningdifferentapplicationscanhavedifferentusersandroles.Asingledatabase can hold several registered users and roles,maybe for different websites, eachactingasa separateapplication; in this case, eachweb sitewill beidentified by a different application name. The configuration is normally definedstatically in the global ASP.NET configuration file,Machine.config, the relatedpartofwhichisshownhere,abitsimplified:
CodeSample126
membershipdefaultProvider=“AspNetSqlMembershipProvider”
providers
addname=“AspNetSqlMembershipProvider”applicationName=”/”
type=“System.Web.Security.SqlMembershipProvider”
connectionStringName=“LocalSqlServer”/
/providers
/membership
roleManagerenabled=“true”defaultProvider=“AspNetSqlRoleProvider”
providers
addname=“AspNetSqlRoleProvider”connectionStringName=“LocalSqlServer”
applicationName=”/”type=“System.Web.Security.SqlRoleProvider”/
addname=“AspNetWindowsTokenRoleProvider”applicationName=”/”
type=“System.Web.Security.WindowsTokenRoleProvider”/
/providers
/roleManager
It’s easy to see that the “application” concept of the Membership and Roleprovidersmatches closely that of “tenants.” The problem is that the application(applicationName attribute) is hardcoded in the Web.config file. However,becauseitsassociatedproperty(ApplicationName)intheproviderimplementationclass is virtual, we can have classes that inherit from the included ones(SqlMembershipProviderandSqlRoleProvider)andoverridethispropertysothatitreturnsthepropervalueeachtimeitiscalled:
CodeSample127
//Membershipprovider
publicclassMultitenantSqlMembershipProvider:SqlMembershipProvider
{
publicoverrideStringApplicationName
{
get{returnTenantsConfiguration.GetCurrentTenant().Name;}
set{/*nothingtodohere*/}
}
}
//Roleprovider
publicclassMultitenantSqlRoleProvider:SqlRoleProvider
{
publicoverrideStringApplicationName
{
get{returnTenantsConfiguration.GetCurrentTenant().Name;}
set{/*nothingtodohere*/}
}
}
Note: There are providerimplementations for other RDBMs, such as Oracle and MySQL. Because theimplementationwouldbeexactlythesame,IonlyshowtheSQLServerversion.
Now, we need to register our new implementations to either theMachine.config(formachine-wideavailability)orWeb.configfiles:
CodeSample128
membershipdefaultProvider=“MultitenantSqlMembershipProvider”
providers
addname=“MultitenantSqlMembershipProvider”
type=“MyNamespace.MultitenantSqlMembershipProvider,MyAssembly”
connectionStringName=“LocalSqlServer”/
/providers
/membership
roleManagerenabled=“true”defaultProvider=“MultitenantSqlRoleProvider”
providers
addname=“MultitenantSqlRoleProvider”
connectionStringName=“LocalSqlServer”
type=“MyNamespace.MultitenantSqlRoleProvider,MyAssembly”/
/providers
/roleManager
Tip: Even if I opted to talk about the Membership provider, by nomeansdo I think thatyoushouldbeusing it innew (Greenfield)developments.ThefutureaccordingtoMicrosoft isASP.NETIdentity(at least, forthemoment),andIstronglyurgeyoutogetintoit.There’sagoodposthereonhowtomigratefromtheMembershipprovidertoASP.NETIdentity.
ASP.NETIdentityAmuchbetterway toprovideauthentication toyourASP.NETapplications is
the ASP.NET Identity framework. Microsoft designed this framework tocomplement the shortcomings and problems of the old provider model, and tooffersupportfornewauthenticationstandards,suchasOAuth.Unfortunately,theMicrosoftarchitectsthatdesigneditleftoutbuilt-insupportformultitenancy.Allisnot lost, however, because it has already been implemented for us by thecommunity, in the person of James Skimming and theAspNet.Identity.EntityFramework.Multitenant package, among others.ASP.NETIdentityisprovider-agnostic,whichmeansitcanuse,forexample,EntityFrameworkorNHibernateas itspersistenceengine,but James’ implementationreliesontheEntityFramework.
Note: There’s a package calledNHibernate.AspNet.Identity, byAntonioBastos, that providesan implementationofASP.NETIdentitywithNHibernate.YoucangettheNuGetpackagehere,and
thesourcecodehere.
So,afteryouaddtheASP.NETIdentitypackageusingNuGet,youwillneedtoaddtheAspNet.Identity.EntityFramework.Multitenantpackage:
Figure31:ASP.NETIdentityEntityFrameworkwithmultitenancysupport
Afteryoudo,weneedtomakeacoupleofchangesto theASP.NETIdentityclasses in our project. The class that interests us is ApplicationUser, whichshould be changed so as to inherit from MultitenantIdentityUser instead ofIdentityUser:
CodeSample129
publicclassApplicationUser:MultitenantIdentityUser
{
//restgoeshere
}
The class MultitenantIdentityUser already provides a property calledTenantId,whichisusedtodistinguishbetweendifferenttenants.Thispropertyismapped to the TenantId column of the AspNetUserLogins table. The modellookslikethis:
Figure32:ASP.NETIdentitymodel
Other than that,weneed to changeourDbContext-derived class tomake itinheritfromMultitenantIdentityDbContextApplicationUseraswell:
CodeSample130
public class ApplicationDbContext:MultitenantIdentityDbContextApplicationUser
{
//restgoeshere
}
Finally,weneed to teachASP.NET Identityhow toget thecurrent tenant. Inorder to do that, we need to change the Create method of theApplicationUserManagerclasslikethis:
CodeSample131
publicstaticApplicationUserManagerCreate(
IdentityFactoryOptionsApplicationUserManageroptions,IOwinContextcontext)
{
var tenant = TenantsConfiguration.GetCurrentTenant(); varmanager=newApplicationUserManager(
newMultitenantUserStoreApplicationUser(
context.GetApplicationDbContext())
{TenantId=tenant.Name});
//restgoeshere
}
The ApplicationUserManager.Create method is registered as the factorymethod for our user store.Whenever it is called, it will use the current tenantname.That’sallthereistoit—ASP.NETIdentitycanbeusedinexactlythesamewayasitwouldbewithoutthemultitenantsupport.
Note: James Skimming madeavailable the source code for his AspNet.Identity.EntityFramework.MultitenantpackageinGitHubhere.Theactualcommitwherethesechangesareoutlinedishere.
HTTPS
HTTPSIfwearetofollowthehostheaderapproach,andwewouldliketouseHTTPS,
wewillneedtosupplyacertificateforeachofthenamedtenants.Now,wehavetwooptions:
AcquireacertificatefromarootauthorityGenerateourown(dummy)certificateinIISManager,ifwedon’tcareaboutgettingamessageaboutaninvalidrootauthority
Fordevelopmentpurposes,wecangowithadummycertificate.WeneedtotellIIStouseit:
Figure33:Creatinganewsite
Figure34:Addingabindingtoasite
Note: In theunlikelycasethatallour tenantswillshareat leastpartof thedomain,wecanuseawildcardcertificate;otherwise,wewillneedone foreachtenant,whichmightprovecostly.
Applicationpools
ApplicationpoolsAsmentionedinChapter2,havingdifferentapplicationpoolspertenantallows
us to have the application running under different accounts. This can provehelpful, for example, if eachuser hasdifferent default databaseson the server,andyoudon’t specify the initial databaseon theconnectionstring.Each tenantwilllandonitsowndatabase,accordingtothedatabaseserverconfiguration.Itiswiser,however,tonottrustthedefaultdatabaseassignedtoauser,andtouseadatabasepartitioningorselectionstrategy,aswillbeoutlinedinthenextchapter.
Cookies
CookiesCookiesarestoredontheclientside, inthebrowser’sownstore.Bydefault,
theyareboundtothedomainfromwhichtheyoriginated,whichisfineifwearetousetheHostHeaderStrategy,butnotsomuchifwewanttouseanothertenantidentificationstrategy.Forexample,ifwedon’tdoanythingaboutit,acookiesentin the context of a tenant will also be available if the user decides to browseanothertenant’ssite.Cookiesdon’treallyoffermanyoptions,otherthanthepath(relativepartof theURL), thedomain,and theavailability fornon-HTTPSsites.Theonlythingthatwecanplaywitharethenamesofthekeys:wecanadd,forexample,aprefixthatidentifiesthetenanttowhichthecookieapplies,somethinglike:
CodeSample132
vartenantCookies=HttpContext.Current.Request.Cookies.OfTypeHttpCookie()
.Where(x=x.Name.StartsWith(String.Concat(tenant,”:”)));
varcookie=newHttpCookie();
cookie.Name=String.Concat(tenant,”:”,key);
cookie.Value=value;
HttpContext.Current.Response.Cookies.Add(cookie);
Chapter11DataAccess
Chapter11DataAccessIntroductionProblem:eachtenantneedstohave,atleastpartially,separatedataandeven
schema.
Whenitcomestoaccessingdatainamultitenantapplication,therearethreemajortechniques:
Separatedatabase:Eachtenant’sdataiskeptinaseparatedatabaseinstance,withadifferentconnectionstringforeach;themultitenantsystemshouldpickautomaticallytheoneappropriateforthecurrenttenant
Figure35:Separatedatabases
Separateschema:Thesamedatabaseinstanceisusedforallthetenants’data,buteachtenantwillhaveaseparateschema;notallRDBMSessupportthisproperly,forexample,SQLServerdoesn’t,butOracledoes.WhenIsaySQLServerdoesn’tsupportthis,Idon’tmeantosaythatitdoesn’thaveschemas,it’sjustthattheydonotofferanisolationmechanismasOracleschemasdo,anditisn’tpossibletospecify,perqueryorperconnection,theschematousebydefault.
Figure36:Separateschemas
Partitioneddata:Thedataforalltenantsiskeptinthesamephysical
instanceandschema,andapartitioningcolumnisusedtodifferentiatetenants;itisuptotheframeworktoissueproperSQLqueriesthatfilterdataappropriately.
Figure37:Partitioneddata
Note:Ifyouareinterestedinlearningmore,thisarticlepresentssomeofthedifferencesbetweenOracleandSQLServerregardingschemas.
Asforactuallyretrievingandupdatingdata inrelationaldatabases, twomainapproachesexist:
UsingADO.NET,orsomethinwrapperaroundit,liketheEnterpriseLibraryDataAccessBlockUsinganObject-RelationalMapper(ORM),suchasNHibernateorEntityFramework(CodeFirst)
Adiscussionastowhichoftheseapproachesisbetterisoutsidethescopeofthis book, and personally, it feels pointless to me: both have pros and cons.ORMs, however, offer some configuration mechanisms that allow data to befiltered automatically based on some condition, such as the tenant’s name.Therefore,wewilldiscusshow touseORMsNHibernateandEntityFrameworkCode First for multitenant data access, and we will leave out SQL-basedsolutions.
NHibernateDifferentDatabasesIn the NHibernate architecture, a connection string is tied to a session
factory. It’s thesession factory thatbuildssessions,which in turnencapsulateADO.NETconnections.Ingeneral,itisagoodpracticetohaveasinglesessionfactory;inourcase,wewillneedonepertenant(andconnectionstring).
Tomakesessionfactoriesdiscoverable,wewillusethesamemechanismweusedearlier in thisbook,and that is theCommonServiceLocator.Each tenantwill have its own registration of ISessionFactory under its name, and eachsession factory will point to a likewise-named connection string. Consider thefollowingbootstrapcodethat,again,usesUnityastheInversionofControl(IoC)framework:
CodeSample133
protectedvoidApplication_BeginRequest()
{
vartenant=TenantsConfiguration.GetCurrentTenant().Name;
varsessionFactory=ServiceLocator.Current
.TryResolveISessionFactory(tenant);
if(sessionFactory==null)
{
this.SetupSessionFactory(tenant);
}
}
privatevoidSetupSessionFactory(Stringtenant)
{
varcfg=newConfiguration().DataBaseIntegration(x=
{
x.ConnectionStringName=tenant;
//restgoeshere
});
//etc
//get the Unity instance from the Common Service Locator – another optionwould be to encapsulate this in some class or to use a custom connectionprovider
varunity=ServiceLocator.Current.GetInstanceIUnityContainer();
unity.RegisterInstanceISessionFactory(tenant,cfg.BuildSessionFactory());
}
Here’swhatitdoes:whenarequestisreceived,itgetsthetenantnamefromit, and checks to see if there is already a registered session factory under thatnameintheCommonServiceLocator.Ifnot,itstartstheprocessofbuildingandregisteringthesessionfactoryforthecurrenttenant.
ThiscodecaneithergointheGlobal.asax.cs file,asan instancemethodofthe HttpApplication-derived class, or in a module, a class that implementsIHttpModule. The latter is recommended to allow for code reuse and bettermaintainability, but if you are to follow this path, you will need to register themoduleyourselfintheWeb.configfile,sectionsystem.webServer/modules:
CodeSample134
system.webServer
modules
addname=“MyModule”type=“MyNamespace.MyModule,MyAssembly”/
/modules
/system.webServer
So, whenever you wish to open a session, you first need to retrieve theappropriatesessionfactoryforthecurrenttenant:
CodeSample135
vartenant=TenantsConfiguration.GetCurrentTenant().Name;
//lookupthesessionfactoryforthecurrenttenant
varsessionFactory=ServiceLocator.Current.GetInstanceISessionFactory(tenant);
using(varsession=sessionFactory.OpenSession())
{
//…
}
Note:IfyouwanttolearnmoreabouttheNHibernatearchitecture,IsuggestreadingNHibernateSuccinctly,alsointheSuccinctlyseries.
DifferentSchemasTheproblemwithNHibernateandmappingsisthatnormallyweonlyhaveone
sessionfactoryandaconfigurationinstancefromwhichitoriginated.Becauseit’sthe configuration instance that holds the mappings, which then passes to thesessionfactoryandintheend,tothesessionsspawnedfromit,weneedtohavedifferentconfigurationinstances,oneforeachtenant.
Here’showwecanconfiguretheschemaforthecurrenttenant:
CodeSample136
publicclassMyMultitenantEntityClassMapping:ClassMappingMyMultiTenanEntity
{
publicMyMultitenantEntityClassMapping()
{
vartenant=TenantsConfiguration.GetCurrentTenant().Name;
this.Schema(tenant);
//restgoeshere
}
}
Or,wecandoitthroughconventions:
CodeSample137
varmapper=newConventionModelMapper();
vartenant=TenantsConfiguration.GetCurrentTenant().Name;
mapper.BeforeMapClass+=(modelInspector,type,classCustomizer)=
{
classCustomizer.Schema(tenant);
};
Tip:Don’tforgetthattheschemanamecannottakeallcharacters,normally—onlyalphanumericcharactersareallowed—soyoumayneedtodosometransformationonthetenantname.
DatapartitioningNHibernate has a nice feature by the name of filter which can be used to
define arbitrary SQL restrictions at the entity level. A filter can be enabled ordisabledandcantakeruntimeparameters,somethingthatplaysverynicelywithtenant names.For example, sayour tablehasa tenant column that keeps thenameofthetenantthatitrefersto.WewouldaddaSQLrestrictionof“tenant=’abc.com’”,exceptthatwecan’thardcodethetenantname;weuseparametersinstead. A filter is defined in the entity’s mapping. Here’s an example usingmappingbycode:
CodeSample138
publicclassMyMultitenantEntityClassMapping:ClassMappingMyMultitenantEntity
{
publicMyMultitenantEntityClassMapping()
{
this.Filter(“tenant”,filter=
{
filter.Condition(“tenant=:tenant”);
});
//restgoeshere
}
}
Noticethe“tenant=:tenant”part;thisisaSQLrestrictionindisguise,wheretenant is the name of a column and :tenant is a named parameter, whichhappenstohavethesamename.Iomittedthemostpartofthemapping,becauseonly the filtering part is relevant for our discussion. Similar code should berepeated inall themappingsofall the tenant-awareentities,and,ofcourse, thepropercolumnnameshouldbespecified.
Here’sanotherexampleusingtheconventionalmapper:
CodeSample139
varmapper=newConventionModelMapper();
vartenant=TenantsConfiguration.GetCurrentTenant().Name;
mapper.BeforeMapClass+=(modelInspector,type,classCustomizer)=
{
classCustomizer.Filter(“tenant”,filter=
{
filter.Condition(“tenant=:tenant”);
});
};
Now,wheneverweopenanewsession,weneed toenable the tenant filterandassignavaluetoitstenantparameter:
CodeSample140
vartenant=TenantsConfiguration.GetCurrentTenant().Name;
session
.EnableFilter(“tenant”)
.SetParameter(“tenant”,tenant);
The restriction and parameter value will last for the whole lifetime of the
session, unless explicitly changed. You can see I am resorting to the staticauxiliary method GetCurrentTenant that was defined in Chapter 3. Now,whenever you query for theMyMultitenantEntity class, the filter SQL will beappended to the generated SQL, with the parameter properly replaced by itsactualvalue.
GenericRepositoryTomakelifeeasierforthedeveloper,wecanencapsulatethecreationofthe
sessions and the configuration of the filter behind a Repository Pattern (orGenericRepository)façade.Thispatterndictatesthatthedataaccessbehiddenbehindmethodsandcollectionsthatabstractthedatabaseoperationsandmakethemlookjustlikein-memory.
Note: I won’t go into a discussion of whether the Repository/GenericRepositoryPatternisagoodthingornot.Ipersonallyunderstanditsdrawbacks,butIconsideritusefulincertainscenarios,suchasthisone.
ApossibledefinitionforaGenericRepositoryinterfaceis:
CodeSample141
publicinterfaceIRepository:IDisposable
{
TFindT(Objectid);
IQueryableTAllT(paramsExpressionFuncT,Object[]expansions);
voidDelete(Objectitem);
voidSave(Objectitem);
voidUpdate(Objectitem);
voidRefresh(Objectitem);
voidSaveChanges();
}
Mostmethodsshouldbefamiliartoreaders.Wewillnotcoverthisinterfaceindepth;instead,let’smoveontoapossibleimplementationforNHibernate:
CodeSample142
publicsealedclassSessionRepository:IRepository
{
privateISessionsession;
publicSessionRepository()
{
vartenant=TenantsConfiguration.GetCurrentTenant().Name;
//lookuptheoneandonlysessionfactory
varsessionFactory=ServiceLocator.Current
.GetInstanceISessionFactory();
this.session=sessionFactory.OpenSession();
//enablethefilterwiththecurrenttenant
this.session.EnableFilter(“tenant”)
.SetParameter(“tenant”,tenant);
this.session.BeginTransaction();
}
publicTFindT(paramsObject[]ids)whereT:class
{
returnthis.session.GetT(ids.Single());
}
publicIQueryableTQueryT(params
ExpressionFuncT,Object[]expansions)whereT:class
{
varall=this.session.QueryT()asIQueryableT;
foreach(varexpansioninexpansions)
{
all=all.Include(expansion);
}
returnall;
}
publicvoidDeleteT(Titem)whereT:class
{
this.session.Delete(item);
}
publicvoidSaveT(Titem)whereT:class
{
this.session.Save(item);
}
publicvoidUpdateT(Titem)whereT:class
{
this.session.Update(item);
}
publicvoidRefreshT(Titem)whereT:class
{
this.session.Refresh(item);
}
publicvoidDetachT(Titem)whereT:class
{
this.session.Evict(item);
}
publicvoidSaveChanges()
{
this.session.Flush();
try
{
this.session.Transaction.Commit();
}
catch
{
this.session.Transaction.Rollback();
}
this.session.BeginTransaction();
}
publicvoidDispose()
{
if(this.context!=null)
{
this.session.Dispose();
this.session=null;
}
}
}
Remember that now there is only a single session factory, because there isalso a single database. Now, all we have to do is register the IRepositoryinterface with our IoC framework and always access it through the CommonServiceLocator:
CodeSample143
//registerourimplementationundertheIRepositoryinterface
unity.RegisterTypeIRepository,SessionRepository(
newPerRequestLifetimeManager());
//getareferencetoanewinstance
using(varrepository=ServiceLocator.Current.GetInstanceIRepository())
{
//querysomeentity
varitems=repository.AllMyEntity().ToList();
}
Tip: The process of enabling the filterandsettingthetenantparametermustalwaysbedone,soeithermakesureyouuse a repository or perform the initialization yourself, perhaps in someinfrastructurecodeifpossible.
Note: Iusedfor theregistrationthePerRequestLifetimeManagerpresentedintheApplicationServiceschapter.
EntityFrameworkCodeFirstDifferentdatabasesThearchitectureofEntityFrameworkCodeFirstisquitedifferentfromthatof
NHibernate.Forone,thereisnobuildermethodthatwecanhookuptothatcanreturnatenant-specificcontextwithitsownconnectionstring.Whatwecandoisbuild our own factory method that returns a proper connection string, for thecurrenttenant:
CodeSample144
publicclassMultitenantContext:DbContext
{
publicMultitenantContext():base(GetTenantConnection()){}
privatestaticStringGetTenantConnection()
{
vartenant=TenantsConfiguration.GetCurrentTenant();
returnString.Format(“Name={0}”,tenant.Name);
}
//restgoeshere
}
Itisimportantthatyoudonotallowthecreationofacontextwithanyarbitraryconnectionstring,because thisdefeats thepurposeof transparentmultitenancythroughseparatedatabases.
DifferentschemasWithCodeFirst,itisveryeasytoapplyourownconventionstoamodel:
CodeSample145
protectedoverridevoidOnModelCreating(DbModelBuildermodelBuilder)
{
vartenant=TenantsConfiguration.GetCurrentTenant();
//repeatforallmultitenantentities
modelBuilder.Types().Configure(x=x.ToTable(x.ClrType.Name,tenant.Name);
//restgoeshere
base.OnModelCreating(modelBuilder);
}
Tip: Make sure the schema name isvalid.
DatapartitioningEven ifnotquiteaspowerful,EntityFrameworkCodeFirstallowsus tomap
anentitywithadiscriminatorcolumnandvalue.Thisbasicallyservesthepurposeof allowing the Table Per Class Hierarchy / Single Table Inheritance strategy,wherethiscolumnisusedtotelltowhichentityarecordmapsto,inatablethatisshared by a hierarchy of classes. The only out-of-the-box way to achieve thisconfiguration is by overriding the OnModelCreating method and specifying adiscriminatorcolumnandvalue:
CodeSample146
protectedoverridevoidOnModelCreating(DbModelBuildermodelBuilder)
{
vartenant=TenantsConfiguration.GetCurrentTenant();
//repeatforallmultitenantentities
modelBuilder.EntityMyMultitenantEntity().Map(m=m.Requires(“Tenant”)
.HasValue(tenant.Name));
//restgoeshere
base.OnModelCreating(modelBuilder);
}
This tells Entity Framework that, whenever a DbContext is created, someentitiesshouldbemappedsothatwhenevertheyareinsertedorretrieved,EntityFramework will always consider a Tenant discriminator column with a valueidenticaltothecurrenttenant,automatically.
Note:Again, foramore in-depthunderstandingofEntityFrameworkCodeFirst, I suggest reading Entity Framework Code First Succinctly, also in theSuccinctlyseries.
GenericRepositoryFor a more secure solution, here’s what an implementation of the Generic
RepositoryPatternforanEntityFrameworkCodeFirstdatacontextmightbe:
CodeSample147
publicsealedclassMultitenantContextRepository:IRepository
{
privateMultitenantContextcontext;
publicMultitenantContextRepository()
{
//ifyouusethecodefromthepreviousexample,thisisnotnecessary,itisdonethere
vartenant=TenantsConfiguration.GetCurrentTenant();
//settheconnectionstringnamefromthecurrenttenant
this.context=new
MultitenantContext(String.Format(“Name={0}”,
tenant.Name));
}
publicTFindT(paramsObject[]ids)whereT:class
{
returnthis.context.SetT().Find(ids);
}
publicIQueryableTQueryT(params
ExpressionFuncT,Object[]expansions)whereT:class
{
varall=this.context.SetT()asIQueryableT;
foreach(varexpansioninexpansions)
{
all=all.Include(expansion);
}
returnall;
}
publicvoidDeleteT(Titem)whereT:class
{
this.context.SetT().Remove(item);
}
publicvoidSaveT(Titem)whereT:class
{
this.context.SetT().Add(item);
}
publicvoidUpdateT(Titem)whereT:class
{
this.context.Entry(item).State=EntityState.Modified;
}
publicvoidRefreshT(Titem)whereT:class
{
this.context.Entry(item).Reload();
}
publicvoidDetachT(Titem)whereT:class
{
this.context.Entry(item).State=EntityState.Detached;
}
publicvoidSaveChanges()
{
this.context.SaveChanges();
}
publicvoidDispose()
{
if(this.context!=null)
{
this.context.Dispose();
this.context=null;
}
}
}
Pretty straightforward, I think. Just store it in Unity (or any IoC container ofyourchoice)andyou’redone:
CodeSample148
//registerourimplementationwithUnityundertheIRepositoryinterface
unity.RegisterTypeIRepository,MultitenantContextRepository(
newPerRequestLifetimeManager());
//getareferencetoanewinstanceusingCommonServiceLocator
using(varrepository=ServiceLocator.Current.GetInstanceIRepository())
{
//querysomeentity
varitems=repository.AllMyEntity().ToList();
}
Note:NoticeagainthePerRequestLifetimeManagerlifetimemanager.
Chapter12PuttingItAllTogether
Chapter12PuttingItAllTogetherClassmodelTheframeworkclassesthatweusedintheexamplesare:
Figure38:Highlevelinterfaces
Andtheircommonimplementations:
Figure39:Typicalimplementationsofhighlevelinterfaces
Choosingtherighttenantidentificationstrategy
ChoosingtherighttenantidentificationstrategyAll of the tenant identification strategies presented in Chapter 3 have some
pros and cons; some require some additional configuration, others only reallyapplytosimplescenarios.Someofthecriteriainclude:
EaseoftestingoneortheothertenantTheneedtobindatenanttoaspecificsource(IPrangeordomain)Easeofsettingitup
Youcanusethefollowingdecisionflow:
Figure40:Simpledecisionflowforchoosingthetenantidentificationstrategy
Choosingtherighttenantlocationstrategy
ChoosingtherighttenantlocationstrategyWhenchoosingatenantlocation(andregistrationstrategy),youneedtotake
intoaccountthesequestions:
Doyouneedtoregisternewtenantsdynamically?Howmucheffortisacceptableforregisteringnewtenants(e.g.,changingsomeconfigurationfile,recompilingtheapplication,restartingit)?Aresomeaspectsoftenantsonlyconfigurablethroughconfiguration?
Youneed toweighall of theseconstraintsbeforeyoumakeadecision.Thefollowingpicturesummarizesthis:
Figure41:Simpledecisionflowforchoosingthetenantlocationstrategy
Choosingtherightdataaccessatrategy
ChoosingtherightdataaccessatrategyThetwoAPIspresented,NHibernateandEntityFramework,supportallofthe
data access strategies. In the end, I think the decisive factor is the level ofisolation that we want: separate databases offer the highest level, and datapartitioningthroughdiscriminatorcolumnsoffersthelowest.Criteriamightinclude:
ContractualrequirementsSLAsRegulatorydemandsTechnicalreasonsNeedtochargeperusedspace
Figure42:Decisionflowforselectingdataseparationstrategy
As for theAPIs themselves, therewill be probably somebenefit in terms ofease of usewithEntity Framework, but it all goes down towhat the developerknowsbest,althoughit’struethathavingseparateschemasisslightlytrickierwithNHibernate.Inanycase,usinganORMseemsawiserdecisionthanstickingwithplain ADO.NET, because of all of the additional services that ORMs provide.UsingtheGenericRepositorypatterntoabstractawaythedataaccessAPImightbeagoodideaonlyifyoudonotneedanyoftheAPI-specificfeatures,andjuststickwiththeleastcommondenominator.
Monitoring
MonitoringEven if youdonotusecustomperformancecounters, thebuilt-inonesoffer
greattoolswhenitcomestomonitoring—andperhaps,billing—individualtenants.Someperformancecountersoffergood insightsonwhat’sgoingon, tenant-wise(youcanselectthesitestomonitorindependently):
ASP.NETApplicationsASP.NETAppsv4.0.30319APP_POOL_WAS(applicationpoolinformation)
You can view reports on some of thesemetrics, which can help youmakedecisionsorserveasabillingbasis:
Figure43:Reportingonperformancecounters
It is evenpossible to addalerts to be triggeredwhen certain valuesexceedtheirnormalthresholds:
Figure44:Addingaperformancecounteralert
This example shows a rule for generating an alert for when theManagedMemoryUsed(estimated)metricoftheASP.NETApplicationscounterexceeds1,000,000, for the _LM_W3SVC_1_ROOT site (abc.com) which is evaluatedevery15seconds.
Layout
LayoutIf you need to support different layouts per tenant, make sure you follow
modernguidelinesfordefiningthemarkupofyourpages;namely,enforceastrictseparationbetweencontent(HTML)andlayout(CSS)andusetheproperHTMLelementsfororganizingyourcontents.Forexample,donotusetheTableelementfor layout or grouping contents, because it does not scale well and imposeslimitationsonwhatwecanchange.
Note:TheCSSZenGardensiteoffers fantasticexamplesonthepowerofCSS.
References
References
AsEasyAsFallingOffaLog:UsingtheLoggingApplicationBlockASP.NET2.0ProviderModelASP.NET2.0ProviderModel:IntroductiontotheProviderModelCommonServiceLocatorConceptmappingbetweenSQLServerandOracleConfiguring/MappingPropertiesandTypeswiththeFluentAPICSSZenGardenDevelopingMulti-tenantApplicationsfortheCloud,3rdEditionDifferenceBetweenCNameandARecordEnterpriseLibrary-DataAccessBlockEnterpriseLibrary-LoggingApplicationBlockEnterpriseLibrary-LoggingApplicationBlockDatabaseProviderEntityFrameworkEntityFrameworkCodeFirstInheritanceHands-OnLabsforMicrosoftEnterpriseLibrary6HowtoviewtheASP.NETPerformanceCountersAvailableonYourComputerKatanaProjectMonitoringASP.NETApplicationPerformanceMuchADOaboutDataAccess:UsingtheDataAccessApplicationBlockMultitenancyNHibernateOWINOWINMiddlewareintheIISintegratedpipelinePerformanceCountersforASP.NETPerfViewSingleTableInheritanceUnderstandingmulti-tenancyinSharePointServer2013WatiN
DetailedTableofContents
DetailedTableofContentsAbouttheAuthor
Chapter1Introduction
Aboutthisbook
Whatismultitenancyandwhyshouldyoucare?
Multitenancyrequirements
Applicationservices
Frameworks
Chapter2Setup
Introduction
VisualStudio
NuGet
Addressmapping
IIS
IISExpress
Chapter3Concepts
Whodoyouwanttotalkto?
Hostheaderstrategy
Querystringstrategy
SourceIPaddressstrategy
Sourcedomainstrategy
Gettingthecurrenttenant
What’sinaname?
Findingtenants
Tenantlocationstrategies
Bootstrappingtenants
Chapter4ASP.NETWebForms
Introduction
Branding
Masterpages
Themesandskins
Showingorhidingtenant-specificcontents
Security
Authorization
Unittesting
Chapter5ASP.NETMVC
Introduction
Branding
Pagelayouts
Viewlocations
CSSBundles
Security
UnitTesting
Chapter6WebServices
Introduction
WCF
WebAPI
UnitTesting
Chapter7Routing
Introduction
Routing
Routehandlers
Routeconstraints
IISRewriteModule
Chapter8OWIN
Introduction
Registeringservices
WebAPI
ReplacingSystem.Web
Unittesting
Chapter9ApplicationServices
Introduction
InversionofControl
Caching
Configuration
Logging
Monitoring
Tracing
PerformanceCounters
HealthMonitoring
Analytics
Chapter10Security
Introduction
Authentication
ASP.NETMembershipandRoleProviders
ASP.NETIdentity
HTTPS
Applicationpools
Cookies
Chapter11DataAccess
Introduction
NHibernate
EntityFrameworkCodeFirst
Chapter12PuttingItAllTogether
Classmodel
Choosingtherighttenantidentificationstrategy
Choosingtherighttenantlocationstrategy
Choosingtherightdataaccessatrategy
Monitoring
Layout
References
Publisher:BookRixGmbH&Co.KGSonnenstraße2381669MunichGermany
PublicationDate:February1st2016
http://www.bookrix.com/-ir0c7a3135af765
ISBN:978-3-7396-3496-8
BookRix-Edition,publishinginformation
WethankyouforchoosingtopurchasethisbookfromBookRix.com.WewanttoinviteyoubacktoBookRix.cominordertoevaluatethebook.Hereyoucandiscusswithotherreaders,discovermorebooksandevenbecomeanauthoryourself.
ThankyouagainforsupportingourBookRix-community.