196

Getting Started with ASP.NET Multitenant Applicationsdl.booktolearn.com/...getting_started_with_asp_net...applications_aff… · ASP.NET is an umbrella under which different frameworks

  • Upload
    others

  • View
    8

  • Download
    1

Embed Size (px)

Citation preview

Page 1: Getting Started with ASP.NET Multitenant Applicationsdl.booktolearn.com/...getting_started_with_asp_net...applications_aff… · ASP.NET is an umbrella under which different frameworks
Page 2: Getting Started with ASP.NET Multitenant Applicationsdl.booktolearn.com/...getting_started_with_asp_net...applications_aff… · ASP.NET is an umbrella under which different frameworks

DanielElliott

Page 3: Getting Started with ASP.NET Multitenant Applicationsdl.booktolearn.com/...getting_started_with_asp_net...applications_aff… · ASP.NET is an umbrella under which different frameworks

GettingStartedwithASP.NETMultitenantApplications

BookRixGmbH&Co.KG81669Munich

Page 4: Getting Started with ASP.NET Multitenant Applicationsdl.booktolearn.com/...getting_started_with_asp_net...applications_aff… · ASP.NET is an umbrella under which different frameworks

TableOfContents

TableofContents

TheStorybehindtheSuccinctlySeriesofBooks

AbouttheAuthor

Chapter1Introduction

Chapter2Setup

Chapter3Concepts

Chapter4ASP.NETWebForms

Chapter5ASP.NETMVC

Chapter6WebServices

Chapter7Routing

Chapter8OWIN

Chapter9ApplicationServices

Chapter10Security

Chapter11DataAccess

Chapter12PuttingItAllTogether

References

DetailedTableofContents

Page 5: Getting Started with ASP.NET Multitenant Applicationsdl.booktolearn.com/...getting_started_with_asp_net...applications_aff… · ASP.NET is an umbrella under which different frameworks

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.

Page 6: Getting Started with ASP.NET Multitenant Applicationsdl.booktolearn.com/...getting_started_with_asp_net...applications_aff… · ASP.NET is an umbrella under which different frameworks

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

Page 7: Getting Started with ASP.NET Multitenant Applicationsdl.booktolearn.com/...getting_started_with_asp_net...applications_aff… · ASP.NET is an umbrella under which different frameworks

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.

Page 8: Getting Started with ASP.NET Multitenant Applicationsdl.booktolearn.com/...getting_started_with_asp_net...applications_aff… · ASP.NET is an umbrella under which different frameworks

Multitenancyrequirements

MultitenancyrequirementsAshigh-levelrequirements,ourapplicationframeworkshouldbeableto:

LoadanumberoftenantsandtheirinformationautomaticallyIdentify,fromeachHTTPrequest,thetenantthatitwasmeantto,oruseafallbackvalueApplyabrandingthemetoeachtenant’srequestedpagesAllowuserstologinonlytothetenanttheyareassociatedwithOffercommonapplicationservicestoalltenantsthatdon’tcollide;forexample,somethingstoredinapplicationcacheforonetenantwillnotbeoverwrittenbyanothertenant

Page 9: Getting Started with ASP.NET Multitenant Applicationsdl.booktolearn.com/...getting_started_with_asp_net...applications_aff… · ASP.NET is an umbrella under which different frameworks

Applicationservices

ApplicationservicesApplication services offer common functionality that .NET/ASP.NET provides

outofthebox,butnotinamultitenantway.ThesewillbediscussedinChapter9,“ApplicationServices.”

Page 10: Getting Started with ASP.NET Multitenant Applicationsdl.booktolearn.com/...getting_started_with_asp_net...applications_aff… · ASP.NET is an umbrella under which different frameworks

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.

Page 11: Getting Started with ASP.NET Multitenant Applicationsdl.booktolearn.com/...getting_started_with_asp_net...applications_aff… · ASP.NET is an umbrella under which different frameworks

Chapter2Setup

Chapter2SetupIntroductionThis chapter will walk you through setting up your development and test

environment.

Page 12: Getting Started with ASP.NET Multitenant Applicationsdl.booktolearn.com/...getting_started_with_asp_net...applications_aff… · ASP.NET is an umbrella under which different frameworks

VisualStudio

VisualStudioInorder tobeable to follow theconceptsandcodedescribedhere,youwill

needaworking installationofVisualStudio;anyversionfrom2012upwardswilldo,includingtheshinynewCommunityEdition.ImyselfhaveusedVisualStudio2013,but youare free touseadifferent version; basically, you’ll needaVisualStudioversionthatcanhandle.NET4.xandNuGetpackages.

Page 13: Getting Started with ASP.NET Multitenant Applicationsdl.booktolearn.com/...getting_started_with_asp_net...applications_aff… · ASP.NET is an umbrella under which different frameworks

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

Page 14: Getting Started with ASP.NET Multitenant Applicationsdl.booktolearn.com/...getting_started_with_asp_net...applications_aff… · ASP.NET is an umbrella under which different frameworks

NHibernate

Unity

UnityBootstrapperforASP.NETMVC

EnterpriseLibraryLoggingApplicationBlock

ASP.NETIdentity(EntityFramework)

ASP.NETIdentity(NHibernate)

OWINIdentity

AspNet.Identity.EntityFramework.Multitenant

Page 15: Getting Started with ASP.NET Multitenant Applicationsdl.booktolearn.com/...getting_started_with_asp_net...applications_aff… · ASP.NET is an umbrella under which different frameworks

WatiN

Page 16: Getting Started with ASP.NET Multitenant Applicationsdl.booktolearn.com/...getting_started_with_asp_net...applications_aff… · ASP.NET is an umbrella under which different frameworks

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.

Page 17: Getting Started with ASP.NET Multitenant Applicationsdl.booktolearn.com/...getting_started_with_asp_net...applications_aff… · ASP.NET is an umbrella under which different frameworks

IIS

IISSettingupamultitenant site in Internet InformationServices (IIS) is easy:

justopentheIISManagerappletandselectSitesAddwebsite.Thenaddahostnamethatyouwishtohandle:

Figure2:SettingupamultitenantsiteinIIS

Nowaddahostnameforall theadditional tenants,select thesite,andclickBindings.

Figure3:Addingadditionalhostnamestoasite

Page 18: Getting Started with ASP.NET Multitenant Applicationsdl.booktolearn.com/...getting_started_with_asp_net...applications_aff… · ASP.NET is an umbrella under which different frameworks

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

Page 19: Getting Started with ASP.NET Multitenant Applicationsdl.booktolearn.com/...getting_started_with_asp_net...applications_aff… · ASP.NET is an umbrella under which different frameworks

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.

Page 20: Getting Started with ASP.NET Multitenant Applicationsdl.booktolearn.com/...getting_started_with_asp_net...applications_aff… · ASP.NET is an umbrella under which different frameworks

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.”

Page 21: Getting Started with ASP.NET Multitenant Applicationsdl.booktolearn.com/...getting_started_with_asp_net...applications_aff… · ASP.NET is an umbrella under which different frameworks

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.

Page 22: Getting Started with ASP.NET Multitenant Applicationsdl.booktolearn.com/...getting_started_with_asp_net...applications_aff… · ASP.NET is an umbrella under which different frameworks

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);

}

Page 23: Getting Started with ASP.NET Multitenant Applicationsdl.booktolearn.com/...getting_started_with_asp_net...applications_aff… · ASP.NET is an umbrella under which different frameworks

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

Page 24: Getting Started with ASP.NET Multitenant Applicationsdl.booktolearn.com/...getting_started_with_asp_net...applications_aff… · ASP.NET is an umbrella under which different frameworks

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

Page 25: Getting Started with ASP.NET Multitenant Applicationsdl.booktolearn.com/...getting_started_with_asp_net...applications_aff… · ASP.NET is an umbrella under which different frameworks

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.

Page 26: Getting Started with ASP.NET Multitenant Applicationsdl.booktolearn.com/...getting_started_with_asp_net...applications_aff… · ASP.NET is an umbrella under which different frameworks

Note: A variation of this pattern coulduse theHTTPvariablePATH_INFO insteadofQUERY_STRING,but thiswouldhaveimpacts,namely,withMVC.

SourceIPaddressstrategy

Figure8:SourceIPstrategy

Nowsupposewewanttodeterminethetenant’snamefromtheIPaddressof

Page 27: Getting Started with ASP.NET Multitenant Applicationsdl.booktolearn.com/...getting_started_with_asp_net...applications_aff… · ASP.NET is an umbrella under which different frameworks

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++)

{

Page 28: Getting Started with ASP.NET Multitenant Applicationsdl.booktolearn.com/...getting_started_with_asp_net...applications_aff… · ASP.NET is an umbrella under which different frameworks

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);

}

Page 29: Getting Started with ASP.NET Multitenant Applicationsdl.booktolearn.com/...getting_started_with_asp_net...applications_aff… · ASP.NET is an umbrella under which different frameworks

}

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);

}

}

Page 30: Getting Started with ASP.NET Multitenant Applicationsdl.booktolearn.com/...getting_started_with_asp_net...applications_aff… · ASP.NET is an umbrella under which different frameworks

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++)

Page 31: Getting Started with ASP.NET Multitenant Applicationsdl.booktolearn.com/...getting_started_with_asp_net...applications_aff… · ASP.NET is an umbrella under which different frameworks

{

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);

}

}

Page 32: Getting Started with ASP.NET Multitenant Applicationsdl.booktolearn.com/...getting_started_with_asp_net...applications_aff… · ASP.NET is an umbrella under which different frameworks

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);

}

Page 33: Getting Started with ASP.NET Multitenant Applicationsdl.booktolearn.com/...getting_started_with_asp_net...applications_aff… · ASP.NET is an umbrella under which different frameworks

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))

{

Page 34: Getting Started with ASP.NET Multitenant Applicationsdl.booktolearn.com/...getting_started_with_asp_net...applications_aff… · ASP.NET is an umbrella under which different frameworks

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

Page 35: Getting Started with ASP.NET Multitenant Applicationsdl.booktolearn.com/...getting_started_with_asp_net...applications_aff… · ASP.NET is an umbrella under which different frameworks

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(‘.’)

Page 36: Getting Started with ASP.NET Multitenant Applicationsdl.booktolearn.com/...getting_started_with_asp_net...applications_aff… · ASP.NET is an umbrella under which different frameworks

.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

Page 37: Getting Started with ASP.NET Multitenant Applicationsdl.booktolearn.com/...getting_started_with_asp_net...applications_aff… · ASP.NET is an umbrella under which different frameworks

easy finding, let’s add some static factories to theTenantsConfiguration classintroducedjustnow:

CodeSample14

publicstaticclassTenantsConfiguration

{

//restgoeshere

publicstaticclassIdentifiers

{

//restgoeshere

publicstaticSourceDomainTenantIdentifierStrategySourceDomain()

{

returnnewSourceDomainTenantIdentifierStrategy();

}

publicstaticSourceIPTenantIdentifierStrategySourceIP()

{

returnnewSourceIPTenantIdentifierStrategy();

}

}

}

Page 38: Getting Started with ASP.NET Multitenant Applicationsdl.booktolearn.com/...getting_started_with_asp_net...applications_aff… · ASP.NET is an umbrella under which different frameworks

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

}

Page 39: Getting Started with ASP.NET Multitenant Applicationsdl.booktolearn.com/...getting_started_with_asp_net...applications_aff… · ASP.NET is an umbrella under which different frameworks

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.

Page 40: Getting Started with ASP.NET Multitenant Applicationsdl.booktolearn.com/...getting_started_with_asp_net...applications_aff… · ASP.NET is an umbrella under which different frameworks

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

Page 41: Getting Started with ASP.NET Multitenant Applicationsdl.booktolearn.com/...getting_started_with_asp_net...applications_aff… · ASP.NET is an umbrella under which different frameworks

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”};

Page 42: Getting Started with ASP.NET Multitenant Applicationsdl.booktolearn.com/...getting_started_with_asp_net...applications_aff… · ASP.NET is an umbrella under which different frameworks

}

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.

Page 43: Getting Started with ASP.NET Multitenant Applicationsdl.booktolearn.com/...getting_started_with_asp_net...applications_aff… · ASP.NET is an umbrella under which different frameworks

Note:Therearelotsofotheroptionsforprovidingthiskindofinformation,likewithattributes,forexample.

Page 44: Getting Started with ASP.NET Multitenant Applicationsdl.booktolearn.com/...getting_started_with_asp_net...applications_aff… · ASP.NET is an umbrella under which different frameworks

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;}

Page 45: Getting Started with ASP.NET Multitenant Applicationsdl.booktolearn.com/...getting_started_with_asp_net...applications_aff… · ASP.NET is an umbrella under which different frameworks

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

{

Page 46: Getting Started with ASP.NET Multitenant Applicationsdl.booktolearn.com/...getting_started_with_asp_net...applications_aff… · ASP.NET is an umbrella under which different frameworks

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;

}

}

}

Page 47: Getting Started with ASP.NET Multitenant Applicationsdl.booktolearn.com/...getting_started_with_asp_net...applications_aff… · ASP.NET is an umbrella under which different frameworks

[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

Page 48: Getting Started with ASP.NET Multitenant Applicationsdl.booktolearn.com/...getting_started_with_asp_net...applications_aff… · ASP.NET is an umbrella under which different frameworks

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

Page 49: Getting Started with ASP.NET Multitenant Applicationsdl.booktolearn.com/...getting_started_with_asp_net...applications_aff… · ASP.NET is an umbrella under which different frameworks

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

Page 50: Getting Started with ASP.NET Multitenant Applicationsdl.booktolearn.com/...getting_started_with_asp_net...applications_aff… · ASP.NET is an umbrella under which different frameworks

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()

Page 51: Getting Started with ASP.NET Multitenant Applicationsdl.booktolearn.com/...getting_started_with_asp_net...applications_aff… · ASP.NET is an umbrella under which different frameworks

{

//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(

Page 52: Getting Started with ASP.NET Multitenant Applicationsdl.booktolearn.com/...getting_started_with_asp_net...applications_aff… · ASP.NET is an umbrella under which different frameworks

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;

}

Page 53: Getting Started with ASP.NET Multitenant Applicationsdl.booktolearn.com/...getting_started_with_asp_net...applications_aff… · ASP.NET is an umbrella under which different frameworks

}

}

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.

Page 54: Getting Started with ASP.NET Multitenant Applicationsdl.booktolearn.com/...getting_started_with_asp_net...applications_aff… · ASP.NET is an umbrella under which different frameworks

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);

Page 55: Getting Started with ASP.NET Multitenant Applicationsdl.booktolearn.com/...getting_started_with_asp_net...applications_aff… · ASP.NET is an umbrella under which different frameworks

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)

{

Page 56: Getting Started with ASP.NET Multitenant Applicationsdl.booktolearn.com/...getting_started_with_asp_net...applications_aff… · ASP.NET is an umbrella under which different frameworks

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

Page 57: Getting Started with ASP.NET Multitenant Applicationsdl.booktolearn.com/...getting_started_with_asp_net...applications_aff… · ASP.NET is an umbrella under which different frameworks

}

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.

Page 58: Getting Started with ASP.NET Multitenant Applicationsdl.booktolearn.com/...getting_started_with_asp_net...applications_aff… · ASP.NET is an umbrella under which different frameworks
Page 59: Getting Started with ASP.NET Multitenant Applicationsdl.booktolearn.com/...getting_started_with_asp_net...applications_aff… · ASP.NET is an umbrella under which different frameworks

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.

Page 60: Getting Started with ASP.NET Multitenant Applicationsdl.booktolearn.com/...getting_started_with_asp_net...applications_aff… · ASP.NET is an umbrella under which different frameworks

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

Page 61: Getting Started with ASP.NET Multitenant Applicationsdl.booktolearn.com/...getting_started_with_asp_net...applications_aff… · ASP.NET is an umbrella under which different frameworks

Unity/CommonServiceLocator

DataModel

EntityFrameworkCodeFirstorNHibernate

Page 62: Getting Started with ASP.NET Multitenant Applicationsdl.booktolearn.com/...getting_started_with_asp_net...applications_aff… · ASP.NET is an umbrella under which different frameworks

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”/

Page 63: Getting Started with ASP.NET Multitenant Applicationsdl.booktolearn.com/...getting_started_with_asp_net...applications_aff… · ASP.NET is an umbrella under which different frameworks

/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

Page 64: Getting Started with ASP.NET Multitenant Applicationsdl.booktolearn.com/...getting_started_with_asp_net...applications_aff… · ASP.NET is an umbrella under which different frameworks

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;

Page 65: Getting Started with ASP.NET Multitenant Applicationsdl.booktolearn.com/...getting_started_with_asp_net...applications_aff… · ASP.NET is an umbrella under which different frameworks

}

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.

Page 66: Getting Started with ASP.NET Multitenant Applicationsdl.booktolearn.com/...getting_started_with_asp_net...applications_aff… · ASP.NET is an umbrella under which different frameworks

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

Page 67: Getting Started with ASP.NET Multitenant Applicationsdl.booktolearn.com/...getting_started_with_asp_net...applications_aff… · ASP.NET is an umbrella under which different frameworks

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.

Page 68: Getting Started with ASP.NET Multitenant Applicationsdl.booktolearn.com/...getting_started_with_asp_net...applications_aff… · ASP.NET is an umbrella under which different frameworks

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);

}

};

Page 69: Getting Started with ASP.NET Multitenant Applicationsdl.booktolearn.com/...getting_started_with_asp_net...applications_aff… · ASP.NET is an umbrella under which different frameworks

}

}

}

}

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

Page 70: Getting Started with ASP.NET Multitenant Applicationsdl.booktolearn.com/...getting_started_with_asp_net...applications_aff… · ASP.NET is an umbrella under which different frameworks

{

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;

Page 71: Getting Started with ASP.NET Multitenant Applicationsdl.booktolearn.com/...getting_started_with_asp_net...applications_aff… · ASP.NET is an umbrella under which different frameworks

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))

Page 72: Getting Started with ASP.NET Multitenant Applicationsdl.booktolearn.com/...getting_started_with_asp_net...applications_aff… · ASP.NET is an umbrella under which different frameworks

{

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();

Page 73: Getting Started with ASP.NET Multitenant Applicationsdl.booktolearn.com/...getting_started_with_asp_net...applications_aff… · ASP.NET is an umbrella under which different frameworks

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

Page 74: Getting Started with ASP.NET Multitenant Applicationsdl.booktolearn.com/...getting_started_with_asp_net...applications_aff… · ASP.NET is an umbrella under which different frameworks

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.

Page 75: Getting Started with ASP.NET Multitenant Applicationsdl.booktolearn.com/...getting_started_with_asp_net...applications_aff… · ASP.NET is an umbrella under which different frameworks

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

Page 76: Getting Started with ASP.NET Multitenant Applicationsdl.booktolearn.com/...getting_started_with_asp_net...applications_aff… · ASP.NET is an umbrella under which different frameworks

[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)

Page 77: Getting Started with ASP.NET Multitenant Applicationsdl.booktolearn.com/...getting_started_with_asp_net...applications_aff… · ASP.NET is an umbrella under which different frameworks

{

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”);

}

Page 78: Getting Started with ASP.NET Multitenant Applicationsdl.booktolearn.com/...getting_started_with_asp_net...applications_aff… · ASP.NET is an umbrella under which different frameworks

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,

Page 79: Getting Started with ASP.NET Multitenant Applicationsdl.booktolearn.com/...getting_started_with_asp_net...applications_aff… · ASP.NET is an umbrella under which different frameworks

””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

Page 80: Getting Started with ASP.NET Multitenant Applicationsdl.booktolearn.com/...getting_started_with_asp_net...applications_aff… · ASP.NET is an umbrella under which different frameworks

base.OnLoad(e);

}

In Chapter 6, we will look at another technique that can also be used forrestrictingaccesses.

Page 81: Getting Started with ASP.NET Multitenant Applicationsdl.booktolearn.com/...getting_started_with_asp_net...applications_aff… · ASP.NET is an umbrella under which different frameworks

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

Page 82: Getting Started with ASP.NET Multitenant Applicationsdl.booktolearn.com/...getting_started_with_asp_net...applications_aff… · ASP.NET is an umbrella under which different frameworks

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.

Page 83: Getting Started with ASP.NET Multitenant Applicationsdl.booktolearn.com/...getting_started_with_asp_net...applications_aff… · ASP.NET is an umbrella under which different frameworks

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

Page 84: Getting Started with ASP.NET Multitenant Applicationsdl.booktolearn.com/...getting_started_with_asp_net...applications_aff… · ASP.NET is an umbrella under which different frameworks

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)

{

Page 85: Getting Started with ASP.NET Multitenant Applicationsdl.booktolearn.com/...getting_started_with_asp_net...applications_aff… · ASP.NET is an umbrella under which different frameworks

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);

}

Page 86: Getting Started with ASP.NET Multitenant Applicationsdl.booktolearn.com/...getting_started_with_asp_net...applications_aff… · ASP.NET is an umbrella under which different frameworks

}

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);

Page 87: Getting Started with ASP.NET Multitenant Applicationsdl.booktolearn.com/...getting_started_with_asp_net...applications_aff… · ASP.NET is an umbrella under which different frameworks

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)

Page 88: Getting Started with ASP.NET Multitenant Applicationsdl.booktolearn.com/...getting_started_with_asp_net...applications_aff… · ASP.NET is an umbrella under which different frameworks

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

{

Page 89: Getting Started with ASP.NET Multitenant Applicationsdl.booktolearn.com/...getting_started_with_asp_net...applications_aff… · ASP.NET is an umbrella under which different frameworks

[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.

Page 90: Getting Started with ASP.NET Multitenant Applicationsdl.booktolearn.com/...getting_started_with_asp_net...applications_aff… · ASP.NET is an umbrella under which different frameworks

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;

Page 91: Getting Started with ASP.NET Multitenant Applicationsdl.booktolearn.com/...getting_started_with_asp_net...applications_aff… · ASP.NET is an umbrella under which different frameworks

Thread.CurrentThread.CurrentUICulture=culture;

HttpContext.Current=ctx;

HttpContext.Current.User=principal;

}

}

Page 92: Getting Started with ASP.NET Multitenant Applicationsdl.booktolearn.com/...getting_started_with_asp_net...applications_aff… · ASP.NET is an umbrella under which different frameworks

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.

Page 93: Getting Started with ASP.NET Multitenant Applicationsdl.booktolearn.com/...getting_started_with_asp_net...applications_aff… · ASP.NET is an umbrella under which different frameworks

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)]

Page 94: Getting Started with ASP.NET Multitenant Applicationsdl.booktolearn.com/...getting_started_with_asp_net...applications_aff… · ASP.NET is an umbrella under which different frameworks

publicclassDateService:IDateService

{

//restgoeshere

}

Note: For a good discussion of theASP.NETcompatibilitymode,checkoutthisarticle.

Page 95: Getting Started with ASP.NET Multitenant Applicationsdl.booktolearn.com/...getting_started_with_asp_net...applications_aff… · ASP.NET is an umbrella under which different frameworks

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.

Page 96: Getting Started with ASP.NET Multitenant Applicationsdl.booktolearn.com/...getting_started_with_asp_net...applications_aff… · ASP.NET is an umbrella under which different frameworks

UnitTesting

UnitTestingWhenever the ASP.NET classic APIs are available, we can follow the

prescriptiondescribedintheUnitTestingsectionofChapter5.ForOWIN,jumptoChapter8.

Page 97: Getting Started with ASP.NET Multitenant Applicationsdl.booktolearn.com/...getting_started_with_asp_net...applications_aff… · ASP.NET is an umbrella under which different frameworks

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.

Page 98: Getting Started with ASP.NET Multitenant Applicationsdl.booktolearn.com/...getting_started_with_asp_net...applications_aff… · ASP.NET is an umbrella under which different frameworks

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);

}

}

Page 99: Getting Started with ASP.NET Multitenant Applicationsdl.booktolearn.com/...getting_started_with_asp_net...applications_aff… · ASP.NET is an umbrella under which different frameworks

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(

Page 100: Getting Started with ASP.NET Multitenant Applicationsdl.booktolearn.com/...getting_started_with_asp_net...applications_aff… · ASP.NET is an umbrella under which different frameworks

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;

}

}

Page 101: Getting Started with ASP.NET Multitenant Applicationsdl.booktolearn.com/...getting_started_with_asp_net...applications_aff… · ASP.NET is an umbrella under which different frameworks

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

Page 102: Getting Started with ASP.NET Multitenant Applicationsdl.booktolearn.com/...getting_started_with_asp_net...applications_aff… · ASP.NET is an umbrella under which different frameworks

(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

Page 103: Getting Started with ASP.NET Multitenant Applicationsdl.booktolearn.com/...getting_started_with_asp_net...applications_aff… · ASP.NET is an umbrella under which different frameworks

!—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

Page 104: Getting Started with ASP.NET Multitenant Applicationsdl.booktolearn.com/...getting_started_with_asp_net...applications_aff… · ASP.NET is an umbrella under which different frameworks

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=(.+)”/

Page 105: Getting Started with ASP.NET Multitenant Applicationsdl.booktolearn.com/...getting_started_with_asp_net...applications_aff… · ASP.NET is an umbrella under which different frameworks

!—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.

Page 106: Getting Started with ASP.NET Multitenant Applicationsdl.booktolearn.com/...getting_started_with_asp_net...applications_aff… · ASP.NET is an umbrella under which different frameworks

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.

Page 107: Getting Started with ASP.NET Multitenant Applicationsdl.booktolearn.com/...getting_started_with_asp_net...applications_aff… · ASP.NET is an umbrella under which different frameworks

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)

{

Page 108: Getting Started with ASP.NET Multitenant Applicationsdl.booktolearn.com/...getting_started_with_asp_net...applications_aff… · ASP.NET is an umbrella under which different frameworks

//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);

}

}

Page 109: Getting Started with ASP.NET Multitenant Applicationsdl.booktolearn.com/...getting_started_with_asp_net...applications_aff… · ASP.NET is an umbrella under which different frameworks

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

Page 110: Getting Started with ASP.NET Multitenant Applicationsdl.booktolearn.com/...getting_started_with_asp_net...applications_aff… · ASP.NET is an umbrella under which different frameworks

classes:

Figure16:ASP.NETIdentitypackageforOWIN

Page 111: Getting Started with ASP.NET Multitenant Applicationsdl.booktolearn.com/...getting_started_with_asp_net...applications_aff… · ASP.NET is an umbrella under which different frameworks

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

}

}

Page 112: Getting Started with ASP.NET Multitenant Applicationsdl.booktolearn.com/...getting_started_with_asp_net...applications_aff… · ASP.NET is an umbrella under which different frameworks

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

Page 113: Getting Started with ASP.NET Multitenant Applicationsdl.booktolearn.com/...getting_started_with_asp_net...applications_aff… · ASP.NET is an umbrella under which different frameworks

publicstaticclassStartup

{

publicstaticvoidConfiguration(IAppBuilderbuilder)

{

builder.UseWebApi(newHttpConfiguration());

builder.UseStaticFiles();

builder.UseDefaultFiles();

//restgoeshere

builder.UseMultitenancyMiddleware();

builder.UseOwinHostHeaderTenantIdentifierStrategy();

//restgoeshere

}

}

Page 114: Getting Started with ASP.NET Multitenant Applicationsdl.booktolearn.com/...getting_started_with_asp_net...applications_aff… · ASP.NET is an umbrella under which different frameworks

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.

Page 115: Getting Started with ASP.NET Multitenant Applicationsdl.booktolearn.com/...getting_started_with_asp_net...applications_aff… · ASP.NET is an umbrella under which different frameworks

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.

Page 116: Getting Started with ASP.NET Multitenant Applicationsdl.booktolearn.com/...getting_started_with_asp_net...applications_aff… · ASP.NET is an umbrella under which different frameworks

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.

Page 117: Getting Started with ASP.NET Multitenant Applicationsdl.booktolearn.com/...getting_started_with_asp_net...applications_aff… · ASP.NET is an umbrella under which different frameworks

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;

Page 118: Getting Started with ASP.NET Multitenant Applicationsdl.booktolearn.com/...getting_started_with_asp_net...applications_aff… · ASP.NET is an umbrella under which different frameworks

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;

}

}

Page 119: Getting Started with ASP.NET Multitenant Applicationsdl.booktolearn.com/...getting_started_with_asp_net...applications_aff… · ASP.NET is an umbrella under which different frameworks

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;

Page 120: Getting Started with ASP.NET Multitenant Applicationsdl.booktolearn.com/...getting_started_with_asp_net...applications_aff… · ASP.NET is an umbrella under which different frameworks

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);

}

}

Page 121: Getting Started with ASP.NET Multitenant Applicationsdl.booktolearn.com/...getting_started_with_asp_net...applications_aff… · ASP.NET is an umbrella under which different frameworks

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.

Page 122: Getting Started with ASP.NET Multitenant Applicationsdl.booktolearn.com/...getting_started_with_asp_net...applications_aff… · ASP.NET is an umbrella under which different frameworks

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);

Page 123: Getting Started with ASP.NET Multitenant Applicationsdl.booktolearn.com/...getting_started_with_asp_net...applications_aff… · ASP.NET is an umbrella under which different frameworks

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

Page 124: Getting Started with ASP.NET Multitenant Applicationsdl.booktolearn.com/...getting_started_with_asp_net...applications_aff… · ASP.NET is an umbrella under which different frameworks

{

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

Page 125: Getting Started with ASP.NET Multitenant Applicationsdl.booktolearn.com/...getting_started_with_asp_net...applications_aff… · ASP.NET is an umbrella under which different frameworks

.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;

}

}

Page 126: Getting Started with ASP.NET Multitenant Applicationsdl.booktolearn.com/...getting_started_with_asp_net...applications_aff… · ASP.NET is an umbrella under which different frameworks

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:

Page 127: Getting Started with ASP.NET Multitenant Applicationsdl.booktolearn.com/...getting_started_with_asp_net...applications_aff… · ASP.NET is an umbrella under which different frameworks

CodeSample97

container.RegisterInstanceIConfiguration(AppSettingsConfiguration.Instance);

Page 128: Getting Started with ASP.NET Multitenant Applicationsdl.booktolearn.com/...getting_started_with_asp_net...applications_aff… · ASP.NET is an umbrella under which different frameworks

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

Page 129: Getting Started with ASP.NET Multitenant Applicationsdl.booktolearn.com/...getting_started_with_asp_net...applications_aff… · ASP.NET is an umbrella under which different frameworks

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:

Page 130: Getting Started with ASP.NET Multitenant Applicationsdl.booktolearn.com/...getting_started_with_asp_net...applications_aff… · ASP.NET is an umbrella under which different frameworks

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

Page 131: Getting Started with ASP.NET Multitenant Applicationsdl.booktolearn.com/...getting_started_with_asp_net...applications_aff… · ASP.NET is an umbrella under which different frameworks

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,

Page 132: Getting Started with ASP.NET Multitenant Applicationsdl.booktolearn.com/...getting_started_with_asp_net...applications_aff… · ASP.NET is an umbrella under which different frameworks

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

Page 133: Getting Started with ASP.NET Multitenant Applicationsdl.booktolearn.com/...getting_started_with_asp_net...applications_aff… · ASP.NET is an umbrella under which different frameworks

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.

Page 134: Getting Started with ASP.NET Multitenant Applicationsdl.booktolearn.com/...getting_started_with_asp_net...applications_aff… · ASP.NET is an umbrella under which different frameworks

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

Page 135: Getting Started with ASP.NET Multitenant Applicationsdl.booktolearn.com/...getting_started_with_asp_net...applications_aff… · ASP.NET is an umbrella under which different frameworks

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

Page 136: Getting Started with ASP.NET Multitenant Applicationsdl.booktolearn.com/...getting_started_with_asp_net...applications_aff… · ASP.NET is an umbrella under which different frameworks

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

Page 137: Getting Started with ASP.NET Multitenant Applicationsdl.booktolearn.com/...getting_started_with_asp_net...applications_aff… · ASP.NET is an umbrella under which different frameworks

/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());

Page 138: Getting Started with ASP.NET Multitenant Applicationsdl.booktolearn.com/...getting_started_with_asp_net...applications_aff… · ASP.NET is an umbrella under which different frameworks

}

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;

Page 139: Getting Started with ASP.NET Multitenant Applicationsdl.booktolearn.com/...getting_started_with_asp_net...applications_aff… · ASP.NET is an umbrella under which different frameworks

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

Page 140: Getting Started with ASP.NET Multitenant Applicationsdl.booktolearn.com/...getting_started_with_asp_net...applications_aff… · ASP.NET is an umbrella under which different frameworks

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,

Page 141: Getting Started with ASP.NET Multitenant Applicationsdl.booktolearn.com/...getting_started_with_asp_net...applications_aff… · ASP.NET is an umbrella under which different frameworks

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

Page 142: Getting Started with ASP.NET Multitenant Applicationsdl.booktolearn.com/...getting_started_with_asp_net...applications_aff… · ASP.NET is an umbrella under which different frameworks

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

Page 143: Getting Started with ASP.NET Multitenant Applicationsdl.booktolearn.com/...getting_started_with_asp_net...applications_aff… · ASP.NET is an umbrella under which different frameworks

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

Page 144: Getting Started with ASP.NET Multitenant Applicationsdl.booktolearn.com/...getting_started_with_asp_net...applications_aff… · ASP.NET is an umbrella under which different frameworks

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)

Page 145: Getting Started with ASP.NET Multitenant Applicationsdl.booktolearn.com/...getting_started_with_asp_net...applications_aff… · ASP.NET is an umbrella under which different frameworks

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

Page 146: Getting Started with ASP.NET Multitenant Applicationsdl.booktolearn.com/...getting_started_with_asp_net...applications_aff… · ASP.NET is an umbrella under which different frameworks

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)

Page 147: Getting Started with ASP.NET Multitenant Applicationsdl.booktolearn.com/...getting_started_with_asp_net...applications_aff… · ASP.NET is an umbrella under which different frameworks

{

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;

Page 148: Getting Started with ASP.NET Multitenant Applicationsdl.booktolearn.com/...getting_started_with_asp_net...applications_aff… · ASP.NET is an umbrella under which different frameworks

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()

{

}

}

Page 149: Getting Started with ASP.NET Multitenant Applicationsdl.booktolearn.com/...getting_started_with_asp_net...applications_aff… · ASP.NET is an umbrella under which different frameworks

[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;}

}

Page 150: Getting Started with ASP.NET Multitenant Applicationsdl.booktolearn.com/...getting_started_with_asp_net...applications_aff… · ASP.NET is an umbrella under which different frameworks

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

Page 151: Getting Started with ASP.NET Multitenant Applicationsdl.booktolearn.com/...getting_started_with_asp_net...applications_aff… · ASP.NET is an umbrella under which different frameworks

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).

Page 152: Getting Started with ASP.NET Multitenant Applicationsdl.booktolearn.com/...getting_started_with_asp_net...applications_aff… · ASP.NET is an umbrella under which different frameworks

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.

Page 153: Getting Started with ASP.NET Multitenant Applicationsdl.booktolearn.com/...getting_started_with_asp_net...applications_aff… · ASP.NET is an umbrella under which different frameworks

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:

Page 154: Getting Started with ASP.NET Multitenant Applicationsdl.booktolearn.com/...getting_started_with_asp_net...applications_aff… · ASP.NET is an umbrella under which different frameworks

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)

Page 155: Getting Started with ASP.NET Multitenant Applicationsdl.booktolearn.com/...getting_started_with_asp_net...applications_aff… · ASP.NET is an umbrella under which different frameworks

{

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

Page 156: Getting Started with ASP.NET Multitenant Applicationsdl.booktolearn.com/...getting_started_with_asp_net...applications_aff… · ASP.NET is an umbrella under which different frameworks

.GetInstanceIConfiguration();

varkey=config.GetValue(“GoogleAnalyticsKey”);

html.Raw(String.Format(Script,key));

}

}

Itwouldthenbeinvokedasthis,onalocalorsharedview:

CodeSample125

@Html.GoogleAnalytics()

Andthat’s it!Eachtenantwillget itsownpropertypageonGoogleAnalytics,andyoucanstartmonitoringthem.

Page 157: Getting Started with ASP.NET Multitenant Applicationsdl.booktolearn.com/...getting_started_with_asp_net...applications_aff… · ASP.NET is an umbrella under which different frameworks

Chapter10Security

Chapter10SecurityIntroductionAll applications need security enforcing. Multitenant applications have

additional requirements, in the sense that something that is allowed for sometenantmaybeshouldn’tbeforothertenants.Thischaptertalksaboutsomeofthesecuritymechanismstoconsiderwhenwritingmultitenantapplications.

Page 158: Getting Started with ASP.NET Multitenant Applicationsdl.booktolearn.com/...getting_started_with_asp_net...applications_aff… · ASP.NET is an umbrella under which different frameworks

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:

Page 159: Getting Started with ASP.NET Multitenant Applicationsdl.booktolearn.com/...getting_started_with_asp_net...applications_aff… · ASP.NET is an umbrella under which different frameworks

Figure30:ASP.NETMembershipandRoleproviderschema

Keytounderstandingthisschemaarethesetables:

Table7:ASP.NETMembershipandRoleprovidertablesandtheirpurposes

Table

Purpose

aspnet_Applications

StoresalltheapplicationsthattheMembershipandRoleprovidersknowof

aspnet_Users

Storesalltheregisteredusers,associatedwithanapplication

aspnet_Roles

Storesalloftheroles

aspnet_Membership

Additionalinformationaboutregisteredusers

Page 160: Getting Started with ASP.NET Multitenant Applicationsdl.booktolearn.com/...getting_started_with_asp_net...applications_aff… · ASP.NET is an umbrella under which different frameworks

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

Page 161: Getting Started with ASP.NET Multitenant Applicationsdl.booktolearn.com/...getting_started_with_asp_net...applications_aff… · ASP.NET is an umbrella under which different frameworks

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”

Page 162: Getting Started with ASP.NET Multitenant Applicationsdl.booktolearn.com/...getting_started_with_asp_net...applications_aff… · ASP.NET is an umbrella under which different frameworks

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

Page 163: Getting Started with ASP.NET Multitenant Applicationsdl.booktolearn.com/...getting_started_with_asp_net...applications_aff… · ASP.NET is an umbrella under which different frameworks

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:

Page 164: Getting Started with ASP.NET Multitenant Applicationsdl.booktolearn.com/...getting_started_with_asp_net...applications_aff… · ASP.NET is an umbrella under which different frameworks

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)

Page 165: Getting Started with ASP.NET Multitenant Applicationsdl.booktolearn.com/...getting_started_with_asp_net...applications_aff… · ASP.NET is an umbrella under which different frameworks

{

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.

Page 166: Getting Started with ASP.NET Multitenant Applicationsdl.booktolearn.com/...getting_started_with_asp_net...applications_aff… · ASP.NET is an umbrella under which different frameworks

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.

Page 167: Getting Started with ASP.NET Multitenant Applicationsdl.booktolearn.com/...getting_started_with_asp_net...applications_aff… · ASP.NET is an umbrella under which different frameworks

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.

Page 168: Getting Started with ASP.NET Multitenant Applicationsdl.booktolearn.com/...getting_started_with_asp_net...applications_aff… · ASP.NET is an umbrella under which different frameworks

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);

Page 169: Getting Started with ASP.NET Multitenant Applicationsdl.booktolearn.com/...getting_started_with_asp_net...applications_aff… · ASP.NET is an umbrella under which different frameworks

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

Page 170: Getting Started with ASP.NET Multitenant Applicationsdl.booktolearn.com/...getting_started_with_asp_net...applications_aff… · ASP.NET is an umbrella under which different frameworks

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()

Page 171: Getting Started with ASP.NET Multitenant Applicationsdl.booktolearn.com/...getting_started_with_asp_net...applications_aff… · ASP.NET is an umbrella under which different frameworks

{

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

Page 172: Getting Started with ASP.NET Multitenant Applicationsdl.booktolearn.com/...getting_started_with_asp_net...applications_aff… · ASP.NET is an umbrella under which different frameworks

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

Page 173: Getting Started with ASP.NET Multitenant Applicationsdl.booktolearn.com/...getting_started_with_asp_net...applications_aff… · ASP.NET is an umbrella under which different frameworks

}

}

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()

Page 174: Getting Started with ASP.NET Multitenant Applicationsdl.booktolearn.com/...getting_started_with_asp_net...applications_aff… · ASP.NET is an umbrella under which different frameworks

{

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

Page 175: Getting Started with ASP.NET Multitenant Applicationsdl.booktolearn.com/...getting_started_with_asp_net...applications_aff… · ASP.NET is an umbrella under which different frameworks

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()

Page 176: Getting Started with ASP.NET Multitenant Applicationsdl.booktolearn.com/...getting_started_with_asp_net...applications_aff… · ASP.NET is an umbrella under which different frameworks

{

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);

Page 177: Getting Started with ASP.NET Multitenant Applicationsdl.booktolearn.com/...getting_started_with_asp_net...applications_aff… · ASP.NET is an umbrella under which different frameworks

}

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

{

Page 178: Getting Started with ASP.NET Multitenant Applicationsdl.booktolearn.com/...getting_started_with_asp_net...applications_aff… · ASP.NET is an umbrella under which different frameworks

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();

}

Page 179: Getting Started with ASP.NET Multitenant Applicationsdl.booktolearn.com/...getting_started_with_asp_net...applications_aff… · ASP.NET is an umbrella under which different frameworks

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

Page 180: Getting Started with ASP.NET Multitenant Applicationsdl.booktolearn.com/...getting_started_with_asp_net...applications_aff… · ASP.NET is an umbrella under which different frameworks

}

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

Page 181: Getting Started with ASP.NET Multitenant Applicationsdl.booktolearn.com/...getting_started_with_asp_net...applications_aff… · ASP.NET is an umbrella under which different frameworks

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}”,

Page 182: Getting Started with ASP.NET Multitenant Applicationsdl.booktolearn.com/...getting_started_with_asp_net...applications_aff… · ASP.NET is an umbrella under which different frameworks

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

{

Page 183: Getting Started with ASP.NET Multitenant Applicationsdl.booktolearn.com/...getting_started_with_asp_net...applications_aff… · ASP.NET is an umbrella under which different frameworks

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(

Page 184: Getting Started with ASP.NET Multitenant Applicationsdl.booktolearn.com/...getting_started_with_asp_net...applications_aff… · ASP.NET is an umbrella under which different frameworks

newPerRequestLifetimeManager());

//getareferencetoanewinstanceusingCommonServiceLocator

using(varrepository=ServiceLocator.Current.GetInstanceIRepository())

{

//querysomeentity

varitems=repository.AllMyEntity().ToList();

}

Note:NoticeagainthePerRequestLifetimeManagerlifetimemanager.

Page 185: Getting Started with ASP.NET Multitenant Applicationsdl.booktolearn.com/...getting_started_with_asp_net...applications_aff… · ASP.NET is an umbrella under which different frameworks

Chapter12PuttingItAllTogether

Chapter12PuttingItAllTogetherClassmodelTheframeworkclassesthatweusedintheexamplesare:

Figure38:Highlevelinterfaces

Andtheircommonimplementations:

Figure39:Typicalimplementationsofhighlevelinterfaces

Page 186: Getting Started with ASP.NET Multitenant Applicationsdl.booktolearn.com/...getting_started_with_asp_net...applications_aff… · ASP.NET is an umbrella under which different frameworks

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

Page 187: Getting Started with ASP.NET Multitenant Applicationsdl.booktolearn.com/...getting_started_with_asp_net...applications_aff… · ASP.NET is an umbrella under which different frameworks

Choosingtherighttenantlocationstrategy

ChoosingtherighttenantlocationstrategyWhenchoosingatenantlocation(andregistrationstrategy),youneedtotake

intoaccountthesequestions:

Doyouneedtoregisternewtenantsdynamically?Howmucheffortisacceptableforregisteringnewtenants(e.g.,changingsomeconfigurationfile,recompilingtheapplication,restartingit)?Aresomeaspectsoftenantsonlyconfigurablethroughconfiguration?

Youneed toweighall of theseconstraintsbeforeyoumakeadecision.Thefollowingpicturesummarizesthis:

Figure41:Simpledecisionflowforchoosingthetenantlocationstrategy

Page 188: Getting Started with ASP.NET Multitenant Applicationsdl.booktolearn.com/...getting_started_with_asp_net...applications_aff… · ASP.NET is an umbrella under which different frameworks

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.

Page 189: Getting Started with ASP.NET Multitenant Applicationsdl.booktolearn.com/...getting_started_with_asp_net...applications_aff… · ASP.NET is an umbrella under which different frameworks

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.

Page 190: Getting Started with ASP.NET Multitenant Applicationsdl.booktolearn.com/...getting_started_with_asp_net...applications_aff… · ASP.NET is an umbrella under which different frameworks

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.

Page 191: Getting Started with ASP.NET Multitenant Applicationsdl.booktolearn.com/...getting_started_with_asp_net...applications_aff… · ASP.NET is an umbrella under which different frameworks

References

References

AsEasyAsFallingOffaLog:UsingtheLoggingApplicationBlockASP.NET2.0ProviderModelASP.NET2.0ProviderModel:IntroductiontotheProviderModelCommonServiceLocatorConceptmappingbetweenSQLServerandOracleConfiguring/MappingPropertiesandTypeswiththeFluentAPICSSZenGardenDevelopingMulti-tenantApplicationsfortheCloud,3rdEditionDifferenceBetweenCNameandARecordEnterpriseLibrary-DataAccessBlockEnterpriseLibrary-LoggingApplicationBlockEnterpriseLibrary-LoggingApplicationBlockDatabaseProviderEntityFrameworkEntityFrameworkCodeFirstInheritanceHands-OnLabsforMicrosoftEnterpriseLibrary6HowtoviewtheASP.NETPerformanceCountersAvailableonYourComputerKatanaProjectMonitoringASP.NETApplicationPerformanceMuchADOaboutDataAccess:UsingtheDataAccessApplicationBlockMultitenancyNHibernateOWINOWINMiddlewareintheIISintegratedpipelinePerformanceCountersforASP.NETPerfViewSingleTableInheritanceUnderstandingmulti-tenancyinSharePointServer2013WatiN

Page 192: Getting Started with ASP.NET Multitenant Applicationsdl.booktolearn.com/...getting_started_with_asp_net...applications_aff… · ASP.NET is an umbrella under which different frameworks

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

Page 193: Getting Started with ASP.NET Multitenant Applicationsdl.booktolearn.com/...getting_started_with_asp_net...applications_aff… · ASP.NET is an umbrella under which different frameworks

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

Page 194: Getting Started with ASP.NET Multitenant Applicationsdl.booktolearn.com/...getting_started_with_asp_net...applications_aff… · ASP.NET is an umbrella under which different frameworks

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

Page 195: Getting Started with ASP.NET Multitenant Applicationsdl.booktolearn.com/...getting_started_with_asp_net...applications_aff… · ASP.NET is an umbrella under which different frameworks
Page 196: Getting Started with ASP.NET Multitenant Applicationsdl.booktolearn.com/...getting_started_with_asp_net...applications_aff… · ASP.NET is an umbrella under which different frameworks

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.