table of contents - sumit jangidsumitjangid.com/uploads/books/.net/the little aspnet core... ·...

Post on 10-Mar-2021

3 Views

Category:

Documents

0 Downloads

Preview:

Click to see full reader

TRANSCRIPT

1.1

1.2

1.2.1

1.2.2

1.2.3

1.3

1.3.1

1.3.2

1.3.3

1.3.4

1.3.5

1.3.6

1.3.7

1.4

1.5

1.5.1

1.5.2

1.5.3

1.5.4

1.6

1.6.1

1.6.2

1.7

TableofContentsIntroduction

Yourfirstapplication

GettheSDK

HelloWorldinC#

CreateanASP.NETCoreproject

MVCbasics

Createacontroller

Createmodels

Createaview

Addaserviceclass

Usedependencyinjection

Finishthecontroller

Updatethelayout

Addexternalpackages

Useadatabase

Connecttoadatabase

Updatethecontext

Createamigration

Createanewserviceclass

Addmorefeatures

Addnewto-doitems

Completeitemswithacheckbox

Securityandidentity

2

1.7.1

1.7.2

1.7.3

1.7.4

1.8

1.8.1

1.8.2

1.9

1.9.1

1.9.2

1.10

Requireauthentication

Usingidentityintheapplication

Authorizationwithroles

Moreresources

Automatedtesting

Unittesting

Integrationtesting

Deploytheapplication

DeploytoAzure

DeploywithDocker

Conclusion

3

TheLittleASP.NETCoreBookbyNateBarbettini

Copyright©2018.Allrightsreserved.

ISBN:978-1-387-75615-5

ReleasedundertheCreativeCommonsAttribution4.0license.Youarefreetoshare,copy,andredistributethisbookinanyformat,orremixandtransformitforanypurpose(evencommercially).Youmustgiveappropriatecreditandprovidealinktothelicense.

Formoreinformation,visithttps://creativecommons.org/licenses/by/4.0/

IntroductionThanksforpickingupTheLittleASP.NETCoreBook!IwrotethisshortbooktohelpdevelopersandpeopleinterestedinwebprogramminglearnaboutASP.NETCore,anewframeworkforbuildingwebapplicationsandAPIs.

TheLittleASP.NETCoreBookisstructuredasatutorial.You'llbuildanapplicationfromstarttofinishandlearn:

ThebasicsoftheMVC(Model-View-Controller)patternHowfront-endcode(HTML,CSS,JavaScript)workstogetherwithback-endcodeWhatdependencyinjectionisandwhyit'susefulHowtoreadandwritedatatoadatabaseHowtoaddlog-in,registration,andsecurityHowtodeploytheapplicationtotheweb

Introduction

4

Don'tworry,youdon'tneedtoknowanythingaboutASP.NETCore(oranyoftheabove)togetstarted.

BeforeyoubeginThecodeforthefinishedversionoftheapplicationyou'llbuildisavailableonGitHub:

https://www.github.com/nbarbettini/little-aspnetcore-todo

Feelfreetodownloaditifyouwanttoseethefinishedproduct,orcompareasyouwriteyourowncode.

Thebookitselfisupdatedfrequentlywithbugfixesandnewcontent.Ifyou'rereadingaPDF,e-book,orprintversion,checktheofficialwebsite(littleasp.net/book)toseeifthere'sanupdatedversionavailable.Theverylastpageofthebookcontainsversioninformationandachangelog.

Readinginyourownlanguage

Thankstosomefantasticmultilingualcontributors,theLittleASP.NETCoreBookhasbeentranslatedintootherlanguages:

Turkish:https://sahinyanlik.gitbooks.io/kisa-asp-net-core-kitabi/

Chinese:https://windsting.github.io/little-aspnetcore-book/book/

WhothisbookisforIfyou'renewtoprogramming,thisbookwillintroduceyoutothepatternsandconceptsusedtobuildmodernwebapplications.You'lllearnhowtobuildawebapp(andhowthebigpiecesfittogether)by

Introduction

5

buildingsomethingfromscratch!Whilethislittlebookwon'tbeabletocoverabsolutelyeverythingyouneedtoknowaboutprogramming,it'llgiveyouastartingpointsoyoucanlearnmoreadvancedtopics.

IfyoualreadycodeinabackendlanguagelikeNode,Python,Ruby,Go,orJava,you'llnoticealotoffamiliarideaslikeMVC,viewtemplates,anddependencyinjection.ThecodewillbeinC#,butitwon'tlooktoodifferentfromwhatyoualreadyknow.

Ifyou'reanASP.NETMVCdeveloper,you'llfeelrightathome!ASP.NETCoreaddssomenewtoolsandreuses(andsimplifies)thethingsyoualreadyknow.I'llpointoutsomeofthedifferencesbelow.

Nomatterwhatyourpreviousexperiencewithwebprogramming,thisbookwillteachyoueverythingyouneedtocreateasimpleandusefulwebapplicationinASP.NETCore.You'lllearnhowtobuildfunctionalityusingbackendandfrontendcode,howtointeractwithadatabase,andhowtodeploytheapptotheworld.

WhatisASP.NETCore?ASP.NETCoreisawebframeworkcreatedbyMicrosoftforbuildingwebapplications,APIs,andmicroservices.ItusescommonpatternslikeMVC(Model-View-Controller),dependencyinjection,andarequestpipelinecomprisedofmiddleware.It'sopen-sourceundertheApache2.0license,whichmeansthesourcecodeisfreelyavailable,andthecommunityisencouragedtocontributebugfixesandnewfeatures.

ASP.NETCorerunsontopofMicrosoft's.NETruntime,similartotheJavaVirtualMachine(JVM)ortheRubyinterpreter.YoucanwriteASP.NETCoreapplicationsinanumberoflanguages(C#,VisualBasic,F#).C#isthemostpopularchoice,andit'swhatI'lluseinthisbook.YoucanbuildandrunASP.NETCoreapplicationsonWindows,Mac,andLinux.

Introduction

6

Whydoweneedanotherwebframework?Therearealotofgreatwebframeworkstochoosefromalready:Node/Express,Spring,RubyonRails,Django,Laravel,andmanymore.WhatadvantagesdoesASP.NETCorehave?

Speed.ASP.NETCoreisfast.Because.NETcodeiscompiled,itexecutesmuchfasterthancodeininterpretedlanguageslikeJavaScriptorRuby.ASP.NETCoreisalsooptimizedformultithreadingandasynchronoustasks.It'scommontoseea5-10xspeedimprovementovercodewritteninNode.js.

Ecosystem.ASP.NETCoremaybenew,but.NEThasbeenaroundforalongtime.TherearethousandsofpackagesavailableonNuGet(the.NETpackagemanager;thinknpm,Rubygems,orMaven).TherearealreadypackagesavailableforJSONdeserialization,databaseconnectors,PDFgeneration,oralmostanythingelseyoucanthinkof.

Security.TheteamatMicrosofttakessecurityseriously,andASP.NETCoreisbuilttobesecurefromthegroundup.Ithandlesthingslikesanitizinginputdataandpreventingcross-siterequestforgery(CSRF)attacks,soyoudon'thaveto.Youalsogetthebenefitofstatictypingwiththe.NETcompiler,whichislikehavingaveryparanoidlinterturnedonatalltimes.Thismakesithardertodosomethingyoudidn'tintendwithavariableorchunkofdata.

.NETCoreand.NETStandardThroughoutthisbook,you'llbelearningaboutASP.NETCore(thewebframework).I'lloccasionallymentionthe.NETruntime,thesupportinglibrarythatruns.NETcode.IfthisalreadysoundslikeGreektoyou,just

Introduction

7

skiptothenextchapter!

Youmayalsohearabout.NETCoreand.NETStandard.Thenaminggetsconfusing,sohere'sasimpleexplanation:

.NETStandardisaplatform-agnosticinterfacethatdefinesfeaturesandAPIs.It'simportanttonotethat.NETStandarddoesn'trepresentanyactualcodeorfunctionality,justtheAPIdefinition.Therearedifferent"versions"orlevelsof.NETStandardthatreflecthowmanyAPIsareavailable(orhowwidetheAPIsurfaceareais).Forexample,.NETStandard2.0hasmoreAPIsavailablethan.NETStandard1.5,whichhasmoreAPIsthan.NETStandard1.0.

.NETCoreisthe.NETruntimethatcanbeinstalledonWindows,Mac,orLinux.ItimplementstheAPIsdefinedinthe.NETStandardinterfacewiththeappropriateplatform-specificcodeoneachoperatingsystem.Thisiswhatyou'llinstallonyourownmachinetobuildandrunASP.NETCoreapplications.

Andjustforgoodmeasure,.NETFrameworkisadifferentimplementationof.NETStandardthatisWindows-only.Thiswastheonly.NETruntimeuntil.NETCorecamealongandbrought.NETtoMacandLinux.ASP.NETCorecanalsorunonWindows-only.NETFramework,butIwon'ttouchonthistoomuch.

Ifyou'reconfusedbyallthisnaming,noworries!We'llgettosomerealcodeinabit.

AnotetoASP.NET4developersIfyouhaven'tusedapreviousversionofASP.NET,skipaheadtothenextchapter.

Introduction

8

ASP.NETCoreisacompleteground-uprewriteofASP.NET,withafocusonmodernizingtheframeworkandfinallydecouplingitfromSystem.Web,IIS,andWindows.IfyourememberalltheOWIN/KatanastufffromASP.NET4,you'realreadyhalfwaythere:theKatanaprojectbecameASP.NET5whichwasultimatelyrenamedtoASP.NETCore.

BecauseoftheKatanalegacy,theStartupclassisfrontandcenter,andthere'snomoreApplication_StartorGlobal.asax.Theentirepipelineisdrivenbymiddleware,andthere'snolongerasplitbetweenMVCandWebAPI:controllerscansimplyreturnviews,statuscodes,ordata.Dependencyinjectioncomesbakedin,soyoudon'tneedtoinstallandconfigureacontainerlikeStructureMaporNinjectifyoudon'twantto.Andtheentireframeworkhasbeenoptimizedforspeedandruntimeefficiency.

Alright,enoughintroduction.Let'sdiveintoASP.NETCore!

Introduction

9

YourfirstapplicationReadytobuildyourfirstwebappwithASP.NETCore?You'llneedtogatherafewthingsfirst:

Yourfavoritecodeeditor.YoucanuseAtom,Sublime,Notepad,orwhatevereditoryoupreferwritingcodein.Ifyoudon'thaveafavorite,giveVisualStudioCodeatry.It'safree,cross-platformcodeeditorthathasrichsupportforwritingC#,JavaScript,HTML,andmore.Justsearchfor"downloadvisualstudiocode"andfollowtheinstructions.

Ifyou'reonWindows,youcanalsouseVisualStudiotobuildASP.NETCoreapplications.You'llneedVisualStudio2017version15.3orlater(thefreeCommunityEditionisfine).VisualStudiohasgreatcodecompletionandrefactoringsupportforC#,althoughVisualStudioCodeisclosebehind.

The.NETCoreSDK.Regardlessoftheeditororplatformyou'reusing,you'llneedtoinstallthe.NETCoreSDK,whichincludestheruntime,baselibraries,andcommandlinetoolsyouneedforbuildingASP.NETCoreapplications.TheSDKcanbeinstalledonWindows,Mac,orLinux.

Onceyou'vedecidedonaneditor,you'llneedtogettheSDK.

Yourfirstapplication

10

GettheSDKSearchfor"download.netcore"andfollowtheinstructionsonMicrosoft'sdownloadpagetogetthe.NETCoreSDK.AftertheSDKhasfinishedinstalling,openuptheTerminal(orPowerShellonWindows)andusethedotnetcommandlinetool(alsocalledaCLI)tomakesureeverythingisworking:

dotnet--version

2.1.104

Youcangetmoreinformationaboutyourplatformwiththe--infoflag:

dotnet--info

.NETCommandLineTools(2.1.104)

ProductInformation:

Version:2.1.104

CommitSHA-1hash:48ec687460

RuntimeEnvironment:

OSName:MacOSX

OSVersion:10.13

(moredetails...)

Ifyouseeoutputliketheabove,you'rereadytogo!

GettheSDK

11

HelloWorldinC#BeforeyoudiveintoASP.NETCore,trycreatingandrunningasimpleC#application.

Youcandothisallfromthecommandline.First,openuptheTerminal(orPowerShellonWindows).Navigatetothelocationyouwanttostoreyourprojects,suchasyourDocumentsdirectory:

cdDocuments

Usethedotnetcommandtocreateanewproject:

dotnetnewconsole-oCsharpHelloWorld

Thedotnetnewcommandcreatesanew.NETprojectinC#bydefault.Theconsoleparameterselectsatemplateforaconsoleapplication(aprogramthatoutputstexttothescreen).The-oCsharpHelloWorldparametertellsdotnetnewtocreateanewdirectorycalledCsharpHelloWorldforalltheprojectfiles.Moveintothisnewdirectory:

cdCsharpHelloWorld

dotnetnewconsolecreatesabasicC#programthatwritesthetextHelloWorld!tothescreen.Theprogramiscomprisedoftwofiles:aprojectfile(witha.csprojextension)andaC#codefile(witha.csextension).Ifyouopentheformerinatextorcodeeditor,you'llseethis:

CsharpHelloWorld.csproj

<ProjectSdk="Microsoft.NET.Sdk">

HelloWorldinC#

12

<PropertyGroup>

<OutputType>Exe</OutputType>

<TargetFramework>netcoreapp2.0</TargetFramework>

</PropertyGroup>

</Project>

TheprojectfileisXML-basedanddefinessomemetadataabouttheproject.Later,whenyoureferenceotherpackages,thosewillbelistedhere(similartoapackage.jsonfilefornpm).Youwon'thavetoeditthisfilebyhandveryoften.

Program.cs

usingSystem;

namespaceCsharpHelloWorld

{

classProgram

{

staticvoidMain(string[]args)

{

Console.WriteLine("HelloWorld!");

}

}

}

staticvoidMainistheentrypointmethodofaC#program,andbyconventionit'splacedinaclass(atypeofcodestructureormodule)calledProgram.Theusingstatementatthetopimportsthebuilt-inSystemclassesfrom.NETandmakesthemavailabletothecodeinyourclass.

Frominsidetheprojectdirectory,usedotnetruntoruntheprogram.You'llseetheoutputwrittentotheconsoleafterthecodecompiles:

dotnetrun

HelloWorldinC#

13

HelloWorld!

That'sallittakestoscaffoldandruna.NETprogram!Next,you'lldothesamethingforanASP.NETCoreapplication.

HelloWorldinC#

14

CreateanASP.NETCoreprojectIfyou'restillinthedirectoryyoucreatedfortheHelloWorldsample,movebackuptoyourDocumentsorhomedirectory:

cd..

Next,createanewdirectorytostoreyourentireproject,andmoveintoit:

mkdirAspNetCoreTodo

cdAspNetCoreTodo

Next,createanewprojectwithdotnetnew,thistimewithsomeextraoptions:

dotnetnewmvc--authIndividual-oAspNetCoreTodo

cdAspNetCoreTodo

Thiscreatesanewprojectfromthemvctemplate,andaddssomeadditionalauthenticationandsecuritybitstotheproject.(I'llcoversecurityintheSecurityandidentitychapter.)

YoumightbewonderingwhyyouhaveadirectorycalledAspNetCoreTodoinsideanotherdirectorycalledAspNetCoreTodo.Thetop-levelor"root"directorycancontainoneormoreprojectdirectories.Therootdirectoryissometimescalledasolutiondirectory.Later,you'lladdmoreprojectdirectoriesside-by-sidewiththeAspNetCoreTodoprojectdirectory,allwithinasinglerootsolutiondirectory.

CreateanASP.NETCoreproject

15

You'llseequiteafewfilesshowupinthenewprojectdirectory.Onceyoucdintothenewdirectory,allyouhavetodoisruntheproject:

dotnetrun

Nowlisteningon:http://localhost:5000

Applicationstarted.PressCtrl+Ctoshutdown.

Insteadofprintingtotheconsoleandexiting,thisprogramstartsawebserverandwaitsforrequestsonport5000.

Openyourwebbrowserandnavigatetohttp://localhost:5000.You'llseethedefaultASP.NETCoresplashpage,whichmeansyourprojectisworking!Whenyou'redone,pressCtrl-Cintheterminalwindowtostoptheserver.

ThepartsofanASP.NETCoreproject

Thedotnetnewmvctemplategeneratesanumberoffilesanddirectoriesforyou.Herearethemostimportantthingsyougetoutofthebox:

TheProgram.csandStartup.csfilessetupthewebserverandASP.NETCorepipeline.TheStartupclassiswhereyoucanaddmiddlewarethathandlesandmodifiesincomingrequests,andservesthingslikestaticcontentorerrorpages.It'salsowhereyouaddyourownservicestothedependencyinjectioncontainer(moreonthislater).

TheModels,Views,andControllersdirectoriescontainthecomponentsoftheModel-View-Controller(MVC)architecture.You'llexploreallthreeinthenextchapter.

CreateanASP.NETCoreproject

16

ThewwwrootdirectorycontainsstaticassetslikeCSS,JavaScript,andimagefiles.Filesinwwwrootwillbeservedasstaticcontent,andcanbebundledandminifiedautomatically.

Theappsettings.jsonfilecontainsconfigurationsettingsASP.NETCorewillloadonstartup.Youcanusethistostoredatabaseconnectionstringsorotherthingsthatyoudon'twanttohard-code.

TipsforVisualStudioCode

Ifyou'reusingVisualStudioCodeforthefirsttime,hereareacoupleofhelpfultipstogetyoustarted:

Opentheprojectrootfolder:InVisualStudioCode,chooseFile-OpenorFile-OpenFolder.OpentheAspNetCoreTodofolder(therootdirectory),nottheinnerprojectdirectory.IfVisualStudioCodepromptsyoutoinstallmissingfiles,clickYestoaddthem.

F5torun(anddebugbreakpoints):Withyourprojectopen,pressF5toruntheprojectindebugmode.Thisisthesameasdotnetrunonthecommandline,butyouhavethebenefitofsettingbreakpointsinyourcodebyclickingontheleftmargin:

Lightbulbtofixproblems:Ifyourcodecontainsredsquiggles(compilererrors),putyourcursoronthecodethat'sredandlookforthelightbulbiconontheleftmargin.Thelightbulbmenuwillsuggest

CreateanASP.NETCoreproject

17

commonfixes,likeaddingamissingusingstatementtoyourcode:

Compilequickly:UsetheshortcutCommand-Shift-BorControl-Shift-BtoruntheBuildtask,whichdoesthesamethingasdotnetbuild.

ThesetipsapplytoVisualStudio(notCode)onWindowstoo.Ifyou'reusingVisualStudio,you'llneedtoopenthe.csprojprojectfiledirectly.VisualStudiowilllaterpromptyoutosavetheSolutionfile,whichyoushouldsaveintherootdirectory(thefirstAspNetCoreTodofolder).YoucanalsocreateanASP.NETCoreprojectdirectlywithinVisualStudiousingthetemplatesinFile-NewProject.

AnoteaboutGit

IfyouuseGitorGitHubtomanageyoursourcecode,nowisagoodtimetodogitinitandinitializeaGitrepositoryintheprojectrootdirectory:

cd..

gitinit

Makesureyouadda.gitignorefilethatignoresthebinandobjdirectories.TheVisualStudiotemplateonGitHub'sgitignoretemplaterepo(https://github.com/github/gitignore)worksgreat.

CreateanASP.NETCoreproject

18

There'splentymoretoexplore,solet'sdiveinandstartbuildinganapplication!

CreateanASP.NETCoreproject

19

MVCbasicsInthischapter,you'llexploretheMVCsysteminASP.NETCore.MVC(Model-View-Controller)isapatternforbuildingwebapplicationsthat'susedinalmosteverywebframework(RubyonRailsandExpressarepopularexamples),plusfrontendJavaScriptframeworkslikeAngular.MobileappsoniOSandAndroiduseavariationofMVCaswell.

Asthenamesuggests,MVChasthreecomponents:models,views,andcontrollers.Controllershandleincomingrequestsfromaclientorwebbrowserandmakedecisionsaboutwhatcodetorun.Viewsaretemplates(usuallyHTMLplusatemplatinglanguagelikeHandlebars,Pug,orRazor)thatgetdataaddedtothemandthenaredisplayedtotheuser.Modelsholdthedatathatisaddedtoviews,ordatathatisenteredbytheuser.

AcommonpatternforMVCcodeis:

ThecontrollerreceivesarequestandlooksupsomeinformationinadatabaseThecontrollercreatesamodelwiththeinformationandattachesittoaviewTheviewisrenderedanddisplayedintheuser'sbrowserTheuserclicksabuttonorsubmitsaform,whichsendsanewrequesttothecontroller,andthecyclerepeats

Ifyou'veworkedwithMVCinotherlanguages,you'llfeelrightathomeinASP.NETCoreMVC.Ifyou'renewtoMVC,thischapterwillteachyouthebasicsandwillhelpgetyoustarted.

Whatyou'llbuild

MVCbasics

20

The"HelloWorld"exerciseofMVCisbuildingato-dolistapplication.It'sagreatprojectsinceit'ssmallandsimpleinscope,butittoucheseachpartofMVCandcoversmanyoftheconceptsyou'duseinalargerapplication.

Inthisbook,you'llbuildato-doappthatletstheuseradditemstotheirto-dolistandcheckthemoffoncecomplete.Morespecifically,you'llbecreating:

Awebapplicationserver(sometimescalledthe"backend")usingASP.NETCore,C#,andtheMVCpatternAdatabasetostoretheuser'sto-doitemsusingtheSQLitedatabaseengineandasystemcalledEntityFrameworkCoreWebpagesandaninterfacethattheuserwillinteractwithviatheirbrowser,usingHTML,CSS,andJavaScript(calledthe"frontend")Aloginformandsecuritycheckssoeachuser'sto-dolistiskeptprivate

Soundgood?Let'sbuiltit!Ifyouhaven'talreadycreatedanewASP.NETCoreprojectusingdotnetnewmvc,followthestepsinthepreviouschapter.Youshouldbeabletobuildandruntheprojectandseethedefaultwelcomescreen.

MVCbasics

21

CreateacontrollerTherearealreadyafewcontrollersintheproject'sControllersdirectory,includingtheHomeControllerthatrendersthedefaultwelcomescreenyouseewhenyouvisithttp://localhost:5000.Youcanignorethesecontrollersfornow.

Createanewcontrollerfortheto-dolistfunctionality,calledTodoController,andaddthefollowingcode:

Controllers/TodoController.cs

usingSystem;

usingSystem.Collections.Generic;

usingSystem.Linq;

usingSystem.Threading.Tasks;

usingMicrosoft.AspNetCore.Mvc;

namespaceAspNetCoreTodo.Controllers

{

publicclassTodoController:Controller

{

//Actionsgohere

}

}

Routesthatarehandledbycontrollersarecalledactions,andarerepresentedbymethodsinthecontrollerclass.Forexample,theHomeControllerincludesthreeactionmethods(Index,About,andContact)whicharemappedbyASP.NETCoretotheserouteURLs:

localhost:5000/Home->Index()

localhost:5000/Home/About->About()

localhost:5000/Home/Contact->Contact()

Createacontroller

22

Thereareanumberofconventions(commonpatterns)usedbyASP.NETCore,suchasthepatternthatFooControllerbecomes/Foo,andtheIndexactionnamecanbeleftoutoftheURL.Youcancustomizethisbehaviorifyou'dlike,butfornow,we'llsticktothedefaultconventions.

AddanewactioncalledIndextotheTodoController,replacingthe//Actionsgoherecomment:

publicclassTodoController:Controller

{

publicIActionResultIndex()

{

//Getto-doitemsfromdatabase

//Putitemsintoamodel

//Renderviewusingthemodel

}

}

Actionmethodscanreturnviews,JSONdata,orHTTPstatuscodeslike200OKand404NotFound.TheIActionResultreturntypegivesyoutheflexibilitytoreturnanyofthesefromtheaction.

It'sabestpracticetokeepcontrollersaslightweightaspossible.Inthiscase,thecontrollerwillberesponsibleforgettingtheto-doitemsfromthedatabase,puttingthoseitemsintoamodeltheviewcanunderstand,andsendingtheviewbacktotheuser'sbrowser.

Beforeyoucanwritetherestofthecontrollercode,youneedtocreateamodelandaview.

Createacontroller

23

CreatemodelsTherearetwoseparatemodelclassesthatneedtobecreated:amodelthatrepresentsato-doitemstoredinthedatabase(sometimescalledanentity),andthemodelthatwillbecombinedwithaview(theMVinMVC)andsentbacktotheuser'sbrowser.Becausebothofthemcanbereferredtoas"models",I'llrefertothelatterasaviewmodel.

First,createaclasscalledTodoItemintheModelsdirectory:

Models/TodoItem.cs

usingSystem;

usingSystem.ComponentModel.DataAnnotations;

namespaceAspNetCoreTodo.Models

{

publicclassTodoItem

{

publicGuidId{get;set;}

publicboolIsDone{get;set;}

[Required]

publicstringTitle{get;set;}

publicDateTimeOffset?DueAt{get;set;}

}

}

Thisclassdefineswhatthedatabasewillneedtostoreforeachto-doitem:anID,atitleorname,whethertheitemiscomplete,andwhattheduedateis.Eachlinedefinesapropertyoftheclass:

Createmodels

24

TheIdpropertyisaguid,oragloballyuniqueidentifier.Guids(orGUIDs)arelongstringsoflettersandnumbers,like43ec09f2-7f70-4f4b-9559-65011d5781bb.Becauseguidsarerandomandareextremelyunlikelytobeaccidentallyduplicated,theyarecommonlyusedasuniqueIDs.Youcouldalsouseanumber(integer)asadatabaseentityID,butyou'dneedtoconfigureyourdatabasetoalwaysincrementthenumberwhennewrowsareaddedtothedatabase.Guidsaregeneratedrandomly,soyoudon'thavetoworryaboutauto-incrementing.

TheIsDonepropertyisaboolean(true/falsevalue).Bydefault,itwillbefalseforallnewitems.Lateryou'llusewritecodetoswitchthispropertytotruewhentheuserclicksanitem'scheckboxintheview.

TheTitlepropertyisastring(textvalue).Thiswillholdthenameordescriptionoftheto-doitem.The[Required]attributetellsASP.NETCorethatthisstringcan'tbenullorempty.

TheDueAtpropertyisaDateTimeOffset,whichisaC#typethatstoresadate/timestampalongwithatimezoneoffsetfromUTC.Storingthedate,time,andtimezoneoffsettogethermakesiteasytorenderdatesaccuratelyonsystemsindifferenttimezones.

Noticethe?questionmarkaftertheDateTimeOffsettype?ThatmarkstheDueAtpropertyasnullable,oroptional.Ifthe?wasn'tincluded,everyto-doitemwouldneedtohaveaduedate.TheIdandIsDonepropertiesaren'tmarkedasnullable,sotheyarerequiredandwillalwayshaveavalue(oradefaultvalue).

StringsinC#arealwaysnullable,sothere'snoneedtomarktheTitlepropertyasnullable.C#stringscanbenull,empty,orcontaintext.

Createmodels

25

Eachpropertyisfollowedbyget;set;,whichisashorthandwayofsayingthepropertyisread/write(or,moretechnically,ithasagetterandsettermethods).

Atthispoint,itdoesn'tmatterwhattheunderlyingdatabasetechnologyis.ItcouldbeSQLServer,MySQL,MongoDB,Redis,orsomethingmoreexotic.ThismodeldefineswhatthedatabaseroworentrywilllooklikeinC#soyoudon'thavetoworryaboutthelow-leveldatabasestuffinyourcode.Thissimplestyleofmodelissometimescalleda"plainoldC#object"orPOCO.

Theviewmodel

Often,themodel(entity)youstoreinthedatabaseissimilarbutnotexactlythesameasthemodelyouwanttouseinMVC(theviewmodel).Inthiscase,theTodoItemmodelrepresentsasingleiteminthedatabase,buttheviewmightneedtodisplaytwo,ten,orahundredto-doitems(dependingonhowbadlytheuserisprocrastinating).

Becauseofthis,theviewmodelshouldbeaseparateclassthatholdsanarrayofTodoItems:

Models/TodoViewModel.cs

namespaceAspNetCoreTodo.Models

{

publicclassTodoViewModel

{

publicTodoItem[]Items{get;set;}

}

}

Nowthatyouhavesomemodels,it'stimetocreateaviewthatwilltakeaTodoViewModelandrendertherightHTMLtoshowtheusertheirto-dolist.

Createmodels

26

Createmodels

27

CreateaviewViewsinASP.NETCorearebuiltusingtheRazortemplatinglanguage,whichcombinesHTMLandC#code.(Ifyou'vewrittenpagesusingHandlebarsmoustaches,ERBinRubyonRails,orThymeleafinJava,you'vealreadygotthebasicidea.)

MostviewcodeisjustHTML,withtheoccasionalC#statementaddedintopulldataoutoftheviewmodelandturnitintotextorHTML.TheC#statementsareprefixedwiththe@symbol.

TheviewrenderedbytheIndexactionoftheTodoControllerneedstotakethedataintheviewmodel(asequenceofto-doitems)anddisplayitinanicetablefortheuser.Byconvention,viewsareplacedintheViewsdirectory,inasubdirectorycorrespondingtothecontrollername.Thefilenameoftheviewisthenameoftheactionwitha.cshtmlextension.

CreateaTododirectoryinsidetheViewsdirectory,andaddthisfile:

Views/Todo/Index.cshtml

@modelTodoViewModel

@{

ViewData["Title"]="Manageyourtodolist";

}

<divclass="panelpanel-defaulttodo-panel">

<divclass="panel-heading">@ViewData["Title"]</div>

<tableclass="tabletable-hover">

<thead>

<tr>

<td>&#x2714;</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'llnoticesomesyntaxchanges.Insteadofusing@Html.ActionLink()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=="admin@todo.local")

.SingleOrDefaultAsync();

if(testAdmin!=null)return;

testAdmin=newApplicationUser

{

Authorizationwithroles

91

UserName="admin@todo.local",

Email="admin@todo.local"

};

awaituserManager.CreateAsync(

testAdmin,"NotSecure123!!");

awaituserManager.AddToRoleAsync(

testAdmin,Constants.AdministratorRole);

}

Ifthereisn'talreadyauserwiththeusernameadmin@todo.localinthedatabase,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="fake@example.com"

};

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://nate@mytodoapp.scm.azurewebsites.net/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:nate@barbettini.com.

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).Youcanalsoreachmeviaemailatnate@barbettini.com.

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

top related