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