table of contents - sumit jangidsumitjangid.com/uploads/books/.net/the little aspnet core... ·...
TRANSCRIPT
1.1
1.2
1.2.1
1.2.2
1.2.3
1.3
1.3.1
1.3.2
1.3.3
1.3.4
1.3.5
1.3.6
1.3.7
1.4
1.5
1.5.1
1.5.2
1.5.3
1.5.4
1.6
1.6.1
1.6.2
1.7
TableofContentsIntroduction
Yourfirstapplication
GettheSDK
HelloWorldinC#
CreateanASP.NETCoreproject
MVCbasics
Createacontroller
Createmodels
Createaview
Addaserviceclass
Usedependencyinjection
Finishthecontroller
Updatethelayout
Addexternalpackages
Useadatabase
Connecttoadatabase
Updatethecontext
Createamigration
Createanewserviceclass
Addmorefeatures
Addnewto-doitems
Completeitemswithacheckbox
Securityandidentity
2
1.7.1
1.7.2
1.7.3
1.7.4
1.8
1.8.1
1.8.2
1.9
1.9.1
1.9.2
1.10
Requireauthentication
Usingidentityintheapplication
Authorizationwithroles
Moreresources
Automatedtesting
Unittesting
Integrationtesting
Deploytheapplication
DeploytoAzure
DeploywithDocker
Conclusion
3
TheLittleASP.NETCoreBookbyNateBarbettini
Copyright©2018.Allrightsreserved.
ISBN:978-1-387-75615-5
ReleasedundertheCreativeCommonsAttribution4.0license.Youarefreetoshare,copy,andredistributethisbookinanyformat,orremixandtransformitforanypurpose(evencommercially).Youmustgiveappropriatecreditandprovidealinktothelicense.
Formoreinformation,visithttps://creativecommons.org/licenses/by/4.0/
IntroductionThanksforpickingupTheLittleASP.NETCoreBook!IwrotethisshortbooktohelpdevelopersandpeopleinterestedinwebprogramminglearnaboutASP.NETCore,anewframeworkforbuildingwebapplicationsandAPIs.
TheLittleASP.NETCoreBookisstructuredasatutorial.You'llbuildanapplicationfromstarttofinishandlearn:
ThebasicsoftheMVC(Model-View-Controller)patternHowfront-endcode(HTML,CSS,JavaScript)workstogetherwithback-endcodeWhatdependencyinjectionisandwhyit'susefulHowtoreadandwritedatatoadatabaseHowtoaddlog-in,registration,andsecurityHowtodeploytheapplicationtotheweb
Introduction
4
Don'tworry,youdon'tneedtoknowanythingaboutASP.NETCore(oranyoftheabove)togetstarted.
BeforeyoubeginThecodeforthefinishedversionoftheapplicationyou'llbuildisavailableonGitHub:
https://www.github.com/nbarbettini/little-aspnetcore-todo
Feelfreetodownloaditifyouwanttoseethefinishedproduct,orcompareasyouwriteyourowncode.
Thebookitselfisupdatedfrequentlywithbugfixesandnewcontent.Ifyou'rereadingaPDF,e-book,orprintversion,checktheofficialwebsite(littleasp.net/book)toseeifthere'sanupdatedversionavailable.Theverylastpageofthebookcontainsversioninformationandachangelog.
Readinginyourownlanguage
Thankstosomefantasticmultilingualcontributors,theLittleASP.NETCoreBookhasbeentranslatedintootherlanguages:
Turkish:https://sahinyanlik.gitbooks.io/kisa-asp-net-core-kitabi/
Chinese:https://windsting.github.io/little-aspnetcore-book/book/
WhothisbookisforIfyou'renewtoprogramming,thisbookwillintroduceyoutothepatternsandconceptsusedtobuildmodernwebapplications.You'lllearnhowtobuildawebapp(andhowthebigpiecesfittogether)by
Introduction
5
buildingsomethingfromscratch!Whilethislittlebookwon'tbeabletocoverabsolutelyeverythingyouneedtoknowaboutprogramming,it'llgiveyouastartingpointsoyoucanlearnmoreadvancedtopics.
IfyoualreadycodeinabackendlanguagelikeNode,Python,Ruby,Go,orJava,you'llnoticealotoffamiliarideaslikeMVC,viewtemplates,anddependencyinjection.ThecodewillbeinC#,butitwon'tlooktoodifferentfromwhatyoualreadyknow.
Ifyou'reanASP.NETMVCdeveloper,you'llfeelrightathome!ASP.NETCoreaddssomenewtoolsandreuses(andsimplifies)thethingsyoualreadyknow.I'llpointoutsomeofthedifferencesbelow.
Nomatterwhatyourpreviousexperiencewithwebprogramming,thisbookwillteachyoueverythingyouneedtocreateasimpleandusefulwebapplicationinASP.NETCore.You'lllearnhowtobuildfunctionalityusingbackendandfrontendcode,howtointeractwithadatabase,andhowtodeploytheapptotheworld.
WhatisASP.NETCore?ASP.NETCoreisawebframeworkcreatedbyMicrosoftforbuildingwebapplications,APIs,andmicroservices.ItusescommonpatternslikeMVC(Model-View-Controller),dependencyinjection,andarequestpipelinecomprisedofmiddleware.It'sopen-sourceundertheApache2.0license,whichmeansthesourcecodeisfreelyavailable,andthecommunityisencouragedtocontributebugfixesandnewfeatures.
ASP.NETCorerunsontopofMicrosoft's.NETruntime,similartotheJavaVirtualMachine(JVM)ortheRubyinterpreter.YoucanwriteASP.NETCoreapplicationsinanumberoflanguages(C#,VisualBasic,F#).C#isthemostpopularchoice,andit'swhatI'lluseinthisbook.YoucanbuildandrunASP.NETCoreapplicationsonWindows,Mac,andLinux.
Introduction
6
Whydoweneedanotherwebframework?Therearealotofgreatwebframeworkstochoosefromalready:Node/Express,Spring,RubyonRails,Django,Laravel,andmanymore.WhatadvantagesdoesASP.NETCorehave?
Speed.ASP.NETCoreisfast.Because.NETcodeiscompiled,itexecutesmuchfasterthancodeininterpretedlanguageslikeJavaScriptorRuby.ASP.NETCoreisalsooptimizedformultithreadingandasynchronoustasks.It'scommontoseea5-10xspeedimprovementovercodewritteninNode.js.
Ecosystem.ASP.NETCoremaybenew,but.NEThasbeenaroundforalongtime.TherearethousandsofpackagesavailableonNuGet(the.NETpackagemanager;thinknpm,Rubygems,orMaven).TherearealreadypackagesavailableforJSONdeserialization,databaseconnectors,PDFgeneration,oralmostanythingelseyoucanthinkof.
Security.TheteamatMicrosofttakessecurityseriously,andASP.NETCoreisbuilttobesecurefromthegroundup.Ithandlesthingslikesanitizinginputdataandpreventingcross-siterequestforgery(CSRF)attacks,soyoudon'thaveto.Youalsogetthebenefitofstatictypingwiththe.NETcompiler,whichislikehavingaveryparanoidlinterturnedonatalltimes.Thismakesithardertodosomethingyoudidn'tintendwithavariableorchunkofdata.
.NETCoreand.NETStandardThroughoutthisbook,you'llbelearningaboutASP.NETCore(thewebframework).I'lloccasionallymentionthe.NETruntime,thesupportinglibrarythatruns.NETcode.IfthisalreadysoundslikeGreektoyou,just
Introduction
7
skiptothenextchapter!
Youmayalsohearabout.NETCoreand.NETStandard.Thenaminggetsconfusing,sohere'sasimpleexplanation:
.NETStandardisaplatform-agnosticinterfacethatdefinesfeaturesandAPIs.It'simportanttonotethat.NETStandarddoesn'trepresentanyactualcodeorfunctionality,justtheAPIdefinition.Therearedifferent"versions"orlevelsof.NETStandardthatreflecthowmanyAPIsareavailable(orhowwidetheAPIsurfaceareais).Forexample,.NETStandard2.0hasmoreAPIsavailablethan.NETStandard1.5,whichhasmoreAPIsthan.NETStandard1.0.
.NETCoreisthe.NETruntimethatcanbeinstalledonWindows,Mac,orLinux.ItimplementstheAPIsdefinedinthe.NETStandardinterfacewiththeappropriateplatform-specificcodeoneachoperatingsystem.Thisiswhatyou'llinstallonyourownmachinetobuildandrunASP.NETCoreapplications.
Andjustforgoodmeasure,.NETFrameworkisadifferentimplementationof.NETStandardthatisWindows-only.Thiswastheonly.NETruntimeuntil.NETCorecamealongandbrought.NETtoMacandLinux.ASP.NETCorecanalsorunonWindows-only.NETFramework,butIwon'ttouchonthistoomuch.
Ifyou'reconfusedbyallthisnaming,noworries!We'llgettosomerealcodeinabit.
AnotetoASP.NET4developersIfyouhaven'tusedapreviousversionofASP.NET,skipaheadtothenextchapter.
Introduction
8
ASP.NETCoreisacompleteground-uprewriteofASP.NET,withafocusonmodernizingtheframeworkandfinallydecouplingitfromSystem.Web,IIS,andWindows.IfyourememberalltheOWIN/KatanastufffromASP.NET4,you'realreadyhalfwaythere:theKatanaprojectbecameASP.NET5whichwasultimatelyrenamedtoASP.NETCore.
BecauseoftheKatanalegacy,theStartupclassisfrontandcenter,andthere'snomoreApplication_StartorGlobal.asax.Theentirepipelineisdrivenbymiddleware,andthere'snolongerasplitbetweenMVCandWebAPI:controllerscansimplyreturnviews,statuscodes,ordata.Dependencyinjectioncomesbakedin,soyoudon'tneedtoinstallandconfigureacontainerlikeStructureMaporNinjectifyoudon'twantto.Andtheentireframeworkhasbeenoptimizedforspeedandruntimeefficiency.
Alright,enoughintroduction.Let'sdiveintoASP.NETCore!
Introduction
9
YourfirstapplicationReadytobuildyourfirstwebappwithASP.NETCore?You'llneedtogatherafewthingsfirst:
Yourfavoritecodeeditor.YoucanuseAtom,Sublime,Notepad,orwhatevereditoryoupreferwritingcodein.Ifyoudon'thaveafavorite,giveVisualStudioCodeatry.It'safree,cross-platformcodeeditorthathasrichsupportforwritingC#,JavaScript,HTML,andmore.Justsearchfor"downloadvisualstudiocode"andfollowtheinstructions.
Ifyou'reonWindows,youcanalsouseVisualStudiotobuildASP.NETCoreapplications.You'llneedVisualStudio2017version15.3orlater(thefreeCommunityEditionisfine).VisualStudiohasgreatcodecompletionandrefactoringsupportforC#,althoughVisualStudioCodeisclosebehind.
The.NETCoreSDK.Regardlessoftheeditororplatformyou'reusing,you'llneedtoinstallthe.NETCoreSDK,whichincludestheruntime,baselibraries,andcommandlinetoolsyouneedforbuildingASP.NETCoreapplications.TheSDKcanbeinstalledonWindows,Mac,orLinux.
Onceyou'vedecidedonaneditor,you'llneedtogettheSDK.
Yourfirstapplication
10
GettheSDKSearchfor"download.netcore"andfollowtheinstructionsonMicrosoft'sdownloadpagetogetthe.NETCoreSDK.AftertheSDKhasfinishedinstalling,openuptheTerminal(orPowerShellonWindows)andusethedotnetcommandlinetool(alsocalledaCLI)tomakesureeverythingisworking:
dotnet--version
2.1.104
Youcangetmoreinformationaboutyourplatformwiththe--infoflag:
dotnet--info
.NETCommandLineTools(2.1.104)
ProductInformation:
Version:2.1.104
CommitSHA-1hash:48ec687460
RuntimeEnvironment:
OSName:MacOSX
OSVersion:10.13
(moredetails...)
Ifyouseeoutputliketheabove,you'rereadytogo!
GettheSDK
11
HelloWorldinC#BeforeyoudiveintoASP.NETCore,trycreatingandrunningasimpleC#application.
Youcandothisallfromthecommandline.First,openuptheTerminal(orPowerShellonWindows).Navigatetothelocationyouwanttostoreyourprojects,suchasyourDocumentsdirectory:
cdDocuments
Usethedotnetcommandtocreateanewproject:
dotnetnewconsole-oCsharpHelloWorld
Thedotnetnewcommandcreatesanew.NETprojectinC#bydefault.Theconsoleparameterselectsatemplateforaconsoleapplication(aprogramthatoutputstexttothescreen).The-oCsharpHelloWorldparametertellsdotnetnewtocreateanewdirectorycalledCsharpHelloWorldforalltheprojectfiles.Moveintothisnewdirectory:
cdCsharpHelloWorld
dotnetnewconsolecreatesabasicC#programthatwritesthetextHelloWorld!tothescreen.Theprogramiscomprisedoftwofiles:aprojectfile(witha.csprojextension)andaC#codefile(witha.csextension).Ifyouopentheformerinatextorcodeeditor,you'llseethis:
CsharpHelloWorld.csproj
<ProjectSdk="Microsoft.NET.Sdk">
HelloWorldinC#
12
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp2.0</TargetFramework>
</PropertyGroup>
</Project>
TheprojectfileisXML-basedanddefinessomemetadataabouttheproject.Later,whenyoureferenceotherpackages,thosewillbelistedhere(similartoapackage.jsonfilefornpm).Youwon'thavetoeditthisfilebyhandveryoften.
Program.cs
usingSystem;
namespaceCsharpHelloWorld
{
classProgram
{
staticvoidMain(string[]args)
{
Console.WriteLine("HelloWorld!");
}
}
}
staticvoidMainistheentrypointmethodofaC#program,andbyconventionit'splacedinaclass(atypeofcodestructureormodule)calledProgram.Theusingstatementatthetopimportsthebuilt-inSystemclassesfrom.NETandmakesthemavailabletothecodeinyourclass.
Frominsidetheprojectdirectory,usedotnetruntoruntheprogram.You'llseetheoutputwrittentotheconsoleafterthecodecompiles:
dotnetrun
HelloWorldinC#
13
HelloWorld!
That'sallittakestoscaffoldandruna.NETprogram!Next,you'lldothesamethingforanASP.NETCoreapplication.
HelloWorldinC#
14
CreateanASP.NETCoreprojectIfyou'restillinthedirectoryyoucreatedfortheHelloWorldsample,movebackuptoyourDocumentsorhomedirectory:
cd..
Next,createanewdirectorytostoreyourentireproject,andmoveintoit:
mkdirAspNetCoreTodo
cdAspNetCoreTodo
Next,createanewprojectwithdotnetnew,thistimewithsomeextraoptions:
dotnetnewmvc--authIndividual-oAspNetCoreTodo
cdAspNetCoreTodo
Thiscreatesanewprojectfromthemvctemplate,andaddssomeadditionalauthenticationandsecuritybitstotheproject.(I'llcoversecurityintheSecurityandidentitychapter.)
YoumightbewonderingwhyyouhaveadirectorycalledAspNetCoreTodoinsideanotherdirectorycalledAspNetCoreTodo.Thetop-levelor"root"directorycancontainoneormoreprojectdirectories.Therootdirectoryissometimescalledasolutiondirectory.Later,you'lladdmoreprojectdirectoriesside-by-sidewiththeAspNetCoreTodoprojectdirectory,allwithinasinglerootsolutiondirectory.
CreateanASP.NETCoreproject
15
You'llseequiteafewfilesshowupinthenewprojectdirectory.Onceyoucdintothenewdirectory,allyouhavetodoisruntheproject:
dotnetrun
Nowlisteningon:http://localhost:5000
Applicationstarted.PressCtrl+Ctoshutdown.
Insteadofprintingtotheconsoleandexiting,thisprogramstartsawebserverandwaitsforrequestsonport5000.
Openyourwebbrowserandnavigatetohttp://localhost:5000.You'llseethedefaultASP.NETCoresplashpage,whichmeansyourprojectisworking!Whenyou'redone,pressCtrl-Cintheterminalwindowtostoptheserver.
ThepartsofanASP.NETCoreproject
Thedotnetnewmvctemplategeneratesanumberoffilesanddirectoriesforyou.Herearethemostimportantthingsyougetoutofthebox:
TheProgram.csandStartup.csfilessetupthewebserverandASP.NETCorepipeline.TheStartupclassiswhereyoucanaddmiddlewarethathandlesandmodifiesincomingrequests,andservesthingslikestaticcontentorerrorpages.It'salsowhereyouaddyourownservicestothedependencyinjectioncontainer(moreonthislater).
TheModels,Views,andControllersdirectoriescontainthecomponentsoftheModel-View-Controller(MVC)architecture.You'llexploreallthreeinthenextchapter.
CreateanASP.NETCoreproject
16
ThewwwrootdirectorycontainsstaticassetslikeCSS,JavaScript,andimagefiles.Filesinwwwrootwillbeservedasstaticcontent,andcanbebundledandminifiedautomatically.
Theappsettings.jsonfilecontainsconfigurationsettingsASP.NETCorewillloadonstartup.Youcanusethistostoredatabaseconnectionstringsorotherthingsthatyoudon'twanttohard-code.
TipsforVisualStudioCode
Ifyou'reusingVisualStudioCodeforthefirsttime,hereareacoupleofhelpfultipstogetyoustarted:
Opentheprojectrootfolder:InVisualStudioCode,chooseFile-OpenorFile-OpenFolder.OpentheAspNetCoreTodofolder(therootdirectory),nottheinnerprojectdirectory.IfVisualStudioCodepromptsyoutoinstallmissingfiles,clickYestoaddthem.
F5torun(anddebugbreakpoints):Withyourprojectopen,pressF5toruntheprojectindebugmode.Thisisthesameasdotnetrunonthecommandline,butyouhavethebenefitofsettingbreakpointsinyourcodebyclickingontheleftmargin:
Lightbulbtofixproblems:Ifyourcodecontainsredsquiggles(compilererrors),putyourcursoronthecodethat'sredandlookforthelightbulbiconontheleftmargin.Thelightbulbmenuwillsuggest
CreateanASP.NETCoreproject
17
commonfixes,likeaddingamissingusingstatementtoyourcode:
Compilequickly:UsetheshortcutCommand-Shift-BorControl-Shift-BtoruntheBuildtask,whichdoesthesamethingasdotnetbuild.
ThesetipsapplytoVisualStudio(notCode)onWindowstoo.Ifyou'reusingVisualStudio,you'llneedtoopenthe.csprojprojectfiledirectly.VisualStudiowilllaterpromptyoutosavetheSolutionfile,whichyoushouldsaveintherootdirectory(thefirstAspNetCoreTodofolder).YoucanalsocreateanASP.NETCoreprojectdirectlywithinVisualStudiousingthetemplatesinFile-NewProject.
AnoteaboutGit
IfyouuseGitorGitHubtomanageyoursourcecode,nowisagoodtimetodogitinitandinitializeaGitrepositoryintheprojectrootdirectory:
cd..
gitinit
Makesureyouadda.gitignorefilethatignoresthebinandobjdirectories.TheVisualStudiotemplateonGitHub'sgitignoretemplaterepo(https://github.com/github/gitignore)worksgreat.
CreateanASP.NETCoreproject
18
There'splentymoretoexplore,solet'sdiveinandstartbuildinganapplication!
CreateanASP.NETCoreproject
19
MVCbasicsInthischapter,you'llexploretheMVCsysteminASP.NETCore.MVC(Model-View-Controller)isapatternforbuildingwebapplicationsthat'susedinalmosteverywebframework(RubyonRailsandExpressarepopularexamples),plusfrontendJavaScriptframeworkslikeAngular.MobileappsoniOSandAndroiduseavariationofMVCaswell.
Asthenamesuggests,MVChasthreecomponents:models,views,andcontrollers.Controllershandleincomingrequestsfromaclientorwebbrowserandmakedecisionsaboutwhatcodetorun.Viewsaretemplates(usuallyHTMLplusatemplatinglanguagelikeHandlebars,Pug,orRazor)thatgetdataaddedtothemandthenaredisplayedtotheuser.Modelsholdthedatathatisaddedtoviews,ordatathatisenteredbytheuser.
AcommonpatternforMVCcodeis:
ThecontrollerreceivesarequestandlooksupsomeinformationinadatabaseThecontrollercreatesamodelwiththeinformationandattachesittoaviewTheviewisrenderedanddisplayedintheuser'sbrowserTheuserclicksabuttonorsubmitsaform,whichsendsanewrequesttothecontroller,andthecyclerepeats
Ifyou'veworkedwithMVCinotherlanguages,you'llfeelrightathomeinASP.NETCoreMVC.Ifyou'renewtoMVC,thischapterwillteachyouthebasicsandwillhelpgetyoustarted.
Whatyou'llbuild
MVCbasics
20
The"HelloWorld"exerciseofMVCisbuildingato-dolistapplication.It'sagreatprojectsinceit'ssmallandsimpleinscope,butittoucheseachpartofMVCandcoversmanyoftheconceptsyou'duseinalargerapplication.
Inthisbook,you'llbuildato-doappthatletstheuseradditemstotheirto-dolistandcheckthemoffoncecomplete.Morespecifically,you'llbecreating:
Awebapplicationserver(sometimescalledthe"backend")usingASP.NETCore,C#,andtheMVCpatternAdatabasetostoretheuser'sto-doitemsusingtheSQLitedatabaseengineandasystemcalledEntityFrameworkCoreWebpagesandaninterfacethattheuserwillinteractwithviatheirbrowser,usingHTML,CSS,andJavaScript(calledthe"frontend")Aloginformandsecuritycheckssoeachuser'sto-dolistiskeptprivate
Soundgood?Let'sbuiltit!Ifyouhaven'talreadycreatedanewASP.NETCoreprojectusingdotnetnewmvc,followthestepsinthepreviouschapter.Youshouldbeabletobuildandruntheprojectandseethedefaultwelcomescreen.
MVCbasics
21
CreateacontrollerTherearealreadyafewcontrollersintheproject'sControllersdirectory,includingtheHomeControllerthatrendersthedefaultwelcomescreenyouseewhenyouvisithttp://localhost:5000.Youcanignorethesecontrollersfornow.
Createanewcontrollerfortheto-dolistfunctionality,calledTodoController,andaddthefollowingcode:
Controllers/TodoController.cs
usingSystem;
usingSystem.Collections.Generic;
usingSystem.Linq;
usingSystem.Threading.Tasks;
usingMicrosoft.AspNetCore.Mvc;
namespaceAspNetCoreTodo.Controllers
{
publicclassTodoController:Controller
{
//Actionsgohere
}
}
Routesthatarehandledbycontrollersarecalledactions,andarerepresentedbymethodsinthecontrollerclass.Forexample,theHomeControllerincludesthreeactionmethods(Index,About,andContact)whicharemappedbyASP.NETCoretotheserouteURLs:
localhost:5000/Home->Index()
localhost:5000/Home/About->About()
localhost:5000/Home/Contact->Contact()
Createacontroller
22
Thereareanumberofconventions(commonpatterns)usedbyASP.NETCore,suchasthepatternthatFooControllerbecomes/Foo,andtheIndexactionnamecanbeleftoutoftheURL.Youcancustomizethisbehaviorifyou'dlike,butfornow,we'llsticktothedefaultconventions.
AddanewactioncalledIndextotheTodoController,replacingthe//Actionsgoherecomment:
publicclassTodoController:Controller
{
publicIActionResultIndex()
{
//Getto-doitemsfromdatabase
//Putitemsintoamodel
//Renderviewusingthemodel
}
}
Actionmethodscanreturnviews,JSONdata,orHTTPstatuscodeslike200OKand404NotFound.TheIActionResultreturntypegivesyoutheflexibilitytoreturnanyofthesefromtheaction.
It'sabestpracticetokeepcontrollersaslightweightaspossible.Inthiscase,thecontrollerwillberesponsibleforgettingtheto-doitemsfromthedatabase,puttingthoseitemsintoamodeltheviewcanunderstand,andsendingtheviewbacktotheuser'sbrowser.
Beforeyoucanwritetherestofthecontrollercode,youneedtocreateamodelandaview.
Createacontroller
23
CreatemodelsTherearetwoseparatemodelclassesthatneedtobecreated:amodelthatrepresentsato-doitemstoredinthedatabase(sometimescalledanentity),andthemodelthatwillbecombinedwithaview(theMVinMVC)andsentbacktotheuser'sbrowser.Becausebothofthemcanbereferredtoas"models",I'llrefertothelatterasaviewmodel.
First,createaclasscalledTodoItemintheModelsdirectory:
Models/TodoItem.cs
usingSystem;
usingSystem.ComponentModel.DataAnnotations;
namespaceAspNetCoreTodo.Models
{
publicclassTodoItem
{
publicGuidId{get;set;}
publicboolIsDone{get;set;}
[Required]
publicstringTitle{get;set;}
publicDateTimeOffset?DueAt{get;set;}
}
}
Thisclassdefineswhatthedatabasewillneedtostoreforeachto-doitem:anID,atitleorname,whethertheitemiscomplete,andwhattheduedateis.Eachlinedefinesapropertyoftheclass:
Createmodels
24
TheIdpropertyisaguid,oragloballyuniqueidentifier.Guids(orGUIDs)arelongstringsoflettersandnumbers,like43ec09f2-7f70-4f4b-9559-65011d5781bb.Becauseguidsarerandomandareextremelyunlikelytobeaccidentallyduplicated,theyarecommonlyusedasuniqueIDs.Youcouldalsouseanumber(integer)asadatabaseentityID,butyou'dneedtoconfigureyourdatabasetoalwaysincrementthenumberwhennewrowsareaddedtothedatabase.Guidsaregeneratedrandomly,soyoudon'thavetoworryaboutauto-incrementing.
TheIsDonepropertyisaboolean(true/falsevalue).Bydefault,itwillbefalseforallnewitems.Lateryou'llusewritecodetoswitchthispropertytotruewhentheuserclicksanitem'scheckboxintheview.
TheTitlepropertyisastring(textvalue).Thiswillholdthenameordescriptionoftheto-doitem.The[Required]attributetellsASP.NETCorethatthisstringcan'tbenullorempty.
TheDueAtpropertyisaDateTimeOffset,whichisaC#typethatstoresadate/timestampalongwithatimezoneoffsetfromUTC.Storingthedate,time,andtimezoneoffsettogethermakesiteasytorenderdatesaccuratelyonsystemsindifferenttimezones.
Noticethe?questionmarkaftertheDateTimeOffsettype?ThatmarkstheDueAtpropertyasnullable,oroptional.Ifthe?wasn'tincluded,everyto-doitemwouldneedtohaveaduedate.TheIdandIsDonepropertiesaren'tmarkedasnullable,sotheyarerequiredandwillalwayshaveavalue(oradefaultvalue).
StringsinC#arealwaysnullable,sothere'snoneedtomarktheTitlepropertyasnullable.C#stringscanbenull,empty,orcontaintext.
Createmodels
25
Eachpropertyisfollowedbyget;set;,whichisashorthandwayofsayingthepropertyisread/write(or,moretechnically,ithasagetterandsettermethods).
Atthispoint,itdoesn'tmatterwhattheunderlyingdatabasetechnologyis.ItcouldbeSQLServer,MySQL,MongoDB,Redis,orsomethingmoreexotic.ThismodeldefineswhatthedatabaseroworentrywilllooklikeinC#soyoudon'thavetoworryaboutthelow-leveldatabasestuffinyourcode.Thissimplestyleofmodelissometimescalleda"plainoldC#object"orPOCO.
Theviewmodel
Often,themodel(entity)youstoreinthedatabaseissimilarbutnotexactlythesameasthemodelyouwanttouseinMVC(theviewmodel).Inthiscase,theTodoItemmodelrepresentsasingleiteminthedatabase,buttheviewmightneedtodisplaytwo,ten,orahundredto-doitems(dependingonhowbadlytheuserisprocrastinating).
Becauseofthis,theviewmodelshouldbeaseparateclassthatholdsanarrayofTodoItems:
Models/TodoViewModel.cs
namespaceAspNetCoreTodo.Models
{
publicclassTodoViewModel
{
publicTodoItem[]Items{get;set;}
}
}
Nowthatyouhavesomemodels,it'stimetocreateaviewthatwilltakeaTodoViewModelandrendertherightHTMLtoshowtheusertheirto-dolist.
Createmodels
26
Createmodels
27
CreateaviewViewsinASP.NETCorearebuiltusingtheRazortemplatinglanguage,whichcombinesHTMLandC#code.(Ifyou'vewrittenpagesusingHandlebarsmoustaches,ERBinRubyonRails,orThymeleafinJava,you'vealreadygotthebasicidea.)
MostviewcodeisjustHTML,withtheoccasionalC#statementaddedintopulldataoutoftheviewmodelandturnitintotextorHTML.TheC#statementsareprefixedwiththe@symbol.
TheviewrenderedbytheIndexactionoftheTodoControllerneedstotakethedataintheviewmodel(asequenceofto-doitems)anddisplayitinanicetablefortheuser.Byconvention,viewsareplacedintheViewsdirectory,inasubdirectorycorrespondingtothecontrollername.Thefilenameoftheviewisthenameoftheactionwitha.cshtmlextension.
CreateaTododirectoryinsidetheViewsdirectory,andaddthisfile:
Views/Todo/Index.cshtml
@modelTodoViewModel
@{
ViewData["Title"]="Manageyourtodolist";
}
<divclass="panelpanel-defaulttodo-panel">
<divclass="panel-heading">@ViewData["Title"]</div>
<tableclass="tabletable-hover">
<thead>
<tr>
<td>✔</td>
<td>Item</td>
<td>Due</td>
Createaview
28
</tr>
</thead>
@foreach(variteminModel.Items)
{
<tr>
<td>
<inputtype="checkbox"class="done-checkbox">
</td>
<td>@item.Title</td>
<td>@item.DueAt</td>
</tr>
}
</table>
<divclass="panel-footeradd-item-form">
<!--TODO:Additemform-->
</div>
</div>
Attheverytopofthefile,the@modeldirectivetellsRazorwhichmodeltoexpectthisviewtobeboundto.ThemodelisaccessedthroughtheModelproperty.
Assumingthereareanyto-doitemsinModel.Items,theforeachstatementwillloopovereachto-doitemandrenderatablerow(<tr>element)containingtheitem'snameandduedate.Acheckboxisalsorenderedthatwilllettheusermarktheitemascomplete.
Thelayoutfile
YoumightbewonderingwheretherestoftheHTMLis:whataboutthe<body>tag,ortheheaderandfooterofthepage?ASP.NETCoreusesalayoutviewthatdefinesthebasestructurethateveryotherviewisrenderedinsideof.It'sstoredinViews/Shared/_Layout.cshtml.
Createaview
29
ThedefaultASP.NETCoretemplateincludesBootstrapandjQueryinthislayoutfile,soyoucanquicklycreateawebapplication.Ofcourse,youcanuseyourownCSSandJavaScriptlibrariesifyou'dlike.
Customizingthestylesheet
ThedefaulttemplatealsoincludesastylesheetwithsomebasicCSSrules.Thestylesheetisstoredinthewwwroot/cssdirectory.AddafewnewCSSstylerulestothebottomofthesite.cssfile:
wwwroot/css/site.css
div.todo-panel{
margin-top:15px;
}
tabletr.done{
text-decoration:line-through;
color:#888;
}
YoucanuseCSSruleslikethesetocompletelycustomizehowyourpageslookandfeel.
ASP.NETCoreandRazorcandomuchmore,suchaspartialviewsandserver-renderedviewcomponents,butasimplelayoutandviewisallyouneedfornow.TheofficialASP.NETCoredocumentation(athttps://docs.asp.net)containsanumberofexamplesifyou'dliketolearnmore.
Createaview
30
AddaserviceclassYou'vecreatedamodel,aview,andacontroller.Beforeyouusethemodelandviewinthecontroller,youalsoneedtowritecodethatwillgettheuser'sto-doitemsfromadatabase.
Youcouldwritethisdatabasecodedirectlyinthecontroller,butit'sabetterpracticetokeepyourcodeseparate.Why?Inabig,real-worldapplication,you'llhavetojugglemanyconcerns:
Renderingviewsandhandlingincomingdata:thisiswhatyourcontrolleralreadydoes.Performingbusinesslogic,orcodeandlogicthat'srelatedtothepurposeand"business"ofyourapplication.Inato-dolistapplication,businesslogicmeansdecisionslikesettingadefaultduedateonnewtasks,oronlydisplayingtasksthatareincomplete.Otherexamplesofbusinesslogicincludecalculatingatotalcostbasedonproductpricesandtaxrates,orcheckingwhetheraplayerhasenoughpointstolevelupinagame.Savingandretrievingitemsfromadatabase.
Again,it'spossibletodoallofthesethingsinasingle,massivecontroller,butthatquicklybecomestoohardtomanageandtest.Instead,it'scommontoseeapplicationssplitupintotwo,three,ormore"layers"ortiersthateachhandleone(andonlyone)concern.Thishelpskeepthecontrollersassimpleaspossible,andmakesiteasiertotestandchangethebusinesslogicanddatabasecodelater.
Separatingyourapplicationthiswayissometimescalledamulti-tierorn-tierarchitecture.Insomecases,thetiers(layers)areisolatedincompletelyseparateprojects,butothertimesitjustreferstohowthe
Addaserviceclass
31
classesareorganizedandused.Theimportantthingisthinkingabouthowtosplityourapplicationintomanageablepieces,andavoidhavingcontrollersorbloatedclassesthattrytodoeverything.
Forthisproject,you'llusetwoapplicationlayers:apresentationlayermadeupofthecontrollersandviewsthatinteractwiththeuser,andaservicelayerthatcontainsbusinesslogicanddatabasecode.Thepresentationlayeralreadyexists,sothenextstepistobuildaservicethathandlesto-dobusinesslogicandsavesto-doitemstoadatabase.
Mostlargerprojectsusea3-tierarchitecture:apresentationlayer,aservicelogiclayer,andadatarepositorylayer.Arepositoryisaclassthat'sonlyfocusedondatabasecode(nobusinesslogic).Inthisapplication,you'llcombinetheseintoasingleservicelayerforsimplicity,butfeelfreetoexperimentwithdifferentwaysofarchitectingthecode.
Createaninterface
TheC#languageincludestheconceptofinterfaces,wherethedefinitionofanobject'smethodsandpropertiesisseparatefromtheclassthatactuallycontainsthecodeforthosemethodsandproperties.Interfacesmakeiteasytokeepyourclassesdecoupledandeasytotest,asyou'llseehere(andlaterintheAutomatedtestingchapter).You'lluseaninterfacetorepresenttheservicethatcaninteractwithto-doitemsinthedatabase.
Byconvention,interfacesareprefixedwith"I".CreateanewfileintheServicesdirectory:
Services/ITodoItemService.cs
usingSystem;
usingSystem.Collections.Generic;
usingSystem.Threading.Tasks;
usingAspNetCoreTodo.Models;
Addaserviceclass
32
namespaceAspNetCoreTodo.Services
{
publicinterfaceITodoItemService
{
Task<TodoItem[]>GetIncompleteItemsAsync();
}
}
NotethatthenamespaceofthisfileisAspNetCoreTodo.Services.Namespacesareawaytoorganize.NETcodefiles,andit'scustomaryforthenamespacetofollowthedirectorythefileisstoredin(AspNetCoreTodo.ServicesforfilesintheServicesdirectory,andsoon).
Becausethisfile(intheAspNetCoreTodo.Servicesnamespace)referencestheTodoItemclass(intheAspNetCoreTodo.Modelsnamespace),itneedstoincludeausingstatementatthetopofthefiletoimportthatnamespace.Withouttheusingstatement,you'llseeanerrorlike:
Thetypeornamespacename'TodoItem'couldnotbefound(areyou
missingausingdirectiveoranassemblyreference?)
Sincethisisaninterface,thereisn'tanyactualcodehere,justthedefinition(ormethodsignature)oftheGetIncompleteItemsAsyncmethod.ThismethodrequiresnoparametersandreturnsaTask<TodoItem[]>.
Ifthissyntaxlooksconfusing,think:"aTaskthatcontainsanarrayofTodoItems".
TheTasktypeissimilartoafutureorapromise,andit'susedherebecausethismethodwillbeasynchronous.Inotherwords,themethodmaynotbeabletoreturnthelistofto-doitemsrightawaybecauseitneedstogotalktothedatabasefirst.(Moreonthislater.)
Createtheserviceclass
Addaserviceclass
33
Nowthattheinterfaceisdefined,you'rereadytocreatetheactualserviceclass.I'llcoverdatabasecodeindepthintheUseadatabasechapter,sofornowyou'lljustfakeitandalwaysreturntwohard-codeditems:
Services/FakeTodoItemService.cs
usingSystem;
usingSystem.Collections.Generic;
usingSystem.Threading.Tasks;
usingAspNetCoreTodo.Models;
namespaceAspNetCoreTodo.Services
{
publicclassFakeTodoItemService:ITodoItemService
{
publicTask<TodoItem[]>GetIncompleteItemsAsync()
{
varitem1=newTodoItem
{
Title="LearnASP.NETCore",
DueAt=DateTimeOffset.Now.AddDays(1)
};
varitem2=newTodoItem
{
Title="Buildawesomeapps",
DueAt=DateTimeOffset.Now.AddDays(2)
};
returnTask.FromResult(new[]{item1,item2});
}
}
}
ThisFakeTodoItemServiceimplementstheITodoItemServiceinterfacebutalwaysreturnsthesamearrayoftwoTodoItems.You'llusethistotestthecontrollerandview,andthenaddrealdatabasecodeinUseadatabase.
Addaserviceclass
34
Addaserviceclass
35
UsedependencyinjectionBackintheTodoController,addsomecodetoworkwiththeITodoItemService:
publicclassTodoController:Controller
{
privatereadonlyITodoItemService_todoItemService;
publicTodoController(ITodoItemServicetodoItemService)
{
_todoItemService=todoItemService;
}
publicIActionResultIndex()
{
//Getto-doitemsfromdatabase
//Putitemsintoamodel
//Passtheviewtoamodelandrender
}
}
SinceITodoItemServiceisintheServicesnamespace,you'llalsoneedtoaddausingstatementatthetop:
usingAspNetCoreTodo.Services;
ThefirstlineoftheclassdeclaresaprivatevariabletoholdareferencetotheITodoItemService.ThisvariableletsyouusetheservicefromtheIndexactionmethodlater(you'llseehowinaminute).
ThepublicTodoController(ITodoItemServicetodoItemService)linedefinesaconstructorfortheclass.Theconstructorisaspecialmethodthatiscalledwhenyouwanttocreateanewinstanceofaclass(the
Usedependencyinjection
36
TodoControllerclass,inthiscase).ByaddinganITodoItemServiceparametertotheconstructor,you'vedeclaredthatinordertocreatetheTodoController,you'llneedtoprovideanobjectthatmatchestheITodoItemServiceinterface.
Interfacesareawesomebecausetheyhelpdecouple(separate)thelogicofyourapplication.SincethecontrollerdependsontheITodoItemServiceinterface,andnotonanyspecificclass,itdoesn'tknoworcarewhichclassit'sactuallygiven.ItcouldbetheFakeTodoItemService,adifferentonethattalkstoalivedatabase,orsomethingelse!Aslongasitmatchestheinterface,thecontrollercanuseit.Thismakesitreallyeasytotestpartsofyourapplicationseparately.I'llcovertestingindetailintheAutomatedtestingchapter.
NowyoucanfinallyusetheITodoItemService(viatheprivatevariableyoudeclared)inyouractionmethodtogetto-doitemsfromtheservicelayer:
publicIActionResultIndex()
{
varitems=await_todoItemService.GetIncompleteItemsAsync();
//...
}
RememberthattheGetIncompleteItemsAsyncmethodreturnedaTask<TodoItem[]>?ReturningaTaskmeansthatthemethodwon'tnecessarilyhavearesultrightaway,butyoucanusetheawaitkeywordtomakesureyourcodewaitsuntiltheresultisreadybeforecontinuingon.
TheTaskpatterniscommonwhenyourcodecallsouttoadatabaseoranAPIservice,becauseitwon'tbeabletoreturnarealresultuntilthedatabase(ornetwork)responds.Ifyou'veusedpromisesorcallbacksin
Usedependencyinjection
37
JavaScriptorotherlanguages,Taskisthesameidea:thepromisethattherewillbearesult-sometimeinthefuture.
Ifyou'vehadtodealwith"callbackhell"inolderJavaScriptcode,you'reinluck.Dealingwithasynchronouscodein.NETismucheasierthankstothemagicoftheawaitkeyword!awaitletsyourcodepauseonanasyncoperation,andthenpickupwhereitleftoffwhentheunderlyingdatabaseornetworkrequestfinishes.Inthemeantime,yourapplicationisn'tblocked,becauseitcanprocessotherrequestsasneeded.Thispatternissimplebuttakesalittlegettingusedto,sodon'tworryifthisdoesn'tmakesenserightaway.Justkeepfollowingalong!
TheonlycatchisthatyouneedtoupdatetheIndexmethodsignaturetoreturnaTask<IActionResult>insteadofjustIActionResult,andmarkitasasync:
publicasyncTask<IActionResult>Index()
{
varitems=await_todoItemService.GetIncompleteItemsAsync();
//Putitemsintoamodel
//Passtheviewtoamodelandrender
}
You'realmostthere!You'vemadetheTodoControllerdependontheITodoItemServiceinterface,butyouhaven'tyettoldASP.NETCorethatyouwanttheFakeTodoItemServicetobetheactualservicethat'susedunderthehood.ItmightseemobviousrightnowsinceyouonlyhaveoneclassthatimplementsITodoItemService,butlateryou'llhavemultipleclassesthatimplementthesameinterface,sobeingexplicitisnecessary.
Usedependencyinjection
38
Declaring(or"wiringup")whichconcreteclasstouseforeachinterfaceisdoneintheConfigureServicesmethodoftheStartupclass.Rightnow,itlookssomethinglikethis:
Startup.cs
publicvoidConfigureServices(IServiceCollectionservices)
{
//(...somecode)
services.AddMvc();
}
ThejoboftheConfigureServicesmethodisaddingthingstotheservicecontainer,orthecollectionofservicesthatASP.NETCoreknowsabout.Theservices.AddMvclineaddstheservicesthattheinternalASP.NETCoresystemsneed(asanexperiment,trycommentingoutthisline).AnyotherservicesyouwanttouseinyourapplicationmustbeaddedtotheservicecontainerhereinConfigureServices.
AddthefollowinglineanywhereinsidetheConfigureServicesmethod:
services.AddSingleton<ITodoItemService,FakeTodoItemService>();
ThislinetellsASP.NETCoretousetheFakeTodoItemServicewhenevertheITodoItemServiceinterfaceisrequestedinaconstructor(oranywhereelse).
AddSingletonaddsyourservicetotheservicecontainerasasingleton.ThismeansthatonlyonecopyoftheFakeTodoItemServiceiscreated,andit'sreusedwhenevertheserviceisrequested.Later,whenyouwriteadifferentserviceclassthattalkstoadatabase,you'lluseadifferentapproach(calledscoped)instead.I'llexplainwhyintheUseadatabasechapter.
Usedependencyinjection
39
That'sit!WhenarequestcomesinandisroutedtotheTodoController,ASP.NETCorewilllookattheavailableservicesandautomaticallysupplytheFakeTodoItemServicewhenthecontrollerasksforanITodoItemService.Becausetheservicesare"injected"fromtheservicecontainer,thispatterniscalleddependencyinjection.
Usedependencyinjection
40
FinishthecontrollerThelaststepistofinishthecontrollercode.Thecontrollernowhasalistofto-doitemsfromtheservicelayer,anditneedstoputthoseitemsintoaTodoViewModelandbindthatmodeltotheviewyoucreatedearlier:
Controllers/TodoController.cs
publicasyncTask<IActionResult>Index()
{
varitems=await_todoItemService.GetIncompleteItemsAsync();
varmodel=newTodoViewModel()
{
Items=items
};
returnView(model);
}
Ifyouhaven'talready,makesuretheseusingstatementsareatthetopofthefile:
usingAspNetCoreTodo.Services;
usingAspNetCoreTodo.Models;
Ifyou'reusingVisualStudioorVisualStudioCode,theeditorwillsuggesttheseusingstatementswhenyouputyourcursoronaredsquigglyline.
Testitout
Finishthecontroller
41
Tostarttheapplication,pressF5(ifyou'reusingVisualStudioorVisualStudioCode),orjusttypedotnetrunintheterminal.Ifthecodecompileswithouterrors,theserverwillstartuponport5000bydefault.
Ifyourwebbrowserdidn'topenautomatically,openitandnavigatetohttp://localhost:5000/todo.You'llseetheviewyoucreated,withthedatapulledfromyourfakedatabase(fornow).
Althoughit'spossibletogodirectlytohttp://localhost:5000/todo,itwouldbenicertoaddanitemcalledMyto-dostothenavbar.Todothis,youcaneditthesharedlayoutfile.
Finishthecontroller
42
UpdatethelayoutThelayoutfileatViews/Shared/_Layout.cshtmlcontainsthe"base"HTMLforeachview.Thisincludesthenavbar,whichisrenderedatthetopofeachpage.
Toaddanewitemtothenavbar,findtheHTMLcodefortheexistingnavbaritems:
Views/Shared/_Layout.cshtml
<ulclass="navnavbar-nav">
<li><aasp-area=""asp-controller="Home"asp-action="Index">
Home
</a></li>
<li><aasp-area=""asp-controller="Home"asp-action="About">
About
</a></li>
<li><aasp-area=""asp-controller="Home"asp-action="Contact">
Contact
</a></li>
</ul>
AddyourownitemthatpointstotheTodocontrollerinsteadofHome:
<li>
<aasp-controller="Todo"asp-action="Index">Myto-dos</a>
</li>
Theasp-controllerandasp-actionattributesonthe<a>elementarecalledtaghelpers.Beforetheviewisrendered,ASP.NETCorereplacesthesetaghelperswithrealHTMLattributes.Inthiscase,aURLtothe/Todo/Indexrouteisgeneratedandaddedtothe<a>element
Updatethelayout
43
asanhrefattribute.Thismeansyoudon'thavetohard-codetheroutetotheTodoController.Instead,ASP.NETCoregeneratesitforyouautomatically.
Ifyou'veusedRazorinASP.NET4.x,you'[email protected]()togeneratealinktoanaction,taghelpersarenowtherecommendedwaytocreatelinksinyourviews.Taghelpersareusefulforforms,too(you'llseewhyinalaterchapter).Youcanlearnaboutothertaghelpersinthedocumentationathttps://docs.asp.net.
Updatethelayout
44
AddexternalpackagesOneofthebigadvantagesofusingamatureecosystemlike.NETisthatthenumberofthird-partypackagesandpluginsishuge.Justlikeotherpackagesystems,youcandownloadandinstall.NETpackagesthathelpwithalmostanytaskorproblemyoucanimagine.
NuGetisboththepackagemanagertoolandtheofficialpackagerepository(athttps://www.nuget.org).YoucansearchforNuGetpackagesontheweb,andinstallthemfromyourlocalmachinethroughtheterminal(ortheGUI,ifyou'reusingVisualStudio).
InstalltheHumanizerpackageAttheendofthelastchapter,theto-doapplicationdisplayedto-doitemslikethis:
Theduedatecolumnisdisplayingdatesinaformatthat'sgoodformachines(calledISO8601),butclunkyforhumans.Wouldn'titbenicerifitsimplyread"Xdaysfromnow"?
YoucouldwritecodeyourselfthatconvertedanISO8601dateintoahuman-friendlystring,butfortunately,there'safasterway.
TheHumanizerpackageonNuGetsolvesthisproblembyprovidingmethodsthatcan"humanize"orrewritealmostanything:dates,times,durations,numbers,andsoon.It'safantasticandusefulopen-source
Addexternalpackages
45
projectthat'spublishedunderthepermissiveMITlicense.
Toaddittoyourproject,runthiscommandintheterminal:
dotnetaddpackageHumanizer
IfyoupeekattheAspNetCoreTodo.csprojprojectfile,you'llseeanewPackageReferencelinethatreferencesHumanizer.
UseHumanizerintheviewTouseapackageinyourcode,youusuallyneedtoaddausingstatementthatimportsthepackageatthetopofthefile.
SinceHumanizerwillbeusedtorewritedatesrenderedintheview,youcanuseitdirectlyintheviewitself.First,adda@usingstatementatthetopoftheview:
Views/Todo/Index.cshtml
@modelTodoViewModel
@usingHumanizer
//...
Then,updatethelinethatwritestheDueAtpropertytouseHumanizer'sHumanizemethod:
<td>@item.DueAt.Humanize()</td>
Nowthedatesaremuchmorereadable:
Addexternalpackages
46
TherearepackagesavailableonNuGetforeverythingfromparsingXMLtomachinelearningtopostingtoTwitter.ASP.NETCoreitself,underthehood,isnothingmorethanacollectionofNuGetpackagesthatareaddedtoyourproject.
TheprojectfilecreatedbydotnetnewmvcincludesasinglereferencetotheMicrosoft.AspNetCore.Allpackage,whichisaconvenient"metapackage"thatreferencesalloftheotherASP.NETCorepackagesyouneedforatypicalproject.Thatway,youdon'tneedtohavehundredsofpackagereferencesinyourprojectfile.
Inthenextchapter,you'lluseanothersetofNuGetpackages(asystemcalledEntityFrameworkCore)towritecodethatinteractswithadatabase.
Addexternalpackages
47
UseadatabaseWritingdatabasecodecanbetricky.Unlessyoureallyknowwhatyou'redoing,it'sabadideatopasterawSQLquerystringsintoyourapplicationcode.Anobject-relationalmapper(ORM)makesiteasiertowritecodethatinteractswithadatabasebyaddingalayerofabstractionbetweenyourcodeandthedatabaseitself.HibernateinJavaandActiveRecordinRubyaretwowell-knownORMs.
ThereareanumberofORMsfor.NET,includingonebuiltbyMicrosoftandincludedinASP.NETCorebydefault:EntityFrameworkCore.EntityFrameworkCoremakesiteasytoconnecttoanumberofdifferentdatabasetypes,andletsyouuseC#codetocreatedatabasequeriesthataremappedbackintoC#models(POCOs).
Rememberhowcreatingaserviceinterfacedecoupledthecontrollercodefromtheactualserviceclass?EntityFrameworkCoreislikeabiginterfaceoveryourdatabase.YourC#codecanstaydatabase-agnostic,andyoucanswapoutdifferentprovidersdependingontheunderlyingdatabasetechnology.
EntityFrameworkCorecanconnecttorelationaldatabaseslikeSQLServer,PostgreSQL,andMySQL,andalsoworkswithNoSQL(document)databaseslikeMongo.Duringdevelopment,you'lluseSQLiteinthisprojecttomakethingseasytosetup.
Useadatabase
48
ConnecttoadatabaseThereareafewthingsyouneedtouseEntityFrameworkCoretoconnecttoadatabase.SinceyouuseddotnetnewandtheMVC+IndividualAuthtemplatetosetyourproject,you'vealreadygotthem:
TheEntityFrameworkCorepackages.TheseareincludedbydefaultinallASP.NETCoreprojects.
Adatabase(naturally).Theapp.dbfileintheprojectrootdirectoryisasmallSQLitedatabasecreatedforyoubydotnetnew.SQLiteisalightweightdatabaseenginethatcanrunwithoutrequiringyoutoinstallanyextratoolsonyourmachine,soit'seasyandquicktouseindevelopment.
Adatabasecontextclass.ThedatabasecontextisaC#classthatprovidesanentrypointintothedatabase.It'showyourcodewillinteractwiththedatabasetoreadandsaveitems.AbasiccontextclassalreadyexistsintheData/ApplicationDbContext.csfile.
Aconnectionstring.Whetheryouareconnectingtoalocalfiledatabase(likeSQLite)oradatabasehostedelsewhere,you'lldefineastringthatcontainsthenameoraddressofthedatabasetoconnectto.Thisisalreadysetupforyouintheappsettings.jsonfile:theconnectionstringfortheSQLitedatabaseisDataSource=app.db.
EntityFrameworkCoreusesthedatabasecontext,togetherwiththeconnectionstring,toestablishaconnectiontothedatabase.YouneedtotellEntityFrameworkCorewhichcontext,connectionstring,anddatabaseprovidertouseintheConfigureServicesmethodoftheStartupclass.Here'swhat'sdefinedforyou,thankstothetemplate:
services.AddDbContext<ApplicationDbContext>(options=>
Connecttoadatabase
49
options.UseSqlite(
Configuration.GetConnectionString("DefaultConnection")));
ThiscodeaddstheApplicationDbContexttotheservicecontainer,andtellsEntityFrameworkCoretousetheSQLitedatabaseprovider,withtheconnectionstringfromconfiguration(appsettings.json).
Asyoucansee,dotnetnewcreatesalotofstuffforyou!Thedatabaseissetupandreadytobeused.However,itdoesn'thaveanytablesforstoringto-doitems.InordertostoreyourTodoItementities,you'llneedtoupdatethecontextandmigratethedatabase.
Connecttoadatabase
50
UpdatethecontextThere'snotawholelotgoingoninthedatabasecontextyet:
Data/ApplicationDbContext.cs
publicclassApplicationDbContext
:IdentityDbContext<ApplicationUser>
{
publicApplicationDbContext(
DbContextOptions<ApplicationDbContext>options)
:base(options)
{
}
protectedoverridevoidOnModelCreating(ModelBuilderbuilder)
{
base.OnModelCreating(builder);
//...
}
}
AddaDbSetpropertytotheApplicationDbContext,rightbelowtheconstructor:
publicApplicationDbContext(
DbContextOptions<ApplicationDbContext>options)
:base(options)
{
}
publicDbSet<TodoItem>Items{get;set;}
//...
Updatethecontext
51
ADbSetrepresentsatableorcollectioninthedatabase.BycreatingaDbSet<TodoItem>propertycalledItems,you'retellingEntityFrameworkCorethatyouwanttostoreTodoItementitiesinatablecalledItems.
You'veupdatedthecontextclass,butnowthere'sonesmallproblem:thecontextanddatabasearenowoutofsync,becausethereisn'tactuallyanItemstableinthedatabase.(Justupdatingthecodeofthecontextclassdoesn'tchangethedatabaseitself.)
Inordertoupdatethedatabasetoreflectthechangeyoujustmadetothecontext,youneedtocreateamigration.
Ifyoualreadyhaveanexistingdatabase,searchthewebfor"scaffold-dbcontextexistingdatabase"andreadMicrosoft'sdocumentationonusingtheScaffold-DbContexttooltoreverse-engineeryourdatabasestructureintotheproperDbContextandmodelclassesautomatically.
Updatethecontext
52
CreateamigrationMigrationskeeptrackofchangestothedatabasestructureovertime.Theymakeitpossibletoundo(rollback)asetofchanges,orcreateaseconddatabasewiththesamestructureasthefirst.Withmigrations,youhaveafullhistoryofmodificationslikeaddingorremovingcolumns(andentiretables).
Inthepreviouschapter,youaddedanItemssettothecontext.Sincethecontextnowincludesaset(ortable)thatdoesn'texistinthedatabase,youneedtocreateamigrationtoupdatethedatabase:
dotnetefmigrationsaddAddItems
ThiscreatesanewmigrationcalledAddItemsbyexamininganychangesyou'vemadetothecontext.
IfyougetanerrorlikeNoexecutablefoundmatchingcommand"dotnet-ef",makesureyou'reintherightdirectory.Thesecommandsmustberunfromtheprojectrootdirectory(wheretheProgram.csfileis).
IfyouopenuptheData/Migrationsdirectory,you'llseeafewfiles:
Createamigration
53
Thefirstmigrationfile(withanamelike00_CreateIdentitySchema.cs)wascreatedandappliedforyouwaybackwhenyourandotnetnew.YournewAddItemmigrationisprefixedwithatimestampwhenyoucreateit.
Youcanseealistofmigrationswithdotnetefmigrationslist.
Ifyouopenyourmigrationfile,you'llseetwomethodscalledUpandDown:
Data/Migrations/_AddItems.cs
protectedoverridevoidUp(MigrationBuildermigrationBuilder)
{
//(...somecode)
migrationBuilder.CreateTable(
name:"Items",
columns:table=>new
{
Id=table.Column<Guid>(nullable:false),
DueAt=table.Column<DateTimeOffset>(nullable:true),
IsDone=table.Column<bool>(nullable:false),
Title=table.Column<string>(nullable:true)
},
constraints:table=>
{
table.PrimaryKey("PK_Items",x=>x.Id);
});
//(somecode...)
}
protectedoverridevoidDown(MigrationBuildermigrationBuilder)
{
//(...somecode)
migrationBuilder.DropTable(
name:"Items");
//(somecode...)
}
Createamigration
54
TheUpmethodrunswhenyouapplythemigrationtothedatabase.SinceyouaddedaDbSet<TodoItem>tothedatabasecontext,EntityFrameworkCorewillcreateanItemstable(withcolumnsthatmatchaTodoItem)whenyouapplythemigration.
TheDownmethoddoestheopposite:ifyouneedtoundo(rollback)themigration,theItemstablewillbedropped.
WorkaroundforSQLitelimitations
TherearesomelimitationsofSQLitethatgetinthewayifyoutrytorunthemigrationas-is.Untilthisproblemisfixed,usethisworkaround:
CommentoutorremovethemigrationBuilder.AddForeignKeylinesintheUpmethod.CommentoutorremoveanymigrationBuilder.DropForeignKeylinesintheDownmethod.
Ifyouuseafull-fledgedSQLdatabase,likeSQLServerorMySQL,thiswon'tbeanissueandyouwon'tneedtodothis(admittedlyhackish)workaround.
Applythemigration
Thefinalstepaftercreatingone(ormore)migrationsistoactuallyapplythemtothedatabase:
dotnetefdatabaseupdate
ThiscommandwillcauseEntityFrameworkCoretocreatetheItemstableinthedatabase.
Createamigration
55
Ifyouwanttorollbackthedatabase,youcanprovidethenameofthepreviousmigration:dotnetefdatabaseupdateCreateIdentitySchemaThiswillruntheDownmethodsofanymigrationsnewerthanthemigrationyouspecify.
Ifyouneedtocompletelyerasethedatabaseandstartover,rundotnetefdatabasedropfollowedbydotnetefdatabaseupdatetore-scaffoldthedatabaseandbringituptothecurrentmigration.
That'sit!Boththedatabaseandthecontextarereadytogo.Next,you'llusethecontextinyourservicelayer.
Createamigration
56
CreateanewserviceclassBackintheMVCbasicschapter,youcreatedaFakeTodoItemServicethatcontainedhard-codedto-doitems.Nowthatyouhaveadatabasecontext,youcancreateanewserviceclassthatwilluseEntityFrameworkCoretogettherealitemsfromthedatabase.
DeletetheFakeTodoItemService.csfile,andcreateanewfile:
Services/TodoItemService.cs
usingSystem;
usingSystem.Collections.Generic;
usingSystem.Linq;
usingSystem.Threading.Tasks;
usingAspNetCoreTodo.Data;
usingAspNetCoreTodo.Models;
usingMicrosoft.EntityFrameworkCore;
namespaceAspNetCoreTodo.Services
{
publicclassTodoItemService:ITodoItemService
{
privatereadonlyApplicationDbContext_context;
publicTodoItemService(ApplicationDbContextcontext)
{
_context=context;
}
publicasyncTask<TodoItem[]>GetIncompleteItemsAsync()
{
returnawait_context.Items
.Where(x=>x.IsDone==false)
.ToArrayAsync();
}
}
}
Createanewserviceclass
57
You'llnoticethesamedependencyinjectionpatternherethatyousawintheMVCbasicschapter,exceptthistimeit'stheApplicationDbContextthat'sgettinginjected.TheApplicationDbContextisalreadybeingaddedtotheservicecontainerintheConfigureServicesmethod,soit'savailableforinjectionhere.
Let'stakeacloserlookatthecodeoftheGetIncompleteItemsAsyncmethod.First,itusestheItemspropertyofthecontexttoaccessalltheto-doitemsintheDbSet:
varitems=await_context.Items
Then,theWheremethodisusedtofilteronlytheitemsthatarenotcomplete:
.Where(x=>x.IsDone==false)
TheWheremethodisafeatureofC#calledLINQ(languageintegratedquery),whichtakesinspirationfromfunctionalprogrammingandmakesiteasytoexpressdatabasequeriesincode.Underthehood,EntityFrameworkCoretranslatestheWheremethodintoastatementlikeSELECT*FROMItemsWHEREIsDone=0,oranequivalentquerydocumentinaNoSQLdatabase.
Finally,theToArrayAsyncmethodtellsEntityFrameworkCoretogetalltheentitiesthatmatchedthefilterandreturnthemasanarray.TheToArrayAsyncmethodisasynchronous(itreturnsaTask),soitmustbeawaitedtogetitsvalue.
Tomakethemethodalittleshorter,youcanremovetheintermediateitemsvariableandjustreturntheresultofthequerydirectly(whichdoesthesamething):
publicasyncTask<TodoItem[]>GetIncompleteItemsAsync()
Createanewserviceclass
58
{
returnawait_context.Items
.Where(x=>x.IsDone==false)
.ToArrayAsync();
}
Updatetheservicecontainer
BecauseyoudeletedtheFakeTodoItemServiceclass,you'llneedtoupdatethelineinConfigureServicesthatiswiringuptheITodoItemServiceinterface:
services.AddScoped<ITodoItemService,TodoItemService>();
AddScopedaddsyourservicetotheservicecontainerusingthescopedlifecycle.ThismeansthatanewinstanceoftheTodoItemServiceclasswillbecreatedduringeachwebrequest.Thisisrequiredforserviceclassesthatinteractwithadatabase.
AddingaserviceclassthatinteractswithEntityFrameworkCore(andyourdatabase)withthesingletonlifecycle(orotherlifecycles)cancauseproblems,becauseofhowEntityFrameworkCoremanagesdatabaseconnectionsperrequestunderthehood.Toavoidthat,alwaysusethescopedlifecycleforservicesthatinteractwithEntityFrameworkCore.
TheTodoControllerthatdependsonaninjectedITodoItemServicewillbeblissfullyunawareofthechangeinservicesclasses,butunderthehoodit'llbeusingEntityFrameworkCoreandtalkingtoarealdatabase!
Testitout
Startuptheapplicationandnavigatetohttp://localhost:5000/todo.Thefakeitemsaregone,andyourapplicationismakingrealqueriestothedatabase.Theredoesn'thappentobeanysavedto-doitems,soit's
Createanewserviceclass
59
blankfornow.
Inthenextchapter,you'lladdmorefeaturestotheapplication,startingwiththeabilitytocreatenewto-doitems.
Createanewserviceclass
60
AddmorefeaturesNowthatyou'veconnectedtoadatabaseusingEntityFrameworkCore,you'rereadytoaddsomemorefeaturestotheapplication.First,you'llmakeitpossibletoaddnewto-doitemsusingaform.
Addmorefeatures
61
Addnewto-doitemsTheuserwilladdnewto-doitemswithasimpleformbelowthelist:
Addingthisfeaturerequiresafewsteps:
AddingaformtotheviewCreatinganewactiononthecontrollertohandletheformAddingcodetotheservicelayertoupdatethedatabase
Addaform
TheViews/Todo/Index.cshtmlviewhasaplaceholderfortheAddItemform:
<divclass="panel-footeradd-item-form">
<!--TODO:Additemform-->
</div>
Tokeepthingsseparateandorganized,you'llcreatetheformasapartialview.Apartialviewisasmallpieceofalargerviewthatlivesinaseparatefile.
CreateanAddItemPartial.cshtmlview:
Views/Todo/AddItemPartial.cshtml
Addnewto-doitems
62
@modelTodoItem
<formasp-action="AddItem"method="POST">
<labelasp-for="Title">Addanewitem:</label>
<inputasp-for="Title">
<buttontype="submit">Add</button>
</form>
Theasp-actiontaghelpercangenerateaURLfortheform,justlikewhenyouuseitonan<a>element.Inthiscase,theasp-actionhelpergetsreplacedwiththerealpathtotheAddItemrouteyou'llcreate:
<formaction="/Todo/AddItem"method="POST">
Addinganasp-taghelpertothe<form>elementalsoaddsahiddenfieldtotheformcontainingaverificationtoken.Thisverificationtokencanbeusedtopreventcross-siterequestforgery(CSRF)attacks.You'llverifythetokenwhenyouwritetheaction.
Thattakescareofcreatingthepartialview.Now,referenceitfromthemainTodoview:
Views/Todo/Index.cshtml
<divclass="panel-footeradd-item-form">
@awaitHtml.PartialAsync("AddItemPartial",newTodoItem())
</div>
Addanaction
WhenauserclicksAddontheformyoujustcreated,theirbrowserwillconstructaPOSTrequestto/Todo/AddItemonyourapplication.Thatwon'tworkrightnow,becausethereisn'tanyactionthatcanhandlethe/Todo/AddItemroute.Ifyoutryitnow,ASP.NETCorewillreturna404NotFounderror.
Addnewto-doitems
63
You'llneedtocreateanewactioncalledAddItemontheTodoController:
[ValidateAntiForgeryToken]
publicasyncTask<IActionResult>AddItem(TodoItemnewItem)
{
if(!ModelState.IsValid)
{
returnRedirectToAction("Index");
}
varsuccessful=await_todoItemService.AddItemAsync(newItem);
if(!successful)
{
returnBadRequest("Couldnotadditem.");
}
returnRedirectToAction("Index");
}
NoticehowthenewAddItemactionacceptsaTodoItemparameter?ThisisthesameTodoItemmodelyoucreatedintheMVCbasicschaptertostoreinformationaboutato-doitem.Whenit'susedhereasanactionparameter,ASP.NETCorewillautomaticallyperformaprocesscalledmodelbinding.
Modelbindinglooksatthedatainarequestandtriestointelligentlymatchtheincomingfieldswithpropertiesonthemodel.Inotherwords,whentheusersubmitsthisformandtheirbrowserPOSTstothisaction,ASP.NETCorewillgrabtheinformationfromtheformandplaceitinthenewItemvariable.
The[ValidateAntiForgeryToken]attributebeforetheactiontellsASP.NETCorethatitshouldlookfor(andverify)thehiddenverificationtokenthatwasaddedtotheformbytheasp-actiontaghelper.Thisisanimportantsecuritymeasuretopreventcross-siterequestforgery
Addnewto-doitems
64
(CSRF)attacks,whereyouruserscouldbetrickedintosubmittingdatafromamalicioussite.Theverificationtokenensuresthatyourapplicationisactuallytheonethatrenderedandsubmittedtheform.
TakealookattheAddItemPartial.cshtmlviewoncemore.The@modelTodoItemlineatthetopofthefiletellsASP.NETCorethattheviewshouldexpecttobepairedwiththeTodoItemmodel.Thismakesitpossibletouseasp-for="Title"onthe<input>tagtoletASP.NETCoreknowthatthisinputelementisfortheTitleproperty.
Becauseofthe@modelline,thepartialviewwillexpecttobepassedaTodoItemobjectwhenit'srendered.PassingitanewTodoItemviaHtml.PartialAsyncinitializestheformwithanemptyitem.(Tryappending{Title="hello"}andseewhathappens!)
Duringmodelbinding,anymodelpropertiesthatcan'tbematchedupwithfieldsintherequestareignored.SincetheformonlyincludesaTitleinputelement,youcanexpectthattheotherpropertiesonTodoItem(theIsDoneflag,theDueAtdate)willbeemptyorcontaindefaultvalues.
InsteadofreusingtheTodoItemmodel,anotherapproachwouldbetocreateaseparatemodel(likeNewTodoItem)that'sonlyusedforthisactionandonlyhasthespecificproperties(Title)youneedforaddinganewto-doitem.Modelbindingisstillused,butthiswayyou'veseparatedthemodelthat'susedforstoringato-doiteminthedatabasefromthemodelthat'susedforbindingincomingrequestdata.Thisissometimescalledabindingmodeloradatatransferobject(DTO).Thispatterniscommoninlarger,morecomplexprojects.
Afterbindingtherequestdatatothemodel,ASP.NETCorealsoperformsmodelvalidation.Validationcheckswhetherthedataboundtothemodelfromtheincomingrequestmakessenseorisvalid.Youcan
Addnewto-doitems
65
addattributestothemodeltotellASP.NETCorehowitshouldbevalidated.
The[Required]attributeontheTitlepropertytellsASP.NETCore'smodelvalidatortoconsiderthetitleinvalidifitismissingorblank.TakealookatthecodeoftheAddItemaction:thefirstblockcheckswhethertheModelState(themodelvalidationresult)isvalid.It'scustomarytodothisvalidationcheckrightatthebeginningoftheaction:
if(!ModelState.IsValid)
{
returnRedirectToAction("Index");
}
IftheModelStateisinvalidforanyreason,thebrowserwillberedirectedtothe/Todo/Indexroute,whichrefreshesthepage.
Next,thecontrollercallsintotheservicelayertodotheactualdatabaseoperationofsavingthenewto-doitem:
varsuccessful=await_todoItemService.AddItemAsync(newItem);
if(!successful)
{
returnBadRequest(new{error="Couldnotadditem."});
}
TheAddItemAsyncmethodwillreturntrueorfalsedependingonwhethertheitemwassuccessfullyaddedtothedatabase.Ifitfailsforsomereason,theactionwillreturnanHTTP400BadRequesterroralongwithanobjectthatcontainsanerrormessage.
Finally,ifeverythingcompletedwithouterrors,theactionredirectsthebrowsertothe/Todo/Indexroute,whichrefreshesthepageanddisplaysthenew,updatedlistofto-doitemstotheuser.
Addaservicemethod
Addnewto-doitems
66
Ifyou'reusingacodeeditorthatunderstandsC#,you'llseeredsquiggelylinesunderAddItemAsyncbecausethemethoddoesn'texistyet.
Asalaststep,youneedtoaddamethodtotheservicelayer.First,addittotheinterfacedefinitioninITodoItemService:
publicinterfaceITodoItemService
{
Task<TodoItem[]>GetIncompleteItemsAsync();
Task<bool>AddItemAsync(TodoItemnewItem);
}
Then,theactualimplementationinTodoItemService:
publicasyncTask<bool>AddItemAsync(TodoItemnewItem)
{
newItem.Id=Guid.NewGuid();
newItem.IsDone=false;
newItem.DueAt=DateTimeOffset.Now.AddDays(3);
_context.Items.Add(newItem);
varsaveResult=await_context.SaveChangesAsync();
returnsaveResult==1;
}
ThenewItem.TitlepropertyhasalreadybeensetbyASP.NETCore'smodelbinder,sothismethodonlyneedstoassignanIDandsetthedefaultvaluesfortheotherproperties.Then,thenewitemisaddedtothedatabasecontext.Itisn'tactuallysaveduntilyoucallSaveChangesAsync().Ifthesaveoperationwassuccessful,SaveChangesAsync()willreturn1.
Tryitout
Addnewto-doitems
67
Runtheapplicationandaddsomeitemstoyourto-dolistwiththeform.Sincetheitemsarebeingstoredinthedatabase,they'llstillbethereevenafteryoustopandstarttheapplicationagain.
Asanextrachallenge,tryaddingadatepickerusingHTMLandJavaScript,andlettheuserchoosean(optional)datefortheDueAtproperty.Then,usethatdateinsteadofalwaysmakingnewtasksthatareduein3days.
Addnewto-doitems
68
CompleteitemswithacheckboxAddingitemstoyourto-dolistisgreat,buteventuallyyou'llneedtogetthingsdone,too.IntheViews/Todo/Index.cshtmlview,acheckboxisrenderedforeachto-doitem:
<inputtype="checkbox"class="done-checkbox">
Clickingthecheckboxdoesn'tdoanything(yet).Justlikethelastchapter,you'lladdthisbehaviorusingformsandactions.Inthiscase,you'llalsoneedatinybitofJavaScriptcode.
Addformelementstotheview
First,updatetheviewandwrapeachcheckboxwitha<form>element.Then,addahiddenelementcontainingtheitem'sID:
Views/Todo/Index.cshtml
<td>
<formasp-action="MarkDone"method="POST">
<inputtype="checkbox"class="done-checkbox">
<inputtype="hidden"name="id"value="@item.Id">
</form>
</td>
Whentheforeachlooprunsintheviewandprintsarowforeachto-doitem,acopyofthisformwillexistineachrow.Thehiddeninputcontainingtheto-doitem'sIDmakesitpossibleforyourcontrollercodetotellwhichboxwaschecked.(Withoutit,you'dbeabletotellthatsomeboxwaschecked,butnotwhichone.)
Completeitemswithacheckbox
69
Ifyourunyourapplicationrightnow,thecheckboxesstillwon'tdoanything,becausethere'snosubmitbuttontotellthebrowsertocreateaPOSTrequestwiththeform'sdata.Youcouldaddasubmitbuttonundereachcheckbox,butthatwouldbeasillyuserexperience.Ideally,clickingthecheckboxshouldautomaticallysubmittheform.YoucanachievethatbyaddingsomeJavaScript.
AddJavaScriptcode
Findthesite.jsfileinthewwwroot/jsdirectoryandaddthiscode:
wwwroot/js/site.js
$(document).ready(function(){
//WireupallofthecheckboxestorunmarkCompleted()
$('.done-checkbox').on('click',function(e){
markCompleted(e.target);
});
});
functionmarkCompleted(checkbox){
checkbox.disabled=true;
varrow=checkbox.closest('tr');
$(row).addClass('done');
varform=checkbox.closest('form');
form.submit();
}
ThiscodefirstusesjQuery(aJavaScripthelperlibrary)toattachsomecodetotheclickevenofallthecheckboxesonthepagewiththeCSSclassdone-checkbox.Whenacheckboxisclicked,themarkCompleted()functionisrun.
ThemarkCompleted()functiondoesafewthings:
Completeitemswithacheckbox
70
Addsthedisabledattributetothecheckboxsoitcan'tbeclickedagainAddsthedoneCSSclasstotheparentrowthatcontainsthecheckbox,whichchangesthewaytherowlooksbasedontheCSSrulesinstyle.cssSubmitstheform
Thattakescareoftheviewandfrontendcode.Nowit'stimetoaddanewaction!
Addanactiontothecontroller
Asyou'veprobablyguessed,youneedtoaddanactioncalledMarkDoneintheTodoController:
[ValidateAntiForgeryToken]
publicasyncTask<IActionResult>MarkDone(Guidid)
{
if(id==Guid.Empty)
{
returnRedirectToAction("Index");
}
varsuccessful=await_todoItemService.MarkDoneAsync(id);
if(!successful)
{
returnBadRequest("Couldnotmarkitemasdone.");
}
returnRedirectToAction("Index");
}
Let'sstepthrougheachlineofthisactionmethod.First,themethodacceptsaGuidparametercalledidinthemethodsignature.UnliketheAddItemaction,whichusedamodelandmodelbinding/validation,theidparameterisverysimple.Iftheincomingrequestdataincludesa
Completeitemswithacheckbox
71
fieldcalledid,ASP.NETCorewilltrytoparseitasaguid.Thisworksbecausethehiddenelementyouaddedtothecheckboxformisnamedid.
Sinceyouaren'tusingmodelbinding,there'snoModelStatetocheckforvalidity.Instead,youcanchecktheguidvaluedirectlytomakesureit'svalid.Ifforsomereasontheidparameterintherequestwasmissingorcouldn'tbeparsedasaguid,idwillhaveavalueofGuid.Empty.Ifthat'sthecase,theactiontellsthebrowsertoredirectto/Todo/Indexandrefreshthepage.
Next,thecontrollerneedstocalltheservicelayertoupdatethedatabase.ThiswillbehandledbyanewmethodcalledMarkDoneAsyncontheITodoItemServiceinterface,whichwillreturntrueorfalsedependingonwhethertheupdatesucceeded:
varsuccessful=await_todoItemService.MarkDoneAsync(id);
if(!successful)
{
returnBadRequest("Couldnotmarkitemasdone.");
}
Finally,ifeverythinglooksgood,thebrowserisredirectedtothe/Todo/Indexactionandthepageisrefreshed.
Withtheviewandcontrollerupdated,allthat'sleftisaddingthemissingservicemethod.
Addaservicemethod
First,addMarkDoneAsynctotheinterfacedefinition:
Services/ITodoItemService.cs
Task<bool>MarkDoneAsync(Guidid);
Completeitemswithacheckbox
72
Then,addtheconcreteimplementationtotheTodoItemService:
Services/TodoItemService.cs
publicasyncTask<bool>MarkDoneAsync(Guidid)
{
varitem=await_context.Items
.Where(x=>x.Id==id)
.SingleOrDefaultAsync();
if(item==null)returnfalse;
item.IsDone=true;
varsaveResult=await_context.SaveChangesAsync();
returnsaveResult==1;//Oneentityshouldhavebeenupdated
}
ThismethodusesEntityFrameworkCoreandWhere()tofindanitembyIDinthedatabase.TheSingleOrDefaultAsync()methodwilleitherreturntheitemornullifitcouldn'tbefound.
Onceyou'resurethatitemisn'tnull,it'sasimplematterofsettingtheIsDoneproperty:
item.IsDone=true;
ChangingthepropertyonlyaffectsthelocalcopyoftheitemuntilSaveChangesAsync()iscalledtopersistthechangebacktothedatabase.SaveChangesAsync()returnsanumberthatindicateshowmanyentitieswereupdatedduringthesaveoperation.Inthiscase,it'lleitherbe1(theitemwasupdated)or0(somethingwentwrong).
Tryitout
Completeitemswithacheckbox
73
Runtheapplicationandtrycheckingsomeitemsoffthelist.Refreshthepageandthey'lldisappearcompletely,becauseoftheWhere()filterintheGetIncompleteItemsAsync()method.
Rightnow,theapplicationcontainsasingle,sharedto-dolist.It'dbeevenmoreusefulifitkepttrackofindividualto-dolistsforeachuser.Inthenextchapter,you'lladdloginandsecurityfeaturestotheproject.
Completeitemswithacheckbox
74
SecurityandidentitySecurityisamajorconcernofanymodernwebapplicationorAPI.It'simportanttokeepyouruserorcustomerdatasafeandoutofthehandsofattackers.Thisisaverybroadtopic,involvingthingslike:
SanitizingdatainputtopreventSQLinjectionattacksPreventingcross-domain(CSRF)attacksinformsUsingHTTPS(connectionencryption)sodatacan'tbeinterceptedasittravelsovertheInternetGivingusersawaytosecurelysigninwithapasswordorothercredentialsDesigningpasswordreset,accountrecovery,andmulti-factorauthenticationflows
ASP.NETCorecanhelpmakeallofthiseasiertoimplement.Thefirsttwo(protectionagainstSQLinjectionandcross-domainattacks)arealreadybuilt-in,andyoucanaddafewlinesofcodetoenableHTTPSsupport.Thischapterwillmainlyfocusontheidentityaspectsofsecurity:handlinguseraccounts,authenticating(loggingin)youruserssecurely,andmakingauthorizationdecisionsoncetheyareauthenticated.
Authenticationandauthorizationaredistinctideasthatareoftenconfused.Authenticationdealswithwhetherauserisloggedin,whileauthorizationdealswithwhattheyareallowedtodoaftertheylogin.Youcanthinkofauthenticationasaskingthequestion,"DoIknowwhothisuseris?"Whileauthorizationasks,"DoesthisuserhavepermissiontodoX?"
Securityandidentity
75
TheMVC+IndividualAuthenticationtemplateyouusedtoscaffoldtheprojectincludesanumberofclassesbuiltontopofASP.NETCoreIdentity,anauthenticationandidentitysystemthat'spartofASP.NETCore.Outofthebox,thisaddstheabilitytologinwithanemailandpassword.
WhatisASP.NETCoreIdentity?ASP.NETCoreIdentityistheidentitysystemthatshipswithASP.NETCore.LikeeverythingelseintheASP.NETCoreecosystem,it'sasetofNuGetpackagesthatcanbeinstalledinanyproject(andarealreadyincludedifyouusethedefaulttemplate).
ASP.NETCoreIdentitytakescareofstoringuseraccounts,hashingandstoringpasswords,andmanagingrolesforusers.Itsupportsemail/passwordlogin,multi-factorauthentication,socialloginwithproviderslikeGoogleandFacebook,aswellasconnectingtootherservicesusingprotocolslikeOAuth2.0andOpenIDConnect.
TheRegisterandLoginviewsthatshipwiththeMVC+IndividualAuthenticationtemplatealreadytakeadvantageofASP.NETCoreIdentity,andtheyalreadywork!Tryregisteringforanaccountandloggingin.
Securityandidentity
76
RequireauthenticationOftenyou'llwanttorequiretheusertologinbeforetheycanaccesscertainpartsofyourapplication.Forexample,itmakessensetoshowthehomepagetoeveryone(whetheryou'reloggedinornot),butonlyshowyourto-dolistafteryou'veloggedin.
Youcanusethe[Authorize]attributeinASP.NETCoretorequirealogged-inuserforaparticularaction,oranentirecontroller.TorequireauthenticationforallactionsoftheTodoController,addtheattributeabovethefirstlineofthecontroller:
Controllers/TodoController.cs
[Authorize]
publicclassTodoController:Controller
{
//...
}
Addthisusingstatementatthetopofthefile:
usingMicrosoft.AspNetCore.Authorization;
Tryrunningtheapplicationandaccessing/todowithoutbeingloggedin.You'llberedirectedtotheloginpageautomatically.
The[Authorize]attributeisactuallydoinganauthenticationcheckhere,notanauthorizationcheck(despitethenameoftheattribute).Later,you'llusetheattributetocheckbothauthenticationandauthorization.
Requireauthentication
77
Requireauthentication
78
UsingidentityintheapplicationTheto-dolistitemsthemselvesarestillsharedbetweenallusers,becausethestoredto-doentitiesaren'ttiedtoaparticularuser.Nowthatthe[Authorize]attributeensuresthatyoumustbeloggedintoseetheto-doview,youcanfilterthedatabasequerybasedonwhoisloggedin.
First,injectaUserManager<ApplicationUser>intotheTodoController:
Controllers/TodoController.cs
[Authorize]
publicclassTodoController:Controller
{
privatereadonlyITodoItemService_todoItemService;
privatereadonlyUserManager<ApplicationUser>_userManager;
publicTodoController(ITodoItemServicetodoItemService,
UserManager<ApplicationUser>userManager)
{
_todoItemService=todoItemService;
_userManager=userManager;
}
//...
}
You'llneedtoaddanewusingstatementatthetop:
usingMicrosoft.AspNetCore.Identity;
TheUserManagerclassispartofASP.NETCoreIdentity.YoucanuseittogetthecurrentuserintheIndexaction:
publicasyncTask<IActionResult>Index()
Usingidentityintheapplication
79
{
varcurrentUser=await_userManager.GetUserAsync(User);
if(currentUser==null)returnChallenge();
varitems=await_todoItemService
.GetIncompleteItemsAsync(currentUser);
varmodel=newTodoViewModel()
{
Items=items
};
returnView(model);
}
ThenewcodeatthetopoftheactionmethodusestheUserManagertolookupthecurrentuserfromtheUserpropertyavailableintheaction:
varcurrentUser=await_userManager.GetUserAsync(User);
Ifthereisalogged-inuser,theUserpropertycontainsalightweightobjectwithsome(butnotall)oftheuser'sinformation.TheUserManagerusesthistolookupthefulluserdetailsinthedatabaseviatheGetUserAsync()method.
ThevalueofcurrentUsershouldneverbenull,becausethe[Authorize]attributeispresentonthecontroller.However,it'sagoodideatodoasanitycheck,justincase.YoucanusetheChallenge()methodtoforcetheusertologinagainiftheirinformationismissing:
if(currentUser==null)returnChallenge();
Sinceyou'renowpassinganApplicationUserparametertoGetIncompleteItemsAsync(),you'llneedtoupdatetheITodoItemServiceinterface:
Services/ITodoItemService.cs
Usingidentityintheapplication
80
publicinterfaceITodoItemService
{
Task<TodoItem[]>GetIncompleteItemsAsync(
ApplicationUseruser);
//...
}
SinceyouchangedtheITodoItemServiceinterface,youalsoneedtoupdatethesignatureoftheGetIncompleteItemsAsync()methodintheTodoItemService:
Services/TodoItemService
publicasyncTask<TodoItem[]>GetIncompleteItemsAsync(
ApplicationUseruser)
Thenextstepistoupdatethedatabasequeryandaddafiltertoshowonlytheitemscreatedbythecurrentuser.Beforeyoucandothat,youneedtoaddanewpropertytothedatabase.
Updatethedatabase
You'llneedtoaddanewpropertytotheTodoItementitymodelsoeachitemcan"remember"theuserthatownsit:
Models/TodoItem.cs
publicstringUserId{get;set;}
Sinceyouupdatedtheentitymodelusedbythedatabasecontext,youalsoneedtomigratethedatabase.Createanewmigrationusingdotnetefintheterminal:
dotnetefmigrationsaddAddItemUserId
Usingidentityintheapplication
81
ThiscreatesanewmigrationcalledAddItemUserIdwhichwilladdanewcolumntotheItemstable,mirroringthechangeyoumadetotheTodoItemmodel.
Usedotnetefagaintoapplyittothedatabase:
dotnetefdatabaseupdate
Updatetheserviceclass
Withthedatabaseandthedatabasecontextupdated,youcannowupdatetheGetIncompleteItemsAsync()methodintheTodoItemServiceandaddanotherclausetotheWherestatement:
Services/TodoItemService.cs
publicasyncTask<TodoItem[]>GetIncompleteItemsAsync(
ApplicationUseruser)
{
returnawait_context.Items
.Where(x=>x.IsDone==false&&x.UserId==user.Id)
.ToArrayAsync();
}
Ifyouruntheapplicationandregisterorlogin,you'llseeanemptyto-dolistonceagain.Unfortunately,anyitemsyoutrytoadddisappearintotheether,becauseyouhaven'tupdatedtheAddItemactiontobeuser-awareyet.
UpdatetheAddItemandMarkDoneactions
You'llneedtousetheUserManagertogetthecurrentuserintheAddItemandMarkDoneactionmethods,justlikeyoudidinIndex.
Herearebothupdatedmethods:
Usingidentityintheapplication
82
Controllers/TodoController.cs
[ValidateAntiForgeryToken]
publicasyncTask<IActionResult>AddItem(TodoItemnewItem)
{
if(!ModelState.IsValid)
{
returnRedirectToAction("Index");
}
varcurrentUser=await_userManager.GetUserAsync(User);
if(currentUser==null)returnChallenge();
varsuccessful=await_todoItemService
.AddItemAsync(newItem,currentUser);
if(!successful)
{
returnBadRequest("Couldnotadditem.");
}
returnRedirectToAction("Index");
}
[ValidateAntiForgeryToken]
publicasyncTask<IActionResult>MarkDone(Guidid)
{
if(id==Guid.Empty)
{
returnRedirectToAction("Index");
}
varcurrentUser=await_userManager.GetUserAsync(User);
if(currentUser==null)returnChallenge();
varsuccessful=await_todoItemService
.MarkDoneAsync(id,currentUser);
if(!successful)
{
returnBadRequest("Couldnotmarkitemasdone.");
}
returnRedirectToAction("Index");
Usingidentityintheapplication
83
}
BothservicemethodsmustnowacceptanApplicationUserparameter.UpdatetheinterfacedefinitioninITodoItemService:
Task<bool>AddItemAsync(TodoItemnewItem,ApplicationUseruser);
Task<bool>MarkDoneAsync(Guidid,ApplicationUseruser);
Andfinally,updatetheservicemethodimplementationsintheTodoItemService.InAddItemAsyncmethod,settheUserIdpropertywhenyouconstructanewTodoItem:
publicasyncTask<bool>AddItemAsync(
TodoItemnewItem,ApplicationUseruser)
{
newItem.Id=Guid.NewGuid();
newItem.IsDone=false;
newItem.DueAt=DateTimeOffset.Now.AddDays(3);
newItem.UserId=user.Id;
//...
}
TheWhereclauseintheMarkDoneAsyncmethodalsoneedstocheckfortheuser'sID,soarogueusercan'tcompletesomeoneelse'sitemsbyguessingtheirIDs:
publicasyncTask<bool>MarkDoneAsync(
Guidid,ApplicationUseruser)
{
varitem=await_context.Items
.Where(x=>x.Id==id&&x.UserId==user.Id)
.SingleOrDefaultAsync();
//...
}
Usingidentityintheapplication
84
Alldone!Tryusingtheapplicationwithtwodifferentuseraccounts.Theto-doitemsstayprivateforeachaccount.
Usingidentityintheapplication
85
AuthorizationwithrolesRolesareacommonapproachtohandlingauthorizationandpermissionsinawebapplication.Forexample,it'scommontocreateanAdministratorrolethatgivesadminusersmorepermissionsorpowerthannormalusers.
Inthisproject,you'lladdaManageUserspagethatonlyadministratorscansee.Ifnormaluserstrytoaccessit,they'llseeanerror.
AddaManageUserspage
First,createanewcontroller:
Controllers/ManageUsersController.cs
usingSystem;
usingSystem.Linq;
usingSystem.Threading.Tasks;
usingMicrosoft.AspNetCore.Mvc;
usingMicrosoft.AspNetCore.Authorization;
usingMicrosoft.AspNetCore.Identity;
usingAspNetCoreTodo.Models;
usingMicrosoft.EntityFrameworkCore;
namespaceAspNetCoreTodo.Controllers
{
[Authorize(Roles="Administrator")]
publicclassManageUsersController:Controller
{
privatereadonlyUserManager<ApplicationUser>
_userManager;
publicManageUsersController(
UserManager<ApplicationUser>userManager)
{
_userManager=userManager;
}
Authorizationwithroles
86
publicasyncTask<IActionResult>Index()
{
varadmins=(await_userManager
.GetUsersInRoleAsync("Administrator"))
.ToArray();
vareveryone=await_userManager.Users
.ToArrayAsync();
varmodel=newManageUsersViewModel
{
Administrators=admins,
Everyone=everyone
};
returnView(model);
}
}
}
SettingtheRolespropertyonthe[Authorize]attributewillensurethattheusermustbeloggedinandassignedtheAdministratorroleinordertoviewthepage.
Next,createaviewmodel:
Models/ManageUsersViewModel.cs
usingSystem.Collections.Generic;
usingAspNetCoreTodo.Models;
namespaceAspNetCoreTodo.Models
{
publicclassManageUsersViewModel
{
publicApplicationUser[]Administrators{get;set;}
publicApplicationUser[]Everyone{get;set;}
}
}
Authorizationwithroles
87
Finally,createaViews/ManageUsersfolderandaviewfortheIndexaction:
Views/ManageUsers/Index.cshtml
@modelManageUsersViewModel
@{
ViewData["Title"]="Manageusers";
}
<h2>@ViewData["Title"]</h2>
<h3>Administrators</h3>
<tableclass="table">
<thead>
<tr>
<td>Id</td>
<td>Email</td>
</tr>
</thead>
@foreach(varuserinModel.Administrators)
{
<tr>
<td>@user.Id</td>
<td>@user.Email</td>
</tr>
}
</table>
<h3>Everyone</h3>
<tableclass="table">
<thead>
<tr>
<td>Id</td>
<td>Email</td>
</tr>
</thead>
@foreach(varuserinModel.Everyone)
Authorizationwithroles
88
{
<tr>
<td>@user.Id</td>
<td>@user.Email</td>
</tr>
}
</table>
Startuptheapplicationandtrytoaccessthe/ManageUsersroutewhileloggedinasanormaluser.You'llseethisaccessdeniedpage:
That'sbecauseusersaren'tassignedtheAdministratorroleautomatically.
Createatestadministratoraccount
Forobvioussecurityreasons,itisn'tpossibleforanyonetoregisteranewadministratoraccountthemselves.Infact,theAdministratorroledoesn'tevenexistinthedatabaseyet!
YoucanaddtheAdministratorroleplusatestadministratoraccounttothedatabasethefirsttimetheapplicationstartsup.Addingfirst-timedatatothedatabaseiscalledinitializingorseedingthedatabase.
CreateanewclassintherootoftheprojectcalledSeedData:
Authorizationwithroles
89
SeedData.cs
usingSystem;
usingSystem.Threading.Tasks;
usingAspNetCoreTodo.Models;
usingMicrosoft.AspNetCore.Identity;
usingMicrosoft.EntityFrameworkCore;
usingMicrosoft.Extensions.DependencyInjection;
namespaceAspNetCoreTodo
{
publicstaticclassSeedData
{
publicstaticasyncTaskInitializeAsync(
IServiceProviderservices)
{
varroleManager=services
.GetRequiredService<RoleManager<IdentityRole>>();
awaitEnsureRolesAsync(roleManager);
varuserManager=services
.GetRequiredService<UserManager<ApplicationUser>>(
);
awaitEnsureTestAdminAsync(userManager);
}
}
}
TheInitializeAsync()methodusesanIServiceProvider(thecollectionofservicesthatissetupintheStartup.ConfigureServices()method)togettheRoleManagerandUserManagerfromASP.NETCoreIdentity.
AddtwomoremethodsbelowtheInitializeAsync()method.First,theEnsureRolesAsync()method:
privatestaticasyncTaskEnsureRolesAsync(
RoleManager<IdentityRole>roleManager)
{
varalreadyExists=awaitroleManager
.RoleExistsAsync(Constants.AdministratorRole);
Authorizationwithroles
90
if(alreadyExists)return;
awaitroleManager.CreateAsync(
newIdentityRole(Constants.AdministratorRole));
}
ThismethodcheckstoseeifanAdministratorroleexistsinthedatabase.Ifnot,itcreatesone.Insteadofrepeatedlytypingthestring"Administrator",createasmallclasscalledConstantstoholdthevalue:
Constants.cs
namespaceAspNetCoreTodo
{
publicstaticclassConstants
{
publicconststringAdministratorRole="Administrator";
}
}
Ifyouwant,youcanupdatetheManageUsersControllertousethisconstantvalueaswell.
Next,writetheEnsureTestAdminAsync()method:
SeedData.cs
privatestaticasyncTaskEnsureTestAdminAsync(
UserManager<ApplicationUser>userManager)
{
vartestAdmin=awaituserManager.Users
.Where(x=>x.UserName=="[email protected]")
.SingleOrDefaultAsync();
if(testAdmin!=null)return;
testAdmin=newApplicationUser
{
Authorizationwithroles
91
UserName="[email protected]",
Email="[email protected]"
};
awaituserManager.CreateAsync(
testAdmin,"NotSecure123!!");
awaituserManager.AddToRoleAsync(
testAdmin,Constants.AdministratorRole);
}
Ifthereisn'[email protected],thismethodwillcreateoneandassignatemporarypassword.Afteryouloginforthefirsttime,youshouldchangetheaccount'spasswordtosomethingsecure!
Next,youneedtotellyourapplicationtorunthislogicwhenitstartsup.ModifyProgram.csandupdateMain()tocallanewmethod,InitializeDatabase():
Program.cs
publicstaticvoidMain(string[]args)
{
varhost=BuildWebHost(args);
InitializeDatabase(host);
host.Run();
}
Then,addthenewmethodtotheclassbelowMain():
privatestaticvoidInitializeDatabase(IWebHosthost)
{
using(varscope=host.Services.CreateScope())
{
varservices=scope.ServiceProvider;
try
{
SeedData.InitializeAsync(services).Wait();
}
Authorizationwithroles
92
catch(Exceptionex)
{
varlogger=services
.GetRequiredService<ILogger<Program>>();
logger.LogError(ex,"ErroroccurredseedingtheDB.");
}
}
}
Addthisusingstatementtothetopofthefile:
usingMicrosoft.Extensions.DependencyInjection;
ThismethodgetstheservicecollectionthatSeedData.InitializeAsync()needsandthenrunsthemethodtoseedthedatabase.Ifsomethinggoeswrong,anerrorislogged.
BecauseInitializeAsync()returnsaTask,theWait()methodmustbeusedtomakesureitfinishesbeforetheapplicationstartsup.You'dnormallyuseawaitforthis,butfortechnicalreasonsyoucan'tuseawaitintheProgramclass.Thisisarareexception.Youshoulduseawaiteverywhereelse!
Whenyoustarttheapplicationnext,theadmin@todo.localaccountwillbecreatedandassignedtheAdministratorrole.Trylogginginwiththisaccount,andnavigatingtohttp://localhost:5000/ManageUsers.You'llseealistofallusersregisteredfortheapplication.
Asanextrachallenge,tryaddingmoreadministrationfeaturestothispage.Forexample,youcouldaddabuttonthatgivesanadministratortheabilitytodeleteauseraccount.
Checkforauthorizationinaview
Authorizationwithroles
93
The[Authorize]attributemakesiteasytoperformanauthorizationcheckinacontrolleroractionmethod,butwhatifyouneedtocheckauthorizationinaview?Forexample,itwouldbenicetodisplaya"Manageusers"linkinthenavigationbarifthelogged-inuserisanadministrator.
YoucaninjecttheUserManagerdirectlyintoaviewtodothesetypesofauthorizationchecks.Tokeepyourviewscleanandorganized,createanewpartialviewthatwilladdanitemtothenavbarinthelayout:
Views/Shared/_AdminActionsPartial.cshtml
@usingMicrosoft.AspNetCore.Identity
@usingAspNetCoreTodo.Models
@injectSignInManager<ApplicationUser>signInManager
@injectUserManager<ApplicationUser>userManager
@if(signInManager.IsSignedIn(User))
{
varcurrentUser=awaitUserManager.GetUserAsync(User);
varisAdmin=currentUser!=null
&&awaituserManager.IsInRoleAsync(
currentUser,
Constants.AdministratorRole);
if(isAdmin)
{
<ulclass="navnavbar-navnavbar-right">
<li>
<aasp-controller="ManageUsers"
asp-action="Index">
ManageUsers
</a>
</li>
</ul>
}
}
Authorizationwithroles
94
It'sconventionaltonamesharedpartialviewsstartingwithan_underscore,butit'snotrequired.
ThispartialviewfirstusestheSignInManagertoquicklydeterminewhethertheuserisloggedin.Iftheyaren't,therestoftheviewcodecanbeskipped.Ifthereisalogged-inuser,theUserManagerisusedtolookuptheirdetailsandperformanauthorizationcheckwithIsInRoleAsync().Ifallcheckssucceedandtheuserisanadminstrator,aManageuserslinkisaddedtothenavbar.
Toincludethispartialinthemainlayout,edit_Layout.cshtmlandadditinthenavbarsection:
Views/Shared/_Layout.cshtml
<divclass="navbar-collapsecollapse">
<ulclass="navnavbar-nav">
<!--existingcodehere-->
</ul>
@awaitHtml.PartialAsync("_LoginPartial")
@awaitHtml.PartialAsync("_AdminActionsPartial")
</div>
Whenyouloginwithanadministratoraccount,you'llnowseeanewitemonthetopright:
Authorizationwithroles
95
MoreresourcesASP.NETCoreIdentityhelpsyouaddsecurityandidentityfeatureslikeloginandregistrationtoyourapplication.Thedotnetnewtemplatesgiveyoupre-builtviewsandcontrollersthathandlethesecommonscenariossoyoucangetupandrunningquickly.
There'smuchmorethatASP.NETCoreIdentitycando,suchaspasswordresetandsociallogin.Thedocumentationavailableathttp://docs.asp.netisafantasticresourceforlearninghowtoaddthesefeatures.
AlternativestoASP.NETCoreIdentity
ASP.NETCoreIdentityisn'ttheonlywaytoaddidentityfunctionality.Anotherapproachistouseacloud-hostedidentityservicelikeAzureActiveDirectoryB2CorOktatohandleidentityforyourapplication.Youcanthinkoftheseoptionsaspartofaprogression:
Do-it-yourselfsecurity:Notrecommended,unlessyouareasecurityexpert!ASP.NETCoreIdentity:Yougetalotofcodeforfreewiththetemplates,whichmakesitprettyeasytogetstarted.You'llstillneedtowritesomecodeformoreadvancedscenarios,andmaintainadatabasetostoreuserinformation.Cloud-hostedidentityservices.Theservicehandlesbothsimpleandadvancedscenarios(multi-factorauthentication,accountrecovery,federation),andsignificantlyreducestheamountofcodeyouneedtowriteandmaintaininyourapplication.Plus,sensitiveuserdataisn'tstoredinyourowndatabase.
Moreresources
96
Forthisproject,ASP.NETCoreIdentityisagreatfit.Formorecomplexprojects,I'drecommenddoingsomeresearchandexperimentingwithbothoptionstounderstandwhichisbestforyourusecase.
Moreresources
97
AutomatedtestingWritingtestsisanimportantpartofbuildinganyapplication.Testingyourcodehelpsyoufindandavoidbugs,andmakesiteasiertorefactoryourcodelaterwithoutbreakingfunctionalityorintroducingnewproblems.
Inthischapteryou'lllearnhowtowritebothunittestsandintegrationteststhatexerciseyourASP.NETCoreapplication.Unittestsaresmallteststhatmakesureasinglemethodorchunkoflogicworksproperly.Integrationtests(sometimescalledfunctionaltests)arelargerteststhatsimulatereal-worldscenariosandtestmultiplelayersorpartsofyourapplication.
Automatedtesting
98
UnittestingUnittestsaresmall,shortteststhatcheckthebehaviorofasinglemethodorclass.Whenthecodeyou'retestingreliesonothermethodsorclasses,unittestsrelyonmockingthoseotherclassessothatthetestonlyfocusesononethingatatime.
Forexample,theTodoControllerclasshastwodependencies:anITodoItemServiceandtheUserManager.TheTodoItemService,inturn,dependsontheApplicationDbContext.(TheideathatyoucandrawalinefromTodoController>TodoItemService>ApplicationDbContextiscalledadependencygraph).
Whentheapplicationrunsnormally,theASP.NETCoreservicecontaineranddependencyinjectionsysteminjectseachofthoseobjectsintothedependencygraphwhentheTodoControllerortheTodoItemServiceiscreated.
Whenyouwriteaunittest,ontheotherhand,youhavetohandlethedependencygraphyourself.It'stypicaltoprovidetest-onlyor"mocked"versionsofthosedependencies.Thismeansyoucanisolatejustthelogicintheclassormethodyouaretesting.(Thisisimportant!Ifyou'retestingaservice,youdon'twanttoalsobeaccidentallywritingtoyourdatabase.)
Createatestproject
It'sabestpracticetocreateaseparateprojectforyourtests,sotheyarekeptseparatefromyourapplicationcode.Thenewtestprojectshouldliveinadirectorythat'snextto(notinside)yourmainproject'sdirectory.
Unittesting
99
Ifyou'recurrentlyinyourprojectdirectory,cduponelevel.(ThisrootdirectorywillalsobecalledAspNetCoreTodo).Thenusethiscommandtoscaffoldanewtestproject:
dotnetnewxunit-oAspNetCoreTodo.UnitTests
xUnit.NETisapopulartestframeworkfor.NETcodethatcanbeusedtowritebothunitandintegrationtests.Likeeverythingelse,it'sasetofNuGetpackagesthatcanbeinstalledinanyproject.Thedotnetnewxunittemplatealreadyincludeseverythingyouneed.
Yourdirectorystructureshouldnowlooklikethis:
AspNetCoreTodo/
AspNetCoreTodo/
AspNetCoreTodo.csproj
Controllers/
(etc...)
AspNetCoreTodo.UnitTests/
AspNetCoreTodo.UnitTests.csproj
Sincethetestprojectwillusetheclassesdefinedinyourmainproject,you'llneedtoaddareferencetotheAspNetCoreTodoproject:
dotnetaddreference../AspNetCoreTodo/AspNetCoreTodo.csproj
DeletetheUnitTest1.csfilethat'sautomaticallycreated.You'rereadytowriteyourfirsttest.
Ifyou'reusingVisualStudioCode,youmayneedtocloseandreopentheVisualStudioCodewindowtogetcodecompletionworkinginthenewproject.
Writeaservicetest
Unittesting
100
TakealookatthelogicintheAddItemAsync()methodoftheTodoItemService:
publicasyncTask<bool>AddItemAsync(
TodoItemnewItem,ApplicationUseruser)
{
newItem.Id=Guid.NewGuid();
newItem.IsDone=false;
newItem.DueAt=DateTimeOffset.Now.AddDays(3);
newItem.UserId=user.Id;
_context.Items.Add(newItem);
varsaveResult=await_context.SaveChangesAsync();
returnsaveResult==1;
}
Thismethodmakesanumberofdecisionsorassumptionsaboutthenewitem(inotherwords,performsbusinesslogiconthenewitem)beforeitactuallysavesittothedatabase:
TheUserIdpropertyshouldbesettotheuser'sIDNewitemsshouldalwaysbeincomplete(IsDone=false)ThetitleofthenewitemshouldbecopiedfromnewItem.TitleNewitemsshouldalwaysbedue3daysfromnow
ImagineifyouorsomeoneelserefactoredtheAddItemAsync()methodandforgotaboutpartofthisbusinesslogic.Thebehaviorofyourapplicationcouldchangewithoutyourealizingit!Youcanpreventthisbywritingatestthatdouble-checksthatthisbusinesslogichasn'tchanged(evenifthemethod'sinternalimplementationchanges).
Itmightseemunlikelynowthatyoucouldintroduceachangeinbusinesslogicwithoutrealizingit,butitbecomesmuchhardertokeeptrackofdecisionsandassumptionsinalarge,complexproject.Thelargeryourprojectis,themoreimportantitistohaveautomatedchecksthatmakesurenothinghaschanged!
Unittesting
101
TowriteaunittestthatwillverifythelogicintheTodoItemService,createanewclassinyourtestproject:
AspNetCoreTodo.UnitTests/TodoItemServiceShould.cs
usingSystem;
usingSystem.Threading.Tasks;
usingAspNetCoreTodo.Data;
usingAspNetCoreTodo.Models;
usingAspNetCoreTodo.Services;
usingMicrosoft.EntityFrameworkCore;
usingXunit;
namespaceAspNetCoreTodo.UnitTests
{
publicclassTodoItemServiceShould
{
[Fact]
publicasyncTaskAddNewItemAsIncompleteWithDueDate()
{
//...
}
}
}
Therearemanydifferentwaysofnamingandorganizingtests,allwithdifferentprosandcons.IlikepostfixingmytestclasseswithShouldtocreateareadablesentencewiththetestmethodname,butfeelfreetouseyourownstyle!
The[Fact]attributecomesfromthexUnit.NETpackage,anditmarksthismethodasatestmethod.
TheTodoItemServicerequiresanApplicationDbContext,whichisnormallyconnectedtoyourdatabase.Youwon'twanttousethatfortests.Instead,youcanuseEntityFrameworkCore'sin-memorydatabaseproviderinyourtestcode.Sincetheentiredatabaseexistsinmemory,
Unittesting
102
it'swipedouteverytimethetestisrestarted.And,sinceit'saproperEntityFrameworkCoreprovider,theTodoItemServicewon'tknowthedifference!
UseaDbContextOptionsBuildertoconfigurethein-memorydatabaseprovider,andthenmakeacalltoAddItemAsync():
varoptions=newDbContextOptionsBuilder<ApplicationDbContext>()
.UseInMemoryDatabase(databaseName:"Test_AddNewItem").Options;
//Setupacontext(connectiontothe"DB")forwriting
using(varcontext=newApplicationDbContext(options))
{
varservice=newTodoItemService(context);
varfakeUser=newApplicationUser
{
Id="fake-000",
UserName="[email protected]"
};
awaitservice.AddItemAsync(newTodoItem
{
Title="Testing?"
},fakeUser);
}
Thelastlinecreatesanewto-doitemcalledTesting?,andtellstheservicetosaveittothe(in-memory)database.
Toverifythatthebusinesslogicrancorrectly,writesomemorecodebelowtheexistingusingblock:
//Useaseparatecontexttoreaddatabackfromthe"DB"
using(varcontext=newApplicationDbContext(options))
{
varitemsInDatabase=awaitcontext
.Items.CountAsync();
Assert.Equal(1,itemsInDatabase);
Unittesting
103
varitem=awaitcontext.Items.FirstAsync();
Assert.Equal("Testing?",item.Title);
Assert.Equal(false,item.IsDone);
//Itemshouldbedue3daysfromnow(giveortakeasecond)
vardifference=DateTimeOffset.Now.AddDays(3)-item.DueAt;
Assert.True(difference<TimeSpan.FromSeconds(1));
}
Thefirstassertionisasanitycheck:thereshouldneverbemorethanoneitemsavedtothein-memorydatabase.Assumingthat'strue,thetestretrievesthesaveditemwithFirstAsyncandthenassertsthatthepropertiesaresettotheexpectedvalues.
BothunitandintegrationteststypicallyfollowtheAAA(Arrange-Act-Assert)pattern:objectsanddataaresetupfirst,thensomeactionisperformed,andfinallythetestchecks(asserts)thattheexpectedbehavioroccurred.
Assertingadatetimevalueisalittletricky,sincecomparingtwodatesforequalitywillfailifeventhemillisecondcomponentsaredifferent.Instead,thetestchecksthattheDueAtvalueislessthanasecondawayfromtheexpectedvalue.
Runthetest
Ontheterminal,runthiscommand(makesureyou'restillintheAspNetCoreTodo.UnitTestsdirectory):
dotnettest
Thetestcommandscansthecurrentprojectfortests(markedwith[Fact]attributesinthiscase),andrunsallthetestsitfinds.You'llseeoutputsimilarto:
Startingtestexecution,pleasewait...
Unittesting
104
Discovering:AspNetCoreTodo.UnitTests
Discovered:AspNetCoreTodo.UnitTests
Starting:AspNetCoreTodo.UnitTests
Finished:AspNetCoreTodo.UnitTests
Totaltests:1.Passed:1.Failed:0.Skipped:0.
TestRunSuccessful.
Testexecutiontime:1.9074Seconds
YounowhaveonetestprovidingtestcoverageoftheTodoItemService.Asanextrachallenge,trywritingunitteststhatensure:
TheMarkDoneAsync()methodreturnsfalseifit'spassedanIDthatdoesn'texistTheMarkDoneAsync()methodreturnstruewhenitmakesavaliditemascompleteTheGetIncompleteItemsAsync()methodreturnsonlytheitemsownedbyaparticularuser
Unittesting
105
IntegrationtestingComparedtounittests,integrationtestsaremuchlargerinscope.exercisethewholeapplicationstack.Insteadofisolatingoneclassormethod,integrationtestsensurethatallofthecomponentsofyourapplicationareworkingtogetherproperly:routing,controllers,services,databasecode,andsoon.
Integrationtestsareslowerandmoreinvolvedthanunittests,soit'scommonforaprojecttohavelotsofsmallunittestsbutonlyahandfulofintegrationtests.
Inordertotestthewholestack(includingcontrollerrouting),integrationteststypicallymakeHTTPcallstoyourapplicationjustlikeawebbrowserwould.
TowriteintegrationteststhatmakeHTTPrequests,youcouldmanuallystartyourapplicationandtestsatthesametime,andwriteyourteststomakerequeststohttp://localhost:5000.ASP.NETCoreprovidesanicerwaytohostyourapplicationfortesting,however:theTestServerclass.TestServercanhostyourapplicationforthedurationofthetest,andthenstopitautomaticallywhenthetestiscomplete.
Createatestproject
Ifyou'recurrentlyinyourprojectdirectory,cduponeleveltotherootAspNetCoreTododirectory.Usethiscommandtoscaffoldanewtestproject:
dotnetnewxunit-oAspNetCoreTodo.IntegrationTests
Yourdirectorystructureshouldnowlooklikethis:
Integrationtesting
106
AspNetCoreTodo/
AspNetCoreTodo/
AspNetCoreTodo.csproj
Controllers/
(etc...)
AspNetCoreTodo.UnitTests/
AspNetCoreTodo.UnitTests.csproj
AspNetCoreTodo.IntegrationTests/
AspNetCoreTodo.IntegrationTests.csproj
Ifyouprefer,youcankeepyourunittestsandintegrationtestsinthesameproject.Forlargeprojects,it'scommontosplitthemupsoit'seasytorunthemseparately.
Sincethetestprojectwillusetheclassesdefinedinyourmainproject,you'llneedtoaddareferencetothemainproject:
dotnetaddreference../AspNetCoreTodo/AspNetCoreTodo.csproj
You'llalsoneedtoaddtheMicrosoft.AspNetCore.TestHostNuGetpackage:
dotnetaddpackageMicrosoft.AspNetCore.TestHost
DeletetheUnitTest1.csfilethat'screatedbydotnetnew.You'rereadytowriteanintegrationtest.
Writeanintegrationtest
Thereareafewthingsthatneedtobeconfiguredonthetestserverbeforeeachtest.Insteadofclutteringthetestwiththissetupcode,youcankeepthissetupinaseparateclass.CreateanewclasscalledTestFixture:
Integrationtesting
107
AspNetCoreTodo.IntegrationTests/TestFixture.cs
usingSystem;
usingSystem.Collections.Generic;
usingSystem.IO;
usingSystem.Net.Http;
usingMicrosoft.AspNetCore.Hosting;
usingMicrosoft.AspNetCore.TestHost;
usingMicrosoft.Extensions.Configuration;
namespaceAspNetCoreTodo.IntegrationTests
{
publicclassTestFixture:IDisposable
{
privatereadonlyTestServer_server;
publicHttpClientClient{get;}
publicTestFixture()
{
varbuilder=newWebHostBuilder()
.UseStartup<AspNetCoreTodo.Startup>()
.ConfigureAppConfiguration((context,config)=>
{
config.SetBasePath(Path.Combine(
Directory.GetCurrentDirectory(),
"..\\..\\..\\..\\AspNetCoreTodo"));
config.AddJsonFile("appsettings.json");
});
_server=newTestServer(builder);
Client=_server.CreateClient();
Client.BaseAddress=newUri("http://localhost:8888");
}
publicvoidDispose()
{
Client.Dispose();
_server.Dispose();
}
}
}
Integrationtesting
108
ThisclasstakescareofsettingupaTestServer,andwillhelpkeeptheteststhemselvescleanandtidy.
Nowyou're(really)readytowriteanintegrationtest.CreateanewclasscalledTodoRouteShould:
AspNetCoreTodo.IntegrationTests/TodoRouteShould.cs
usingSystem.Net;
usingSystem.Net.Http;
usingSystem.Threading.Tasks;
usingXunit;
namespaceAspNetCoreTodo.IntegrationTests
{
publicclassTodoRouteShould:IClassFixture<TestFixture>
{
privatereadonlyHttpClient_client;
publicTodoRouteShould(TestFixturefixture)
{
_client=fixture.Client;
}
[Fact]
publicasyncTaskChallengeAnonymousUser()
{
//Arrange
varrequest=newHttpRequestMessage(
HttpMethod.Get,"/todo");
//Act:requestthe/todoroute
varresponse=await_client.SendAsync(request);
//Assert:theuserissenttotheloginpage
Assert.Equal(
HttpStatusCode.Redirect,
response.StatusCode);
Assert.Equal(
"http://localhost:8888/Account"+
Integrationtesting
109
"/Login?ReturnUrl=%2Ftodo",
response.Headers.Location.ToString());
}
}
}
Thistestmakesananonymous(not-logged-in)requesttothe/todorouteandverifiesthatthebrowserisredirectedtotheloginpage.
Thisscenarioisagoodcandidateforanintegrationtest,becauseitinvolvesmultiplecomponentsoftheapplication:theroutingsystem,thecontroller,thefactthatthecontrollerismarkedwith[Authorize],andsoon.It'salsoagoodtestbecauseitensuresyouwon'teveraccidentallyremovethe[Authorize]attributeandmaketheto-doviewaccessibletoeveryone.
RunthetestRunthetestintheterminalwithdotnettest.Ifeverything'sworkingright,you'llseeasuccessmessage:
Startingtestexecution,pleasewait...
Discovering:AspNetCoreTodo.IntegrationTests
Discovered:AspNetCoreTodo.IntegrationTests
Starting:AspNetCoreTodo.IntegrationTests
Finished:AspNetCoreTodo.IntegrationTests
Totaltests:1.Passed:1.Failed:0.Skipped:0.
TestRunSuccessful.
Testexecutiontime:2.0588Seconds
Wrapup
Integrationtesting
110
Testingisabroadtopic,andthere'smuchmoretolearn.Thischapterdoesn'ttouchonUItestingortestingfrontend(JavaScript)code,whichprobablydeserveentirebooksoftheirown.Youshould,however,havetheskillsandbaseknowledgeyouneedtolearnmoreabouttestingandtopracticewritingtestsforyourownapplications.
TheASP.NETCoredocumentation(https://docs.asp.net)andStackOverflowaregreatresourcesforlearningmoreandfindinganswerswhenyougetstuck.
Integrationtesting
111
DeploytheapplicationYou'vecomealongway,butyou'renotquitedoneyet.Onceyou'vecreatedagreatapplication,youneedtoshareitwiththeworld!
BecauseASP.NETCoreapplicationscanrunonWindows,Mac,orLinux,thereareanumberofdifferentwaysyoucandeployyourapplication.Inthischapter,I'llshowyouthemostcommon(andeasiest)waystogolive.
DeploymentoptionsASP.NETCoreapplicationsaretypicallydeployedtooneoftheseenvironments:
ADockerhost.AnymachinecapableofhostingDockercontainerscanbeusedtohostanASP.NETCoreapplication.CreatingaDockerimageisaveryquickwaytogetyourapplicationdeployed,especiallyifyou'refamiliarwithDocker.(Ifyou'renot,don'tworry!I'llcoverthestepslater.)
Azure.MicrosoftAzurehasnativesupportforASP.NETCoreapplications.IfyouhaveanAzuresubscription,youjustneedtocreateaWebAppanduploadyourprojectfiles.I'llcoverhowtodothiswiththeAzureCLIinthenextsection.
Linux(withNginx).Ifyoudon'twanttogotheDockerroute,youcanstillhostyourapplicationonanyLinuxserver(thisincludesAmazonEC2andDigitalOceanvirtualmachines).It'stypicaltopairASP.NETCorewiththeNginxreverseproxy.(MoreaboutNginxbelow.)
Deploytheapplication
112
Windows.YoucanusetheIISwebserveronWindowstohostASP.NETCoreapplications.It'susuallyeasier(andcheaper)tojustdeploytoAzure,butifyouprefermanagingWindowsserversyourself,it'llworkjustfine.
KestrelandreverseproxiesIfyoudon'tcareaboutthegutsofhostingASP.NETCoreapplicationsandjustwantthestep-by-stepinstructions,feelfreetoskiptooneofthenexttwosections.
ASP.NETCoreincludesafast,lightweightwebservercalledKestrel.It'stheserveryou'vebeenusingeverytimeyourandotnetrunandbrowsedtohttp://localhost:5000.Whenyoudeployyourapplicationtoaproductionenvironment,it'llstilluseKestrelbehindthescenes.However,it'srecommendedthatyouputareverseproxyinfrontofKestrel,becauseKestreldoesn'tyethaveloadbalancingandotherfeaturesthatmorematurewebservershave.
OnLinux(andinDockercontainers),youcanuseNginxortheApachewebservertoreceiveincomingrequestsfromtheinternetandroutethemtoyourapplicationhostedwithKestrel.Ifyou'reonWindows,IISdoesthesamething.
Ifyou'reusingAzuretohostyourapplication,thisisalldoneforyouautomatically.I'llcoversettingupNginxasareverseproxyintheDockersection.
Deploytheapplication
113
DeploytoAzureDeployingyourASP.NETCoreapplicationtoAzureonlytakesafewsteps.YoucandoitthroughtheAzurewebportal,oronthecommandlineusingtheAzureCLI.I'llcoverthelatter.
Whatyou'llneed
Git(usegit--versiontomakesureit'sinstalled)TheAzureCLI(followtheinstallinstructionsathttps://github.com/Azure/azure-cli)AnAzuresubscription(thefreesubscriptionisfine)Adeploymentconfigurationfileinyourprojectroot
Createadeploymentconfigurationfile
Sincetherearemultipleprojectsinyourdirectorystructure(thewebapplication,andtwotestprojects),Azurewon'tknowwhichonetopublish.Tofixthis,createafilecalled.deploymentattheverytopofyourdirectorystructure:
.deployment
[config]
project=AspNetCoreTodo/AspNetCoreTodo.csproj
Makesureyousavethefileas.deploymentwithnootherpartstothename.(OnWindows,youmayneedtoputquotesaroundthefilename,like".deployment",topreventa.txtextensionfrombeingadded.)
Ifyoulsordirinyourtop-leveldirectory,youshouldseetheseitems:
DeploytoAzure
114
.deployment
AspNetCoreTodo
AspNetCoreTodo.IntegrationTests
AspNetCoreTodo.UnitTests
SetuptheAzureresources
IfyoujustinstalledtheAzureCLIforthefirsttime,run
azlogin
andfollowthepromptstologinonyourmachine.Then,createanewResourceGroupforthisapplication:
azgroupcreate-lwestus-nAspNetCoreTodoGroup
ThiscreatesaResourceGroupintheWestUSregion.Ifyou'relocatedfarawayfromthewesternUS,useazaccountlist-locationstogetalistoflocationsandfindoneclosertoyou.
Next,createanAppServiceplaninthegroupyoujustcreated:
azappserviceplancreate-gAspNetCoreTodoGroup-nAspNetCoreTodo
Plan--skuF1
F1isthefreeappplan.Ifyouwanttouseacustomdomainnamewithyourapp,usetheD1($10/month)planorhigher.
NowcreateaWebAppintheAppServiceplan:
azwebappcreate-gAspNetCoreTodoGroup-pAspNetCoreTodoPlan-nM
yTodoApp
DeploytoAzure
115
Thenameoftheapp(MyTodoAppabove)mustbegloballyuniqueinAzure.Oncetheappiscreated,itwillhaveadefaultURLintheformat:http://mytodoapp.azurewebsites.net
DeployyourprojectfilestoAzure
YoucanuseGittopushyourapplicationfilesuptotheAzureWebApp.Ifyourlocaldirectoryisn'talreadytrackedasaGitrepo,runthesecommandstosetitup:
gitinit
gitadd.
gitcommit-m"Firstcommit!"
Next,createanAzureusernameandpasswordfordeployment:
azwebappdeploymentuserset--user-namenate
Followtheinstructionstocreateapassword.Thenuseconfig-local-gittospitoutaGitURL:
azwebappdeploymentsourceconfig-local-git-gAspNetCoreTodoGrou
p-nMyTodoApp--outtsv
https://[email protected]/MyTodoApp.git
CopytheURLtotheclipboard,anduseittoaddaGitremotetoyourlocalrepository:
gitremoteaddazure<paste>
Youonlyneedtodothesestepsonce.Now,wheneveryouwanttopushyourapplicationfilestoAzure,checktheminwithGitandrun
DeploytoAzure
116
gitpushazuremaster
You'llseeastreamoflogmessagesastheapplicationisdeployedtoAzure.
Whenit'scomplete,browsetohttp://yourappname.azurewebsites.nettocheckouttheapp!
DeploytoAzure
117
DeploywithDockerIfyouaren'tusingaplatformlikeAzure,containerizationtechnologieslikeDockercanmakeiteasytodeploywebapplicationstoyourownservers.Insteadofspendingtimeconfiguringaserverwiththedependenciesitneedstorunyourapp,copyingfiles,andrestartingprocesses,youcansimplycreateaDockerimagethatdescribeseverythingyourappneedstorun,andspinitupasacontaineronanyDockerhost.
Dockercanmakescalingyourappacrossmultipleserverseasier,too.Onceyouhaveanimage,usingittocreate1containeristhesameprocessascreating100containers.
Beforeyoustart,youneedtheDockerCLIinstalledonyourdevelopmentmachine.Searchfor"getdockerfor(mac/windows/linux)"andfollowtheinstructionsontheofficialDockerwebsite.Youcanverifythatit'sinstalledcorrectlywith
dockerversion
AddaDockerfile
Thefirstthingyou'llneedisaDockerfile,whichislikearecipethattellsDockerwhatyourapplicationneedstobuildandrun.
CreateafilecalledDockerfile(noextension)intheroot,top-levelAspNetCoreTodofolder.Openitinyourfavoriteeditor.Writethefollowingline:
FROMmicrosoft/dotnet:2.0-sdkASbuild
DeploywithDocker
118
ThistellsDockertousethemicrosoft/dotnet:2.0-sdkimageasastartingpoint.ThisimageispublishedbyMicrosoftandcontainsthetoolsanddependenciesyouneedtoexecutedotnetbuildandcompileyourapplication.Byusingthispre-builtimageasastartingpoint,Dockercanoptimizetheimageproducedforyourappandkeepitsmall.
Next,addthisline:
COPYAspNetCoreTodo/*.csproj./app/AspNetCoreTodo/
TheCOPYcommandcopiesthe.csprojprojectfileintotheimageatthepath/app/AspNetCoreTodo/.Notethatnoneoftheactualcode(.csfiles)havebeencopiedintotheimageyet.You'llseewhyinaminute.
WORKDIR/app/AspNetCoreTodo
RUNdotnetrestore
WORKDIRistheDockerequivalentofcd.Thismeansanycommandsexecutednextwillrunfrominsidethe/app/AspNetCoreTododirectorythattheCOPYcommandcreatedinthelaststep.
RunningthedotnetrestorecommandrestorestheNuGetpackagesthattheapplicationneeds,definedinthe.csprojfile.Byrestoringpackagesinsidetheimagebeforeaddingtherestofthecode,Dockerisabletocachetherestoredpackages.Then,ifyoumakecodechanges(butdon'tchangethepackagesdefinedintheprojectfile),rebuildingtheDockerimagewillbesuperfast.
Nowit'stimetocopytherestofthecodeandcompiletheapplication:
COPYAspNetCoreTodo/../AspNetCoreTodo/
RUNdotnetpublish-oout/p:PublishWithAspNetCoreTargetManifest="
false"
DeploywithDocker
119
Thedotnetpublishcommandcompilestheproject,andthe-ooutflagputsthecompiledfilesinadirectorycalledout.
Thesecompiledfileswillbeusedtoruntheapplicationwiththefinalfewcommands:
FROMmicrosoft/dotnet:2.0-runtimeASruntime
ENVASPNETCORE_URLShttp://+:80
WORKDIR/app
COPY--from=build/app/AspNetCoreTodo/out./
ENTRYPOINT["dotnet","AspNetCoreTodo.dll"]
TheFROMcommandisusedagaintoselectasmallerimagethatonlyhasthedependenciesneededtoruntheapplication.TheENVcommandisusedtosetenvironmentvariablesinthecontainer,andtheASPNETCORE_URLSenvironmentvariabletellsASP.NETCorewhichnetworkinterfaceandportitshouldbindto(inthiscase,port80).
TheENTRYPOINTcommandletsDockerknowthatthecontainershouldbestartedasanexecutablebyrunningdotnetAspNetCoreTodo.dll.Thistellsdotnettostartupyourapplicationfromthecompiledfilecreatedbydotnetpublishearlier.(Whenyoudodotnetrunduringdevelopment,you'reaccomplishingthesamethinginonestep.)
ThefullDockerfilelookslikethis:
Dockerfile
FROMmicrosoft/dotnet:2.0-sdkASbuild
COPYAspNetCoreTodo/*.csproj./app/AspNetCoreTodo/
WORKDIR/app/AspNetCoreTodo
RUNdotnetrestore
COPYAspNetCoreTodo/../
RUNdotnetpublish-oout/p:PublishWithAspNetCoreTargetManifest="
false"
FROMmicrosoft/dotnet:2.0-runtimeASruntime
DeploywithDocker
120
ENVASPNETCORE_URLShttp://+:80
WORKDIR/app
COPY--from=build/app/AspNetCoreTodo/out./
ENTRYPOINT["dotnet","AspNetCoreTodo.dll"]
Createanimage
MakesuretheDockerfileissaved,andthenusedockerbuildtocreateanimage:
dockerbuild-taspnetcoretodo.
Don'tmissthetrailingperiod!ThattellsDockertolookforaDockerfileinthecurrentdirectory.
Oncetheimageiscreated,youcanrundockerimagestotolistalltheimagesavailableonyourlocalmachine.Totestitoutinacontainer,run
dockerrun--nameaspnetcoretodo_sample--rm-it-p8080:80aspnet
coretodo
The-itflagtellsDockertorunthecontainerininteractivemode(outputtingtotheterminal,asopposedtorunninginthebackground).Whenyouwanttostopthecontainer,pressControl-C.
RemembertheASPNETCORE_URLSvariablethattoldASP.NETCoretolistenonport80?The-p8080:80optiontellsDockertomapport8080onyourmachinetothecontainer'sport80.Openupyourbrowserandnavigatetohttp://localhost:8080toseetheapplicationrunninginthecontainer!
SetupNginx
DeploywithDocker
121
Atthebeginningofthischapter,ImentionedthatyoushoulduseareverseproxylikeNginxtoproxyrequeststoKestrel.YoucanuseDockerforthis,too.
Theoverallarchitecturewillconsistoftwocontainers:anNginxcontainerlisteningonport80,forwardingrequeststothecontaineryoujustbuiltthathostsyourapplicationwithKestrel.
TheNginxcontainerneedsitsownDockerfile.TokeepitfromconflictingwiththeDockerfileyoujustcreated,makeanewdirectoryinthewebapplicationroot:
mkdirnginx
CreateanewDockerfileandaddtheselines:
nginx/Dockerfile
FROMnginx
COPYnginx.conf/etc/nginx/nginx.conf
Next,createannginx.conffile:
nginx/nginx.conf
events{worker_connections1024;}
http{
server{
listen80;
location/{
proxy_passhttp://kestrel:80;
proxy_http_version1.1;
proxy_set_headerUpgrade$http_upgrade;
proxy_set_headerConnection'keep-alive';
proxy_set_headerHost$host;
proxy_cache_bypass$http_upgrade;
}
DeploywithDocker
122
}
}
ThisconfigurationfiletellsNginxtoproxyincomingrequeststohttp://kestrel:80.(You'llseewhykestrelworksasahostnameinamoment.)
Whenyoumakedeployyourapplicationtoaproductionenvironment,youshouldaddtheserver_namedirectiveandvalidateandrestrictthehostheadertoknowngoodvalues.Formoreinformation,see:
https://github.com/aspnet/Announcements/issues/295
SetupDockerCompose
There'sonemorefiletocreate.Upintherootdirectory,createdocker-compose.yml:
docker-compose.yml
nginx:
build:./nginx
links:
-kestrel:kestrel
ports:
-"80:80"
kestrel:
build:.
ports:
-"80"
DockerComposeisatoolthathelpsyoucreateandrunmulti-containerapplications.Thisconfigurationfiledefinestwocontainers:nginxfromthe./nginx/Dockerfilerecipe,andkestrelfromthe./Dockerfilerecipe.Thecontainersareexplicitlylinkedtogethersotheycancommunicate.
DeploywithDocker
123
Youcantryspinninguptheentiremulti-containerapplicationbyrunning:
docker-composeup
Tryopeningabrowserandnavigatingtohttp://localhost(port80,not8080!).Nginxislisteningonport80(thedefaultHTTPport)andproxyingrequeststoyourASP.NETCoreapplicationhostedbyKestrel.
SetupaDockerserver
Specificsetupinstructionsareoutsidethescopeofthisbook,butanymodernflavorofLinux(likeUbuntu)canbeusedtosetupaDockerhost.Forexample,youcouldcreateavirtualmachinewithAmazonEC2,andinstalltheDockerservice.Youcansearchfor"amazonec2setupdocker"(forexample)forinstructions.
IlikeusingDigitalOceanbecausethey'vemadeitreallyeasytogetstarted.DigitalOceanhasbothapre-builtDockervirtualmachine,andin-depthtutorialsforgettingDockerupandrunning(searchfor"digitaloceandocker").
DeploywithDocker
124
ConclusionThanksformakingittotheendoftheLittleASP.NETCoreBook!Ifthisbookwashelpful(ornot),I'dlovetohearyourthoughts.SendmeyourcommentsviaTwitter:https://twitter.com/nbarbettini
HowtolearnmoreThere'salotmorethatASP.NETCorecandothatcouldn'tfitinthisshortbook,including
BuildingRESTfulAPIsandmicroservicesUsingASP.NETCorewithsingle-pageappslikeAngularandReactRazorPagesBundlingandminifyingstaticassetsWebSocketsandSignalR
Thereareanumberofwaysyoucanlearnmore:
TheASP.NETCoredocumentation.TheofficialASP.NETCoredocumentationathttp://docs.asp.netcontainsanumberofin-depthtutorialscoveringmanyofthesetopics.I'dhighlyrecommendit!
ASP.NETCoreinAction.ThisbookbyAndrewLockisacomprehensive,deepdiveintoASP.NETCore.YoucangetitfromAmazonoralocalbookstore.
CoursesonLinkedInLearningandPluralsight.Ifyoulearnbestfromvideos,therearefantasticcoursesavailableonPluralsightandLinkedInLearning(includingsomebyyourstruly).Ifyoudon'thaveanaccountandneedacoupon,sendmeanemail:[email protected].
Conclusion
125
Nate'sblog.IalsowriteaboutASP.NETCoreandmoreonmyblogathttps://www.recaffeinate.co.
Happycoding!
AbouttheauthorHey,I'mNate!IwrotetheLittleASP.NETCoreBookinalong,caffeine-fueledweekendbecauseIlovethe.NETcommunityandwantedtogivebackinmyownlittleway.Ihopeithelpedyoulearnsomethingnew!
YoucanstayintouchwithmeonTwitter(@nbarbettini)oronmyblog(https://www.recaffeinate.co)[email protected].
SpecialthanksToJennifer,whoalwayssupportsmycrazyideas.
TothefollowingcontributorswhoimprovedtheLittleASP.NETCoreBook:
0xNFMattWelke
TotheseamazingpolyglotprogrammerswhotranslatedtheLittleASP.NETCoreBook:
sahinyanlik(Turkish)windsting,yuyi(SimplifiedChinese)
Changelog
Conclusion
126
Thefull,detailedchangelogisalwaysavailablehere:
https://github.com/nbarbettini/little-aspnetcore-book/releases
1.1.0(2018-05-03):SignificantlyreworkedtheAddmorefeatureschaptertouseMVCthoroughthewholestackandremovetheAJAXpattern.RemovedFacebooklogintosimplifythesecuritychapterandstreamlinetestinganddeployment.UpdatedtheDockerinstructionstoreflectthelatestbestpractices.Fixedtyposandaddedsuggestionsfromreaders.Thebookalsosportsanew,improvedcoverdesign!
1.0.4(2018-01-15):Addedexplanationofservicecontainerlifecycles,clarifiedserverportsandthe-oflag,andremovedsemicolonsafterRazordirectives.CorrectedChinesetranslationauthorcredit.Fixedothersmalltyposandissuesnoticedbyreaders.
1.0.3(2017-11-13):Typofixesandsmallimprovementssuggestedbyreaders.
1.0.2(2017-10-20):Morebugfixesandsmallimprovements.Addedlinktotranslations.
1.0.1(2017-09-23):Bugfixesandsmallimprovements.
1.0.0(2017-09-18):Initialrelease.
Conclusion
127