using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Migrations;
using RankingApp.Data.Models;
using System;
using System.Collections.Generic;
using System.Text;
namespace RankingApp.Data
{
public class ApplicationDbContext : IdentityDbContext
{
//DbSet 으로 추가해줘야지 GameReesult.cs 에서 GameResultList 내용을 DB에 저장할 수 있다. ORM
//즉 서버를띄울때 자동으로 DB를 갱신하여 띄워주게 된다
//PM> add-migration RankingService 명령어르 NeGet 콘솔에서 실행하여 DB를 갱신해준다
//PM> update-database 명령어르 NeGet 콘솔에서 실행하여 DB를 갱신해준다
//주의!! GameResultList 이 테이블 명과 동일해야 한다 dbo.GameResultList 에서 dbo 빼고 GameResultList 만
public DbSet<GameResult> GameResultList { get; set; }
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
: base(options)
{
}
}
}
그런다음 패키지 관리자 콘솔에서 다음 처럼 입력한다음 다음 처럼 적용한다
PM> Add-Migration GameResultList
PM> Update-Database
이 처럼 마이그레이션이 변경된다
테이블명도 자동으로 바뀌게 된다
서비스 쪽에 갱신과 삭제에 대한 로직을 추가 하고 테이블에도 값이 삭제/갱신이 적용 될수 있게 처리 한다
public Task<bool> UpdateGameResult(GameResult gameResult)
{
var result = _context.GameResultList.Where(x => x.Id == gameResult.Id).FirstOrDefault();
if(result==null)
{
return Task.FromResult(false);
}
result.UserName = gameResult.UserName;
result.Score = gameResult.Score;
_context.SaveChanges();
return Task.FromResult(true);
}
public Task<bool> DeleteGameResult(GameResult gameResult)
{
var result = _context.GameResultList.Where(x => x.Id == gameResult.Id).FirstOrDefault();
if (result == null)
{
return Task.FromResult(false);
}
_context.GameResultList.Remove(gameResult);
_context.SaveChanges();
return Task.FromResult(true);
}
Ranking.razor
갱신과 삭제 버튼이 보여 내용을 수정 할수 있도록 하고
추가팝업과 갱신 팝업은 동일한 형태로 사용한다
그리고 로그인이 되어 있지 않다면 유저 정보는 보이지 않도록 Autorized, NotAutorized 처리를 해 놓았다
@page "/ranking"
@using RankingApp.Data.Models;
@using RankingApp.Data.Services;
@inject RankingService RankingService
<h3>Ranking</h3>
<AuthorizeView>
<Authorized>
@if (_gameResultList == null)
{
<p>Loading...</p>
}else{
<table class="table">
<thead>
<tr>
<th>UserName</th>
<th>Score</th>
<th>Date</th>
<th></th>
<th></th>
</tr>
</thead>
<tbody>
@foreach (var gameResult in _gameResultList)
{
<tr>
<td>@gameResult.UserName</td>
<td>@gameResult.Score</td>
<td>@gameResult.Date</td>
<td>
<button class="btn btn-primary" @onclick="() => EditGameResult(gameResult)">
Edit
</button>
</td>
<td>
<button class="btn btn-primary" @onclick="() => DeleteGameResult(gameResult)">
Delete
</button>
</td>
</tr>
}
</tbody>
</table>
<p>
<!-- make button to add gameresult -->
<button class="btn btn-primary" @onclick="AddGameResult">
Add
</button>
</p>
@if(_showPopup)
{
//dialog to show gameresult
<div class="modal" style="display:block" role="dialog">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header"></div>
<h3 class="modal-title">Add/Update Game Result</h3>
<button type="button" class="close" @onclick="ClosePopup">
<span aria-hidden="true">X</span>
</button>
<div class="modal-body">
<label for="UserName"> UserName</label>
<input class="form-control" type="text" placeholder="UserName" @bind-value="_gameResult.UserName" />
<label for="Score"> Score</label>
<input class="form-control" type="text" placeholder="Score" @bind-value="_gameResult.Score" />
<button class="btn-primary" @onclick="SaveGameResult">Save</button>
</div>
</div>
</div>
</div>
}
}
</Authorized>
<NotAuthorized>
인증 안됨(로그인 안됨)
</NotAuthorized>
</AuthorizeView>
@code {
List<GameResult> _gameResultList;
GameResult _gameResult;
bool _showPopup = false;
protected override async Task OnInitializedAsync()
{
//db 에서 데이터 긁어와서 _gameResults 에 넣어줌
await readGameResultList();
}
public async Task<List<GameResult>> readGameResultList()
{
//db 에서 데이터 긁어와서 _gameResultList 에 넣어줌
_gameResultList = await RankingService.GetGameResultAsync();
return _gameResultList;
}
public void AddGameResult()
{
_showPopup = true;
//Add new gameresult to
_gameResult = new GameResult()
{
Id = 0
};
}
//ClosePopup
void ClosePopup()
{
_showPopup = false;
}
void EditGameResult(GameResult gameResult)
{
_showPopup = true;
_gameResult = gameResult;
}
async Task DeleteGameResult(GameResult gameResult)
{
var result = RankingService.DeleteGameResult(gameResult);
await readGameResultList();
}
async Task SaveGameResult()
{
//새로 만드는 상태
if(_gameResult.Id==0)
{
//save to db
_gameResult.Date = DateTime.Now;
var result = RankingService.AddGameResult(_gameResult);
//close popup
ClosePopup();
}
else
{
//수정하고 있는 상태
var result = RankingService.UpdateGameResult(_gameResult);
}
await readGameResultList();
_showPopup = false;
}
}
@page "/ranking"
@using RankingApp.Data.Models;
@using RankingApp.Data.Services;
@inject RankingService RankingService
<h3>Ranking</h3>
<AuthorizeView>
<Authorized>
@if (_gameResultLIst == null)
{
<p>Loading...</p>
}else{
<table class="table">
<thead>
<tr>
<th>UserName</th>
<th>Score</th>
<th>Date</th>
</tr>
</thead>
<tbody>
@foreach (var gameResult in _gameResultLIst)
{
<tr>
<td>@gameResult.UserName</td>
<td>@gameResult.Score</td>
<td>@gameResult.Date</td>
</tr>
}
</tbody>
</table>
<p>
<!-- make button to add gameresult -->
<button class="btn btn-primary" @onclick="AddGameResult">
Add
</button>
</p>
@if(_showPopup)
{
//dialog to show gameresult
<div class="modal" style="display:block" role="dialog">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header"></div>
<h3 class="modal-title">Add/Update Game Result</h3>
<button type="button" class="close" @onclick="ClosePopup">
<span aria-hidden="true">X</span>
</button>
<div class="modal-body">
<label for="UserName"> UserName</label>
<input class="form-control" type="text" placeholder="UserName" @bind-value="_gameResult.UserName" />
<label for="Score"> Score</label>
<input class="form-control" type="text" placeholder="Score" @bind-value="_gameResult.Score" />
<button class="btn-primary" @onclick="SaveGameResult">Save</button>
</div>
</div>
</div>
</div>
}
}
</Authorized>
<NotAuthorized>
인증 안됨(로그인 안됨)
</NotAuthorized>
</AuthorizeView>
@code {
List<GameResult> _gameResultLIst;
GameResult _gameResult;
bool _showPopup = false;
protected override async Task OnInitializedAsync()
{
//db 에서 데이터 긁어와서 _gameResults 에 넣어줌
await readGameResults();
}
public async Task<List<GameResult>> readGameResults()
{
//db 에서 데이터 긁어와서 _gameResults 에 넣어줌
_gameResultLIst = await RankingService.GetGameResultAsync();
return _gameResultLIst;
}
public void AddGameResult()
{
_showPopup = true;
//Add new gameresult to
_gameResult = new GameResult()
{
Id = 0
};
}
//ClosePopup
void ClosePopup()
{
_showPopup = false;
}
async Task SaveGameResult()
{
//새로 만드는 상태
if(_gameResult.Id==0)
{
//save to db
_gameResult.Date = DateTime.Now;
var result = RankingService.AddGameResult(_gameResult);
//close popup
ClosePopup();
}
else
{
//수정하고 있는 상태
}
_gameResultLIst = await readGameResults();
}
}
RankingService.cs
using RankingApp.Data.Models;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace RankingApp.Data.Services
{
public class RankingService
{
ApplicationDbContext _context;
public RankingService(ApplicationDbContext context)
{
_context = context;
}
public Task<List<GameResult>> GetGameResultAsync()
{
//DB 에서 GameResult 테이블의 모든 데이터를 가져온다
List<GameResult> results = _context.GameResult.ToList();
return Task.FromResult(results);
}
public Task<GameResult> AddGameResult(GameResult gameResult)
{
_context.GameResult.Add(gameResult);
_context.SaveChanges(); //db 에 실제 저장이 됨
return Task.FromResult(gameResult);
}
}
}
AddGameResult() 함수가 추가 되었고
_context.GameResult.Add(gameResult);
_context.SaveChanges(); //db 에 실제 저장이 됨
이 코드가 실제 DB 에 저장하는 부분이 된다
이후 결과는 Task 로 리턴해주고 결과를 담을때는 FromResult 로 결과를 담아줘서 리턴하면 await 으로 결과를 기다 릴 수 있다
public async Task<List<GameResult>> readGameResults()
{
//db 에서 데이터 긁어와서 _gameResults 에 넣어줌
_gameResultLIst = await RankingService.GetGameResultAsync();
return _gameResultLIst;
}
PM> add-migration anyName 으로 DB를 마이그레이션 할때 DefaultConnection 에 잇는 Database=RankingDB
RankingDB 이 이름으로 DB 가 생성되게 된다
먼저 최종 작업한 화면은 다음과 같다
여기서 Entity = EntityFramework 는 ORM 같은 것이다
기본 Blazor Server 에서 Ranking 부분을 추가 한것이도 Fetch data 와는 다르게 데이터는 db 에서 읽어온다 그래서 미리 DB 에 데이터를 넣어 놓어 놓는 것으로 시작한다
오른쪽 상단 Register 를 눌러 가입을 하나 하면 dbo.AspNetUsers 에 추저가 하나 추가 된다
그런 다음 EmailConfirmed 를 수동으로 true 로 변경한다 (이 부분은 이메일 확인 부분이다)
유저는 기본으로 제공된느 db 를 사용한다
GameResult.cs 를 Modes 폴더를 하나 만들어추가한다(Services 로 마찬가지로 추가한다)
using System;
namespace RankingApp.Data.Models
{
public class GameResult
{
public int Id { get; set; }
public int UserID { get; set; }
public string UserName { get; set; }
public int Score { get; set; }
public DateTime Date { get; set; }
}
}
원래는 GameResult 테이블을 하나 추가해 다음 처럼 데이터를 추가해야 하지만
이렇게 하지 않고 Entity 를 활용해 자동으로 테이블을 등록하게한다 => db에 대한 버전 관리도 되고 .cs 와 코드적으로 자동 연동 됨으로 이것이 편하다
ApplicationDbContext.cs 가 db 와 .cs 같에 연결해주는 핵심적인 부분이다
먼저 IdentityDbContext 를 상속받는다
DbSet<GameResult> 이 부분이 DB와 코드의 연결 부분이다
IdentityDbContext: Base class for the Entity Framework database context used for identity.
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Migrations;
using RankingApp.Data.Models;
using System;
using System.Collections.Generic;
using System.Text;
namespace RankingApp.Data
{
public class ApplicationDbContext : IdentityDbContext
{
//DbSet 으로 추가해줘야지 GameReesult.cs 에서 GameResult 내용을 DB에 저장할 수 있다. ORM
//즉 서버를띄울때 자동으로 DB를 갱신하여 띄워주게 된다
//PM> add-migration RankingService 명령어르 NeGet 콘솔에서 실행하여 DB를 갱신해준다
//PM> update-database 명령어르 NeGet 콘솔에서 실행하여 DB를 갱신해준다
public DbSet<GameResult> GameResult { get; set; }
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
: base(options)
{
}
}
}
이렇게 추가해주고
Startup.cs 에서
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(
Configuration.GetConnectionString("DefaultConnection")));
이 부분을 통해 DB 로 연결하게 된다 (Identity 를 사용하기 때문에 이 부분이 자동으로 등록 되어 있긴 하지만 안되어 있으면 추가해준다)
using System;
using Microsoft.EntityFrameworkCore.Migrations;
namespace RankingApp.Data.Migrations
{
public partial class RankingService : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "GameResult",
columns: table => new
{
Id = table.Column<int>(nullable: false)
.Annotation("SqlServer:Identity", "1, 1"),
UserID = table.Column<int>(nullable: false),
UserName = table.Column<string>(nullable: true),
Score = table.Column<int>(nullable: false),
Date = table.Column<DateTime>(nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_GameResult", x => x.Id);
});
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "GameResult");
}
}
}
즉 GameResult 를 알아서 해석하여 테이블을 만드는 코드가 된다
Up 일때는 들어가고 Down 일때는 Drop 하게된다 (버전 관리 개념)
이렇게 하지 않으면 DB 에서 Field 를 변경하거나 코드에서 Field 명등 수정 사항이 안맞는 상황들이 발생 할수 있기 때문에 번거로운 작업이 계속 될 수 있는데 이것을 코드수준에서 DB로 맞춰만들어주기 때문에 작업의 효율을 높여준다
이에 더해 버전 관리 또한 가능해지게 된다
데이터는 우선 수동으로 다음처럼 추가한다
데이터를 읽어오는 코드를 먼저 작업하자
그러기 위해서 ApplicationDbContext.cs 에서 다음 처럼
DbSet 을 통해서 DB 와 연결된 부분을 읽어와야 하는데 이미 위에서 다음과 같은 코드를 통해 연결을 해놨으니
public DbSet<GameResult> GameResult { get; set; }
RankingService.cs 서비스를 만들어 데이터를 가져올 수 있는 코드를 만들어 준면 된다
using RankingApp.Data.Models;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace RankingApp.Data.Services
{
public class RankingService
{
ApplicationDbContext _context;
public RankingService(ApplicationDbContext context)
{
_context = context;
}
public Task<List<GameResult>> GetGameResultAsync()
{
//DB 에서 GameResult 테이블의 모든 데이터를 가져온다
List<GameResult> results = _context.GameResult.ToList();
return Task.FromResult(results);
}
}
}
이 코드를 통해 db 에 있는 GameResult 테이블 내용을 읽어올 수 있게 된다
그리고 RankingService 를 사용하기 위해서 Startup.cs 에 AddScoped 로 RankigService 를 추가해준다
public class SingletonService : IDisposable
{
public Guid ID {get; set;} //간단하게 ID를 생성하는 인터페이스이다.
public SingletonService() //cotr + tab,tab으로 해당 class의 생성자를 간단하게 생성할 수 있다.
{
ID = Guid.NewGuid();
}
public Dispose()
{
Console.WriteLine("SingletonService Disposed!");
}
}
public class TransientService : IDisposable
{
public Guid ID {get; set;} //간단하게 ID를 생성하는 인터페이스이다.
public TransientService() //cotr + tab,tab으로 해당 class의 생성자를 간단하게 생성할 수 있다.
{
ID = Guid.NewGuid();
}
public Dispose()
{
Console.WriteLine("TransientService Disposed!");
}
}
public class ScopeService : IDisposable
{
public Guid ID {get; set;} //간단하게 ID를 생성하는 인터페이스이다.
public ScopeService() //cotr + tab,tab으로 해당 class의 생성자를 간단하게 생성할 수 있다.
{
ID = Guid.NewGuid();
}
public Dispose()
{
Console.WriteLine("ScopeService Disposed!");
}
}
using HelloEmpty.Models;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using System.Collections.Generic;
namespace HelloEmpty.controllers
{
[Route("api/[controller]")]
[ApiController]
public class ValuesController : ControllerBase
{
[HttpGet]
public List<HelloMessage> Get()
{
List<HelloMessage> message = new List<HelloMessage>();
message.Add(new HelloMessage() { Message = "Hello Web API 1!" });
message.Add(new HelloMessage() { Message = "Hello Web API 2!" });
message.Add(new HelloMessage() { Message = "Hello Web API 3!" });
return message;
}
}
}
get() 이 클라에서 서버로 api/Values 경로로 api 를 쏘면
서버에서 보낼 내용을 message 에 담아 브라우저에 보낸다
HelloMessage
namespace HelloEmpty.Models
{
public class HelloMessage
{
public string Message { get; set; }
}
}
실제 데이터는 Models 에 들어있게 된다
program 부분인데 이 부분은 디폴트이다
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace HelloEmpty
{
public class Program
{
public static void Main(string[] args)
{
CreateHostBuilder(args).Build().Run();
}
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();
});
}
}
Startup.cs
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace HelloEmpty
{
public class Startup
{
// This method gets called by the runtime. Use this method to add services to the container.
// For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940
public void ConfigureServices(IServiceCollection services)
{
//mvc 를 사용하겠다는 것
//services.AddControllersWithViews();
services.AddControllers();
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseRouting();
//홈페이지 실행할때 어떤 주소로 어떤 부분을 연동하여 실행할지에 대한 부분
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
//endpoints.MapRazorPages();
//endpoints.MapControllerRoute(name: "default", pattern: "{controller=Home}/{action=Index}/{id?}");
/*
endpoints.MapGet("/", async context =>
{
await context.Response.WriteAsync("Hello World!");
});
*/
});
}
}
}
우리는 웹 어플리케이션을 개발할 때, 보통 java, .NET, Node.js 등의 framework 또는 language를 사용하여 server-side project를 만든다. 그리고 Angular, React, Vue.js와 같은 javascript framework를 사용하여 브라우저에 데이터를 보여준다.
하지만, 우리는 server-side application에서 사용한 언어를 client-side에는 사용할 수가 없다. server-side와 client-side는 기본적으로 단절되어있다.
우리가 만약, server language를 client에도 재사용한다면 좋지 않겠는가? 우리는 WebAssembly를 사용하여 그렇게 할 수 있다.
Blazor는 무엇이고, 어떻게 사용해야 하는가?
Blazor는 C#을 사용하여 interactive web application을 만들 수 있는 오픈소스 framework이다. 우리는 Blazor에서 server, client code에 모두 C#을 사용할 수 있다. .NET runtime은 브라우저에서 우리가 생성한 .NET DLL을 실행할 수 있다.
추가적인 plugin없이 주요 브라우저에서 실행이 가능하다. .NET standard와 호환하는 library를 사용할 수 있다. (NuGet package 포함)
JavaScript와의 연결이 되어 있으므로, web sockets, file API, DOM의 브라우저 기능을 사용할 수 있다.
Blazor application에서 JavaScript를 호출할 수 있고, 그 반대도 가능하다.
Blazor Application의 Different Models
Blazor Server
Blazor Server는 ASP.NET Core application의 server측에서만 동작하고, SignalR을 사용하여 HTML과 통신한다. blazor.server.js라는 파일을 통해 가능하다.
server-side application은 메모리에 남아있기 때문에, 사용자당 instance가 생성된다.
장점은, Blazor WebAssembly에 비해 다운로드 사이즈가 작다. older browser에서도 디버깅이 가능하다.
단점은, 서버와 연결되어 있어야 하기 때문에 offline support는 없다. 확장성도 리스크가 될 수 있지만, 동시접속 천단위까지 제공이 가능하다. 마지막으로, serverless deployment는 불가능하며, ASP.NET Core server가 반드시 필요하다.
Blazor WebAssembly
WebAssembly의 도움으로 브라우저 client에서 실행된다. application이 실행되면 browser로 모든 파일을 다운받는다. (HTML, CSS, JavaScript files, .NET Standard DLL files)
offline mode에서 동작한다. server가 필요하지 않다. static files로 구성되어 있기 때문에, CDN, Server, Azure Storage에 배포가 가능하다.
장점은, native browser application의 속도와 비슷한 정도로 빠르다. offline mode에서 동작이 가능하다. serverless deployment가 가능하다. browser로 file만 받으면, server는 필요하지 않다. 추가 plugin 없이, modern browser에서 동작이 가능하다. 배포가 쉽다.
브라우저에서만 실행되기 때문에, 브라우저 capability에 의해 성능이 제한된다.
단점은 최초 로딩시간이 길다. older browser에서는 동작하지 않는다. older browser에서 동작해야 한다면 Blazor server-side application이 더 나은 선택이 되겠다.