[C#/ASP.NET MVC/.NETCORE] Microsoft.Extensions.Caching.Memory 누겟 설치하기
■ Microsoft.Extensions.Caching.Memory 누겟을 설치하는 방법을 보여준다. 1. Visual Studio를 실행한다. 2. [도구] / [NuGet 패키지 관리자] / [패키지 관리자 콘솔] 메뉴를 실행한다.
■ Microsoft.Extensions.Caching.Memory 누겟을 설치하는 방법을 보여준다. 1. Visual Studio를 실행한다. 2. [도구] / [NuGet 패키지 관리자] / [패키지 관리자 콘솔] 메뉴를 실행한다.
■ Microsoft.AspNetCore.Session 누겟을 설치하는 방법을 보여준다. 1. Visual Studio를 실행한다. 2. [도구] / [NuGet 패키지 관리자] / [패키지 관리자 콘솔] 메뉴를 실행한다.
■ Kestrel 사용시 업로드 파일 크기를 설정하는 방법을 보여준다. (2) 1. Program.cs 파일에서 아래와 같이 코드를 추가한다. ▶ Program.cs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 |
using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Hosting; namespace TestProject { /// <summary> /// 프로그램 /// </summary> public class Program { //////////////////////////////////////////////////////////////////////////////////////////////////// Method ////////////////////////////////////////////////////////////////////////////////////////// Static //////////////////////////////////////////////////////////////////////////////// Public #region 프로그램 시작하기 - Main(argumentArray) /// <summary> /// 프로그램 시작하기 /// </summary> /// <param name="argumentArray">인자 배열</param> public static void Main(string[] argumentArray) { CreateHostBuilder(argumentArray).Build().Run(); } #endregion #region 호스트 빌더 생성하기 - CreateHostBuilder(argumentArray) /// <summary> /// 호스트 빌더 생성하기 /// </summary> /// <param name="argumentArray">인자 배열</param> /// <returns>호스트 빌더</returns> public static IHostBuilder CreateHostBuilder(string[] argumentArray) => Host.CreateDefaultBuilder(argumentArray) .ConfigureWebHostDefaults ( builder => { builder.ConfigureKestrel ( (context, options) => { options.Limits.MaxRequestBodySize = 209715200L; } ) .UseStartup<Startup>(); } ); #endregion } } |
2. Startup.cs
■ Kestrel 사용시 업로드 파일 크기를 설정하는 방법을 보여준다. (1) 파일 업로드를 처리하는 컨트롤러의 액션 메소드에 RequestFormLimits/RequestSizeLimit 어트리뷰트를 아래와 같이 추가한다. ▶
■ IIS Express 사용시 업로드 파일 크기를 설정하는 방법을 보여준다. 1. web.config 파일에서 아래와 같이 코드를 추가한다. ▶ web.config
1 2 3 4 5 6 7 8 9 10 11 12 |
<?xml version="1.0" encoding="utf-8"?> <configuration> <system.webServer> <security> <requestFiltering> <requestLimits maxAllowedContentLength="209715200" /> </requestFiltering> </security> </system.webServer> </configuration> |
2. 파일
■ 파일 업로드 액션 메소드에서 파일 정보를 전달받는 매개 변수의 데이터 타입을 설정하는 방법을 보여준다. Upload.cshtml 파일에서 파일 업로드를 위해 form 태그
■ JWT(Json Web Token) 인증을 사용하는 방법을 보여준다. [TestLibrary 프로젝트] ▶ Models/UserModel.cs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
namespace TestLibrary.Models { /// <summary> /// 사용자 /// </summary> public class UserModel { //////////////////////////////////////////////////////////////////////////////////////////////////// Property ////////////////////////////////////////////////////////////////////////////////////////// Public #region ID - ID /// <summary> /// ID /// </summary> public int ID { get; set; } #endregion #region 사용자명 - UserName /// <summary> /// 사용자명 /// </summary> public string UserName { get; set; } #endregion #region 패스워드 - Password /// <summary> /// 패스워드 /// </summary> public string Password { get; set; } #endregion } } |
▶ Models/TestModel.cs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
namespace TestLibrary.Models { /// <summary> /// 테스트 모델 /// </summary> public class TestModel { //////////////////////////////////////////////////////////////////////////////////////////////////// Property ////////////////////////////////////////////////////////////////////////////////////////// Public #region 값 - Value /// <summary> /// 값 /// </summary> public string Value { get; set; } #endregion } } |
[TestClient 프로젝트] ▶ Program.cs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 |
using System; using System.Net; using System.Net.Http; using System.Net.Http.Headers; using System.Threading.Tasks; using TestLibrary.Models; namespace TestClient { /// <summary> /// 프로그램 /// </summary> class Program { //////////////////////////////////////////////////////////////////////////////////////////////////// Method ////////////////////////////////////////////////////////////////////////////////////////// Static //////////////////////////////////////////////////////////////////////////////// Public #region 프로그램 시작하기 - Main() /// <summary> /// 프로그램 시작하기 /// </summary> static void Main() { UserModel user = new UserModel { UserName = "admin", Password = "1234"}; string token = Login("http://localhost:4035/api/authentication/login", user).GetAwaiter().GetResult(); Console.WriteLine($"JWT 토큰 : {token}"); Console.WriteLine("--------------------------------------------------"); TestModel test = new TestModel { Value = "테스트" }; string result = Test("http://localhost:4035/api/authentication/test", token, test).GetAwaiter().GetResult(); Console.WriteLine($"결과 : {result}"); Console.WriteLine("--------------------------------------------------"); Console.WriteLine("아무 키나 눌러 주시기 바랍니다."); Console.ReadKey(true); } #endregion #region HTTP 클라이언트 구하기 - GetHTTPClient(useCookie, baseAddress, tokenShema, token) /// <summary> /// HTTP 클라이언트 구하기 /// </summary> /// <param name="useCookie">쿠키 사용 여부</param> /// <param name="baseAddress">기본 주소</param> /// <param name="tokenShema">토큰 스키마</param> /// <param name="token">토큰</param> /// <returns>HTTP 클라이언트</returns> private static HttpClient GetHTTPClient(bool useCookie, string baseAddress, string tokenShema, string token) { HttpClient client; if(useCookie) { HttpClientHandler httpClientHandler = new HttpClientHandler(); httpClientHandler.UseCookies = true; httpClientHandler.CookieContainer = new CookieContainer(); client = new HttpClient(httpClientHandler); } else { client = new HttpClient(); } client.DefaultRequestHeaders.Accept.Clear(); client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); if(!string.IsNullOrWhiteSpace(baseAddress)) { client.BaseAddress = new Uri(baseAddress); } if(!string.IsNullOrWhiteSpace(tokenShema)) { client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(tokenShema, token); } return client; } #endregion #region 로그인하기 - Login(url, user) /// <summary> /// 로그인하기 /// </summary> /// <param name="url">URL</param> /// <param name="user">사용자</param> /// <returns>JWT 토큰</returns> private static async Task<string> Login(string url, UserModel user) { using(HttpClient client = GetHTTPClient(false, url, null, null)) { var response = await client.PostAsJsonAsync<UserModel>(url, user); if(response.IsSuccessStatusCode) { string token = await response.Content.ReadAsAsync<string>(); return token; } return null; } } #endregion #region 테스트하기 - Test(url, token, test) /// <summary> /// 테스트하기 /// </summary> /// <param name="url">URL</param> /// <param name="token">JWT 토큰</param> /// <param name="test">테스트</param> /// <returns>테스트 결과</returns> private static async Task<string> Test(string url, string token, TestModel test) { using(HttpClient client = GetHTTPClient(false, url, "Bearer", token)) { var response = await client.PostAsJsonAsync<TestModel>(url, test); if(response.IsSuccessStatusCode) { string result = await response.Content.ReadAsAsync<string>(); return result; } else // http://로 호출한 경우 https://로 다시 호출한다. { string finalRequestURL = response.RequestMessage.RequestUri.AbsoluteUri; if(finalRequestURL != url) { response = await client.PostAsJsonAsync<TestModel>(finalRequestURL, test); if(response.IsSuccessStatusCode) { string result = await response.Content.ReadAsAsync<string>(); return result; } else { return null; } } else { return null; } } } } #endregion } } |
[TestServer
■ 쿠키(Cookie) 인증을 사용하는 방법을 보여준다. ▶ Database/TestDB.sql
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 |
CREATE TABLE dbo.[User] ( ID INT NOT NULL IDENTITY(1, 1) PRIMARY KEY ,UserID NVARCHAR(50) NOT NULL ,[Password] NVARCHAR(50) NOT NULL ) GO CREATE PROCEDURE dbo.WriteUser @UserID NVarChar(50), @Password NVarChar(50) AS INSERT INTO dbo.[User] VALUES (@UserID, @Password); GO CREATE PROCEDURE dbo.ListUsers AS SELECT ID ,UserID ,[Password] FROM dbo.[User] ORDER BY ID DESC; GO CREATE PROCEDURE dbo.ViewUser @ID Int AS SELECT ID ,UserID ,[Password] FROM dbo.[User] Where ID = @ID; GO CREATE PROCEDURE dbo.ModifyUser @UserID NVARCHAR(50) ,@Password NVARCHAR(50) ,@ID INT AS UPDATE dbo.[User] SET UserID = @UserID, [Password] = @Password WHERE ID = @ID; GO CREATE PROCEDURE dbo.DeleteUser @ID INT AS DELETE FROM dbo.[User] WHERE ID = @ID; GO CREATE PROCEDURE dbo.SearchUsers @SearchField NVarChar(50), @SearchQuery NVarChar(50) AS DECLARE @SQL NVarChar(2000); SET @SQL = 'SELECT * FROM dbo.[User] WHERE ' + @SearchField + ' LIKE ''%' + @SearchQuery + '%'''; EXECUTE @SQL; GO |
▶ appsettings.json
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
{ "ConnectionStrings" : { "DefaultConnection" : "Data Source=(localdb)\\MSSQLLocalDB;Initial Catalog=TestDB;Integrated Security=True" }, "Logging" : { "LogLevel" : { "Default" : "Information", "Microsoft" : "Warning", "Microsoft.Hosting.Lifetime" : "Information" } }, "AllowedHosts" : "*", "MainSettings" : { "SiteAdministrator" : "admin" } } |
▶ Settings/MainSettings.cs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
namespace TestProject.Settings { /// <summary> /// 메인 설정 /// </summary> public class MainSettings { //////////////////////////////////////////////////////////////////////////////////////////////////// Property ////////////////////////////////////////////////////////////////////////////////////////// Public #region 사이트 관리자 - SiteAdministrator /// <summary> /// 사이트 관리자 /// </summary> public string SiteAdministrator { get; set; } #endregion } } |
▶ Models/UserModel.cs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 |
using System.ComponentModel.DataAnnotations; namespace TestProject.Models { /// <summary> /// 사용자 모델 /// </summary> public class UserModel { //////////////////////////////////////////////////////////////////////////////////////////////////// Property ////////////////////////////////////////////////////////////////////////////////////////// Public #region ID - ID /// <summary> /// ID /// </summary> public int ID { get; set; } #endregion #region 사용자 ID - UserID /// <summary> /// 사용자 ID /// </summary> [Display(Name = "사용자 ID")] [Required(ErrorMessage = "사용자 ID를 입력해 주시기 바랍니다.")] [StringLength(50, MinimumLength = 5, ErrorMessage = "사용자 ID는 5자 이상 50자 이하로 입력해 주시기 바랍니다.")] public string UserID { get; set; } #endregion #region 패스워드 - Password /// <summary> /// 패스워드 /// </summary> [Display(Name = "패스워드")] [Required(ErrorMessage = "패스워드를 입력해 주시기 바랍니다.")] [StringLength(50, MinimumLength = 4, ErrorMessage = "패스워드는 4자 이상 50자 이하로 입력해 주시기 바랍니다.")] public string Password { get; set; } #endregion } } |
▶ Models/IUserRepository.cs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 |
namespace TestProject.Models { /// <summary> /// 사용자 저장소 /// </summary> public interface IUserRepository { //////////////////////////////////////////////////////////////////////////////////////////////////// Method ////////////////////////////////////////////////////////////////////////////////////////// Public #region 사용자 추가하기 - AddUser(userID, password) /// <summary> /// 사용자 추가하기 /// </summary> /// <param name="userID">사용자 ID</param> /// <param name="password">패스워드</param> void AddUser(string userID, string password); #endregion #region 사용자 수정하기 - UpdateUser(id, userID, password) /// <summary> /// 사용자 수정하기 /// </summary> /// <param name="id">ID</param> /// <param name="userID">사용자 ID</param> /// <param name="password">패스워드</param> void UpdateUser(int id, string userID, string password); #endregion #region 사용자 구하기 - GetUser(userID) /// <summary> /// 사용자 구하기 /// </summary> /// <param name="userID">사용자 ID</param> /// <returns>사용자</returns> UserModel GetUser(string userID); #endregion #region 사용자 검증하기 - ValidateUser(userID, password) /// <summary> /// 사용자 검증하기 /// </summary> /// <param name="userID">사용자 ID</param> /// <param name="password">패스워드</param> /// <returns>사용자 검증 여부</returns> bool ValidateUser(string userID, string password); #endregion } } |
▶
■ Microsoft.AspNetCore.Authentication.Cookies 누겟을 설치하는 방법을 보여준다. 1. Visual Studio를 실행한다. 2. [도구] / [NuGet 패키지 관리자] / [패키지 관리자 콘솔] 메뉴를 실행한다.
■ RequestFormLimitsAttribute 클래스를 사용해 폼에서 특정 제한을 설정하는 방법을 보여준다. ▶ Startup.cs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 |
using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.FileProviders; using Microsoft.Extensions.Hosting; using TestProject.Data; using TestProject.Filters; namespace TestProject { /// <summary> /// 시작 /// </summary> public class Startup { //////////////////////////////////////////////////////////////////////////////////////////////////// Property ////////////////////////////////////////////////////////////////////////////////////////// Public #region 구성 - Configuration /// <summary> /// 구성 /// </summary> public IConfiguration Configuration { get; } #endregion //////////////////////////////////////////////////////////////////////////////////////////////////// Constructor ////////////////////////////////////////////////////////////////////////////////////////// Public #region 생성자 - Startup(configuration) /// <summary> /// 생성자 /// </summary> /// <param name="configuration">구성</param> public Startup(IConfiguration configuration) { Configuration = configuration; } #endregion //////////////////////////////////////////////////////////////////////////////////////////////////// Method ////////////////////////////////////////////////////////////////////////////////////////// Public #region 서비스 컬렉션 구성하기 - ConfigureServices(services) /// <summary> /// 서비스 컬렉션 구성하기 /// </summary> /// <param name="services">서비스 컬렉션</param> public void ConfigureServices(IServiceCollection services) { services.AddControllers(); services.AddRazorPages ( options => { options.Conventions.AddPageApplicationModelConvention ( "/UploadStreamOneFileToDatabase", model => { model.Filters.Add(new GenerateAntiforgeryTokenCookieAttribute()); model.Filters.Add(new DisableFormValueModelBindingAttribute()); } ); options.Conventions.AddPageApplicationModelConvention ( "/UploadStreamOneFileToPhysicalStorage", model => { model.Filters.Add(new GenerateAntiforgeryTokenCookieAttribute()); model.Filters.Add(new DisableFormValueModelBindingAttribute()); model.Filters.Add(new RequestFormLimitsAttribute() { MultipartBodyLengthLimit = 1073741824L }); } ); } ); ... } #endregion #region 구성하기 - Configure(app, environment) /// <summary> /// 구성하기 /// </summary> /// <param name="app">애플리케이션 빌더</param> /// <param name="environment">웹 호스트 환경</param> public void Configure(IApplicationBuilder app, IWebHostEnvironment environment) { ... } #endregion } } |
■ web.config 파일에서 최대 허용 컨텐트 길이를 설정하는 방법을 보여준다. ▶ web.config
1 2 3 4 5 6 7 8 9 10 11 12 |
<?xml version="1.0" encoding="utf-8"?> <configuration> <system.webServer> <security> <requestFiltering> <requestLimits maxAllowedContentLength="1073741824" /> </requestFiltering> </security> </system.webServer> </configuration> |
■ KestrelServerOptions 클래스의 Limits 속성을 사용해 최대 요청 BODY 크기를 설정하는 방법을 보여준다. ▶ Program.cs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 |
using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Hosting; namespace TestProject { /// <summary> /// 프로그램 /// </summary> public class Program { //////////////////////////////////////////////////////////////////////////////////////////////////// Method ////////////////////////////////////////////////////////////////////////////////////////// Static //////////////////////////////////////////////////////////////////////////////// Private #region 프로그램 시작하기 - Main(argumentArray) /// <summary> /// 프로그램 시작하기 /// </summary> /// <param name="argumentArray">인자 배열</param> public static void Main(string[] argumentArray) { CreateHostBuilder(argumentArray).Build().Run(); } #endregion #region 호스트 빌더 생성하기 - CreateHostBuilder(argumentArray) /// <summary> /// 호스트 빌더 생성하기 /// </summary> /// <param name="argumentArray">인자 배열</param> /// <returns>호스트 빌더</returns> public static IHostBuilder CreateHostBuilder(string[] argumentArray) => Host.CreateDefaultBuilder(argumentArray) .ConfigureWebHostDefaults ( builder => { builder.ConfigureKestrel ( (context, options) => { options.Limits.MaxRequestBodySize = 1073741824L; } ) .UseStartup<Startup>(); } ); #endregion } } |
■ IISServerOptions 클래스의 MaxRequestBodySize 속성을 사용해 최대 요청 BODY 크기를 설정하는 방법을 보여준다. ▶ Startup.cs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 |
using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.FileProviders; using Microsoft.Extensions.Hosting; using TestProject.Data; using TestProject.Filters; namespace TestProject { /// <summary> /// 시작 /// </summary> public class Startup { //////////////////////////////////////////////////////////////////////////////////////////////////// Property ////////////////////////////////////////////////////////////////////////////////////////// Public #region 구성 - Configuration /// <summary> /// 구성 /// </summary> public IConfiguration Configuration { get; } #endregion //////////////////////////////////////////////////////////////////////////////////////////////////// Constructor ////////////////////////////////////////////////////////////////////////////////////////// Public #region 생성자 - Startup(configuration) /// <summary> /// 생성자 /// </summary> /// <param name="configuration">구성</param> public Startup(IConfiguration configuration) { Configuration = configuration; } #endregion //////////////////////////////////////////////////////////////////////////////////////////////////// Method ////////////////////////////////////////////////////////////////////////////////////////// Public #region 서비스 컬렉션 구성하기 - ConfigureServices(services) /// <summary> /// 서비스 컬렉션 구성하기 /// </summary> /// <param name="services">서비스 컬렉션</param> public void ConfigureServices(IServiceCollection services) { services.AddControllers(); ... services.Configure<IISServerOptions>(option => { option.MaxRequestBodySize = 1073741824L; }); ... } #endregion #region 구성하기 - Configure(app, environment) /// <summary> /// 구성하기 /// </summary> /// <param name="app">애플리케이션 빌더</param> /// <param name="environment">웹 호스트 환경</param> public void Configure(IApplicationBuilder app, IWebHostEnvironment environment) { ... } #endregion } } |
■ FormOptions 클래스의 MultipartBodyLengthLimit 속성을 사용해 멀티 파트 BODY 길이 제한을 설정하는 방법을 보여준다. ▶ Startup.cs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 |
using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.FileProviders; using Microsoft.Extensions.Hosting; using TestProject.Data; using TestProject.Filters; namespace TestProject { /// <summary> /// 시작 /// </summary> public class Startup { //////////////////////////////////////////////////////////////////////////////////////////////////// Property ////////////////////////////////////////////////////////////////////////////////////////// Public #region 구성 - Configuration /// <summary> /// 구성 /// </summary> public IConfiguration Configuration { get; } #endregion //////////////////////////////////////////////////////////////////////////////////////////////////// Constructor ////////////////////////////////////////////////////////////////////////////////////////// Public #region 생성자 - Startup(configuration) /// <summary> /// 생성자 /// </summary> /// <param name="configuration">구성</param> public Startup(IConfiguration configuration) { Configuration = configuration; } #endregion //////////////////////////////////////////////////////////////////////////////////////////////////// Method ////////////////////////////////////////////////////////////////////////////////////////// Public #region 서비스 컬렉션 구성하기 - ConfigureServices(services) /// <summary> /// 서비스 컬렉션 구성하기 /// </summary> /// <param name="services">서비스 컬렉션</param> public void ConfigureServices(IServiceCollection services) { services.AddControllers(); ... services.Configure<FormOptions>(options => { options.MultipartBodyLengthLimit = 1073741824L; }); ... } #endregion #region 구성하기 - Configure(app, environment) /// <summary> /// 구성하기 /// </summary> /// <param name="app">애플리케이션 빌더</param> /// <param name="environment">웹 호스트 환경</param> public void Configure(IApplicationBuilder app, IWebHostEnvironment environment) { ... } #endregion } } |
■ 대용량 파일을 업로드하는 방법을 보여준다. ▶ Utilities/FileHelper.cs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 |
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.AspNetCore.WebUtilities; using Microsoft.Net.Http.Headers; using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.IO; using System.Linq; using System.Net; using System.Reflection; using System.Threading.Tasks; namespace TestProject.Utilities { /// <summary> /// 파일 헬퍼 /// </summary> public static class FileHelper { //////////////////////////////////////////////////////////////////////////////////////////////////// Field ////////////////////////////////////////////////////////////////////////////////////////// Static //////////////////////////////////////////////////////////////////////////////// Private #region Field /// <summary> /// 허용 문자 바이트 배열 /// </summary> /// <remarks> /// IsValidFileExtensionAndSignature 메서드의 특정 문자를 확인해야하는 경우 /// _allowedCharacterByteArray 필드에 문자를 제공합니다. /// </remarks> private static readonly byte[] _allowCharacterByteArray = { }; /// <summary> /// 파일 시그니처 딕셔너리 /// </summary> /// <remarks> /// 더 많은 파일 서명은 파일 서명 데이터베이스 (https://www.filesignatures.net/) 및 추가하려는 파일 형식에 대한 공식 사양을 참조한다. /// </remarks> private static readonly Dictionary<string, List<byte[]>> _fileSignatureDictionary = new Dictionary<string, List<byte[]>> { { ".gif", new List<byte[]> { new byte[] { 0x47, 0x49, 0x46, 0x38 } } }, { ".png", new List<byte[]> { new byte[] { 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A } } }, { ".jpeg", new List<byte[]> { new byte[] { 0xFF, 0xD8, 0xFF, 0xE0 }, new byte[] { 0xFF, 0xD8, 0xFF, 0xE2 }, new byte[] { 0xFF, 0xD8, 0xFF, 0xE3 } } }, { ".jpg", new List<byte[]> { new byte[] { 0xFF, 0xD8, 0xFF, 0xE0 }, new byte[] { 0xFF, 0xD8, 0xFF, 0xE1 }, new byte[] { 0xFF, 0xD8, 0xFF, 0xE8 } } }, { ".zip", new List<byte[]> { new byte[] { 0x50, 0x4B, 0x03, 0x04 }, new byte[] { 0x50, 0x4B, 0x4C, 0x49, 0x54, 0x45 }, new byte[] { 0x50, 0x4B, 0x53, 0x70, 0x58 }, new byte[] { 0x50, 0x4B, 0x05, 0x06 }, new byte[] { 0x50, 0x4B, 0x07, 0x08 }, new byte[] { 0x57, 0x69, 0x6E, 0x5A, 0x69, 0x70 } } } }; #endregion //////////////////////////////////////////////////////////////////////////////////////////////////// Method ////////////////////////////////////////////////////////////////////////////////////////// Static //////////////////////////////////////////////////////////////////////////////// Public // 경고! // 다음 파일 처리 방법에서는 파일의 내용이 검사되지 않는다. // 대부분의 프로덕션 시나리오에서 바이러스 백신/멀웨어 방지 스캐너 API는 // 사용자나 다른 시스템에서 파일을 사용할 수 있도록 하기 전에 파일에 사용된다. // 자세한 정보는 이 샘플 앱과 함께 제공되는 주제를 참조한다. #region 파일에서 처리하기 - ProcessFormFile<T>(formFile, modelStateDictionary, sourceFileExtensionArray, fileSizeLimit) /// <summary> /// 파일에서 처리하기 /// </summary> /// <typeparam name="T">타입</typeparam> /// <param name="formFile">폼 파일</param> /// <param name="modelStateDictionary">모델 상태 딕셔너리</param> /// <param name="sourceFileExtensionArray">소스 파일 확장자 배열</param> /// <param name="fileSizeLimit">파일 크기 제한</param> /// <returns>바이트 배열 태스크</returns> public static async Task<byte[]> ProcessFormFile<T> ( IFormFile formFile, ModelStateDictionary modelStateDictionary, string[] sourceFileExtensionArray, long fileSizeLimit ) { string fieldDisplayName = string.Empty; // 리플렉션을 사용하여 이 IFormFile과 연결된 모델 속성의 표시 이름을 가져온다. // 표시 이름을 찾을 수 없는 경우 오류 메시지에 표시 이름이 표시되지 않는다. MemberInfo propertyMemberInfo = typeof(T).GetProperty ( formFile.Name.Substring ( formFile.Name.IndexOf(".", StringComparison.Ordinal) + 1 ) ); if(propertyMemberInfo != null) { if(propertyMemberInfo.GetCustomAttribute(typeof(DisplayAttribute)) is DisplayAttribute displayAttribute) { fieldDisplayName = $"{displayAttribute.Name} "; } } // 클라이언트가 보낸 파일 이름을 신뢰하지 않는다. // 파일 이름을 표시하려면 값을 HTML로 인코딩한다. string trustedFileNameForDisplay = WebUtility.HtmlEncode(formFile.FileName); // 파일 길이를 확인한다. // 이 검사는 내용으로 BOM만 있는 파일을 포착하지 않는다. if(formFile.Length == 0) { modelStateDictionary.AddModelError ( formFile.Name, $"{fieldDisplayName}({trustedFileNameForDisplay}) is empty." ); return new byte[0]; } if(formFile.Length > fileSizeLimit) { long fileSizeLimitMB = fileSizeLimit / 1048576; modelStateDictionary.AddModelError ( formFile.Name, $"{fieldDisplayName}({trustedFileNameForDisplay}) exceeds {fileSizeLimitMB:N1} MB." ); return new byte[0]; } try { using(MemoryStream memoryStream = new MemoryStream()) { await formFile.CopyToAsync(memoryStream); // 파일의 유일한 내용이 BOM이었고 // BOM을 제거한 후 내용이 실제로 비어있는 경우 내용 길이를 확인한다. if(memoryStream.Length == 0) { modelStateDictionary.AddModelError ( formFile.Name, $"{fieldDisplayName}({trustedFileNameForDisplay}) is empty." ); } if(!IsValidFileExtensionAndSignature(formFile.FileName, memoryStream, sourceFileExtensionArray)) { modelStateDictionary.AddModelError ( formFile.Name, $"{fieldDisplayName}({trustedFileNameForDisplay}) file type isn't permitted or " + "the file's signature doesn't match the file's extension." ); } else { return memoryStream.ToArray(); } } } catch(Exception exception) { modelStateDictionary.AddModelError ( formFile.Name, $"{fieldDisplayName}({trustedFileNameForDisplay}) upload failed. " + "Please contact the Help Desk for support. Error : {exception.HResult}" ); } return new byte[0]; } #endregion #region 스트림 파일 처리하기 - ProcessStreamedFile(multipartSection, contentDispositionHeaderValue, modelStateDictionary, sourceFileExtensionArray, fileSizeLimit) /// <summary> /// 스트림 파일 처리하기 /// </summary> /// <param name="multipartSection">멀티 파트 섹션</param> /// <param name="contentDispositionHeaderValue">컨텐트 배치 헤더 값</param> /// <param name="modelStateDictionary">모델 상태 딕셔너리</param> /// <param name="sourceFileExtensionArray">소스 파일 확장자 배열</param> /// <param name="fileSizeLimit">파일 크기 제한</param> /// <returns>바이트 배열 태스크</returns> public static async Task<byte[]> ProcessStreamedFile ( MultipartSection multipartSection, ContentDispositionHeaderValue contentDispositionHeaderValue, ModelStateDictionary modelStateDictionary, string[] sourceFileExtensionArray, long fileSizeLimit ) { try { using(MemoryStream memoryStream = new MemoryStream()) { await multipartSection.Body.CopyToAsync(memoryStream); // 파일이 비어 있거나 크기 제한을 초과하는지 확인한다. if(memoryStream.Length == 0) { modelStateDictionary.AddModelError("File", "The file is empty."); } else if(memoryStream.Length > fileSizeLimit) { long fileSizeLimitMB = fileSizeLimit / 1048576; modelStateDictionary.AddModelError("File", $"The file exceeds {fileSizeLimitMB:N1} MB."); } else if ( !IsValidFileExtensionAndSignature ( contentDispositionHeaderValue.FileName.Value, memoryStream, sourceFileExtensionArray ) ) { modelStateDictionary.AddModelError ( "File", "The file type isn't permitted or the file's signature doesn't match the file's extension." ); } else { return memoryStream.ToArray(); } } } catch(Exception exception) { modelStateDictionary.AddModelError ( "File", $"The upload failed. Please contact the Help Desk for support. Error: {exception.HResult}" ); } return new byte[0]; } #endregion //////////////////////////////////////////////////////////////////////////////// Private #region 파일 확장자/시그니처 유효 여부 구하기 - IsValidFileExtensionAndSignature(fileName, stream, sourceFileExtensionArray) /// <summary> /// 파일 확장자/시그니처 유효 여부 구하기 /// </summary> /// <param name="fileName">파일명</param> /// <param name="stream">스트림</param> /// <param name="sourceFileExtensionArray">소스 파일 확장자 배열</param> /// <returns>파일 확장자/시그니처 유효 여부</returns> private static bool IsValidFileExtensionAndSignature(string fileName, Stream stream, string[] sourceFileExtensionArray) { if(string.IsNullOrEmpty(fileName) || stream == null || stream.Length == 0) { return false; } string fileExtension = Path.GetExtension(fileName).ToLowerInvariant(); if(string.IsNullOrEmpty(fileExtension) || !sourceFileExtensionArray.Contains(fileExtension)) { return false; } stream.Position = 0; using(BinaryReader reader = new BinaryReader(stream)) { if(fileExtension.Equals(".txt") || fileExtension.Equals(".csv") || fileExtension.Equals(".prn")) { if(_allowCharacterByteArray.Length == 0) { // 문자를 ASCII 인코딩으로 제한한다. for(long i = 0L; i < stream.Length; i++) { if(reader.ReadByte() > sbyte.MaxValue) { return false; } } } else { // 문자를 ASCII 인코딩 및 _allowedCharacterByteArray 배열의 값으로 제한합니다. for(long i = 0L; i < stream.Length; i++) { byte byteValue = reader.ReadByte(); if(byteValue > sbyte.MaxValue || !_allowCharacterByteArray.Contains(byteValue)) { return false; } } } return true; } // _fileSignatureDictionary에 서명이 제공되지 않은 파일을 허용해야 하는 경우 // 다음 코드 블록의 주석 처리를 제거한다. // 시스템에서 허용하려는 모든 파일 형식에 대해 가능한 경우 // 파일에 대한 파일 서명을 추가하고 파일 서명 검사를 수행하는 것이 좋다. //if(!_fileSignatureDictionary.ContainsKey(fileExtension)) //{ // return true; //} // 파일 서명 확인 // -------------- // _fileSignatureDictionary에 제공된 파일 서명을 사용하여 // 다음 코드는 입력 콘텐츠의 파일 서명을 테스트한다. List<byte[]> signatureList = _fileSignatureDictionary[fileExtension]; byte[] headerByteArray = reader.ReadBytes(signatureList.Max(m => m.Length)); return signatureList.Any(signature => headerByteArray.Take(signature.Length).SequenceEqual(signature)); } } #endregion } } |
▶ Utilities/MultipartRequestHelper.cs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 |
using Microsoft.Net.Http.Headers; using System; using System.IO; namespace TestProject.Utilities { /// <summary> /// 멀티 파트 요청 헬퍼 /// </summary> public static class MultipartRequestHelper { //////////////////////////////////////////////////////////////////////////////////////////////////// Method ////////////////////////////////////////////////////////////////////////////////////////// Static //////////////////////////////////////////////////////////////////////////////// Public // Content-Type : multipart/form-data; boundary="----WebKitFormBoundarymx2fSWqWSd0OxQqq" // https://tools.ietf.org/html/rfc2046#section-5.1의 사양에는 70자가 합리적인 제한이라고 나와 있다. #region 경계 구하기 - GetBoundary(mediaTypeHeaderValue, boundaryLengthLimit) /// <summary> /// 경계 구하기 /// </summary> /// <param name="mediaTypeHeaderValue">미디어 타입 헤더 값</param> /// <param name="boundaryLengthLimit">경계 길이 제한</param> /// <returns>경계</returns> public static string GetBoundary(MediaTypeHeaderValue mediaTypeHeaderValue, int boundaryLengthLimit) { string boundary = HeaderUtilities.RemoveQuotes(mediaTypeHeaderValue.Boundary).Value; if(string.IsNullOrWhiteSpace(boundary)) { throw new InvalidDataException("Missing content-type boundary."); } if(boundary.Length > boundaryLengthLimit) { throw new InvalidDataException($"Multipart boundary length limit {boundaryLengthLimit} exceeded."); } return boundary; } #endregion #region 멀티 파트 컨텐트 타입 여부 구하기 - IsMultipartContentType(contentType) /// <summary> /// 멀티 파트 컨텐트 타입 여부 구하기 /// </summary> /// <param name="contentType">컨텐트 타입</param> /// <returns>멀티 파트 컨텐트 타입 여부</returns> public static bool IsMultipartContentType(string contentType) { return !string.IsNullOrEmpty(contentType) && contentType.IndexOf("multipart/", StringComparison.OrdinalIgnoreCase) >= 0; } #endregion #region 폼 데이터 컨텐트 배치 소유 여부 구하기 - HasFormDataContentDisposition(contentDispositionHeaderValue) /// <summary> /// 폼 데이터 컨텐트 배치 소유 여부 구하기 /// </summary> /// <param name="contentDispositionHeaderValue">컨텐트 배치 헤더 값</param> /// <returns>폼 데이터 컨텐트 배치 소유 여부</returns> public static bool HasFormDataContentDisposition(ContentDispositionHeaderValue contentDispositionHeaderValue) { // Content-Disposition: form-data; name="key"; return contentDispositionHeaderValue != null && contentDispositionHeaderValue.DispositionType.Equals("form-data") && string.IsNullOrEmpty(contentDispositionHeaderValue.FileName.Value) && string.IsNullOrEmpty(contentDispositionHeaderValue.FileNameStar.Value); } #endregion #region 파일 컨텐트 배치 소유 여부 구하기 - HasFileContentDisposition(contentDispositionHeaderValue) /// <summary> /// 파일 컨텐트 배치 소유 여부 구하기 /// </summary> /// <param name="contentDispositionHeaderValue"></param> /// <returns>파일 컨텐트 배치 소유 여부</returns> public static bool HasFileContentDisposition(ContentDispositionHeaderValue contentDispositionHeaderValue) { // Content-Disposition: form-data; name="file1"; filename="sample.jpg" return contentDispositionHeaderValue != null && contentDispositionHeaderValue.DispositionType.Equals("form-data") && (!string.IsNullOrEmpty(contentDispositionHeaderValue.FileName.Value) || !string.IsNullOrEmpty(contentDispositionHeaderValue.FileNameStar.Value)); } #endregion } } |
▶ Models/FileItemModel.cs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 |
using System; using System.ComponentModel.DataAnnotations; namespace TestProject.Models { /// <summary> /// 파일 항목 모델 /// </summary> public class FileItemModel { //////////////////////////////////////////////////////////////////////////////////////////////////// Property ////////////////////////////////////////////////////////////////////////////////////////// Public #region ID - ID /// <summary> /// ID /// </summary> public int ID { get; set; } #endregion #region 내용 - Content /// <summary> /// 내용 /// </summary> public byte[] Content { get; set; } #endregion #region 파일명 - FileName /// <summary> /// 파일명 /// </summary> [Display(Name = "파일명")] public string FileName { get; set; } #endregion #region 노트 - Note /// <summary> /// 노트 /// </summary> [Display(Name = "노트")] public string Note { get; set; } #endregion #region 크기 - Size /// <summary> /// 크기 /// </summary> [Display(Name = "크기(바이트)")] [DisplayFormat(DataFormatString = "{0:N0}")] public long Size { get; set; } #endregion #region 업로드 시간 - UploadTime /// <summary> /// 업로드 시간 /// </summary> [Display(Name = "업로드 시간(UTC)")] [DisplayFormat(DataFormatString = "{0:G}")] public DateTime UploadTime { get; set; } #endregion } } |
▶ Data/DatabaseContext.cs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
using Microsoft.EntityFrameworkCore; using TestProject.Models; namespace TestProject.Data { /// <summary> /// 데이터베이스 컨텍스트 /// </summary> public class DatabaseContext : DbContext { //////////////////////////////////////////////////////////////////////////////////////////////////// Constructor ////////////////////////////////////////////////////////////////////////////////////////// Public #region 파일 항목 DB 세트 - FileItemDBSet /// <summary> /// 파일 항목 DB 세트 /// </summary> public DbSet<FileItemModel> FileItemDBSet { get; set; } #endregion //////////////////////////////////////////////////////////////////////////////////////////////////// Constructor ////////////////////////////////////////////////////////////////////////////////////////// Public #region 생성자 - DatabaseContext(option) /// <summary> /// 생성자 /// </summary> /// <param name="option">옵션</param> public DatabaseContext(DbContextOptions<DatabaseContext> option) : base(option) { } #endregion } } |
▶ Filters/DisableFormValueModelBindingAttribute.cs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 |
using Microsoft.AspNetCore.Mvc.Filters; using Microsoft.AspNetCore.Mvc.ModelBinding; using System; using System.Collections.Generic; namespace TestProject.Filters { /// <summary> /// 폼 값 모델 바인딩 비활성 어트리뷰트 /// </summary> [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)] public class DisableFormValueModelBindingAttribute : Attribute, IResourceFilter { //////////////////////////////////////////////////////////////////////////////////////////////////// Method ////////////////////////////////////////////////////////////////////////////////////////// Public #region 리소스 실행전 처리하기 - OnResourceExecuting(context) /// <summary> /// 리소스 실행전 처리하기 /// </summary> /// <param name="context">리소스 실행전 컨텍스트</param> public void OnResourceExecuting(ResourceExecutingContext context) { IList<IValueProviderFactory> list = context.ValueProviderFactories; list.RemoveType<FormValueProviderFactory>(); list.RemoveType<FormFileValueProviderFactory>(); list.RemoveType<JQueryFormValueProviderFactory>(); } #endregion #region 리소스 실행후 처리하기 - OnResourceExecuted(context) /// <summary> /// 리소스 실행후 처리하기 /// </summary> /// <param name="context">리소스 실행후 컨텍스트</param> public void OnResourceExecuted(ResourceExecutedContext context) { } #endregion } } |
▶
■ CorsMiddlewareExtensions 클래스의 UseCors 확장 메소드를 사용해 CORS(Cross Origin Resource Sharing)를 설정하는 방법을 보여준다. ▶ Startup.cs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 |
using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; namespace TestProject { /// <summary> /// 시작 /// </summary> public class Startup { //////////////////////////////////////////////////////////////////////////////////////////////////// Property ////////////////////////////////////////////////////////////////////////////////////////// Public #region 구성 - Configuration /// <summary> /// 구성 /// </summary> public IConfiguration Configuration { get; } #endregion //////////////////////////////////////////////////////////////////////////////////////////////////// Constructor ////////////////////////////////////////////////////////////////////////////////////////// Public #region 생성자 - Startup(configuration) /// <summary> /// 생성자 /// </summary> /// <param name="configuration">구성</param> public Startup(IConfiguration configuration) { Configuration = configuration; } #endregion //////////////////////////////////////////////////////////////////////////////////////////////////// Method ////////////////////////////////////////////////////////////////////////////////////////// Public #region 서비스 컬렉션 구성하기 - ConfigureServices(services) /// <summary> /// 서비스 컬렉션 구성하기 /// </summary> /// <param name="services">서비스 컬렉션</param> public void ConfigureServices(IServiceCollection services) { services.AddControllers(); } #endregion #region 구성하기 - Configure(app, environment) /// <summary> /// 구성하기 /// </summary> /// <param name="app">애플리케이션 빌더</param> /// <param name="environment">웹 호스트 환경</param> public void Configure(IApplicationBuilder app, IWebHostEnvironment environment) { if(environment.IsDevelopment()) { app.UseDeveloperExceptionPage(); } app.UseHttpsRedirection(); app.UseRouting(); app.UseAuthorization(); app.UseEndpoints ( endpoints => { endpoints.MapControllers(); } ); // CORS 정책에 지정된 출처를 추가한다. app.UseCors(option => option.WithOrigins("http://www.test.net/api/test")); // CORS 정책에 모든 출처를 허용하는지 확인한다. app.UseCors(option => option.AllowAnyOrigin().WithMethods(new string[] { "GET", "POST" })); } #endregion } } |
■ NewtonsoftJsonMvcBuilderExtensions 클래스의 AddNewtonsoftJson 확장 메소드를 사용해 카멜 표기법으로 JSON 문자열을 구하는 방법을 보여준다. ▶ Startup.cs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 |
using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Newtonsoft.Json.Serialization; namespace TestProject { /// <summary> /// 시작 /// </summary> public class Startup { //////////////////////////////////////////////////////////////////////////////////////////////////// Property ////////////////////////////////////////////////////////////////////////////////////////// Public #region 구성 - Configuration /// <summary> /// 구성 /// </summary> public IConfiguration Configuration { get; } #endregion //////////////////////////////////////////////////////////////////////////////////////////////////// Constructor ////////////////////////////////////////////////////////////////////////////////////////// Public #region 생성자 - Startup(configuration) /// <summary> /// 생성자 /// </summary> /// <param name="configuration">구성</param> public Startup(IConfiguration configuration) { Configuration = configuration; } #endregion //////////////////////////////////////////////////////////////////////////////////////////////////// Method ////////////////////////////////////////////////////////////////////////////////////////// Public #region 서비스 컬렉션 구성하기 - ConfigureServices(services) /// <summary> /// 서비스 컬렉션 구성하기 /// </summary> /// <param name="services">서비스 컬렉션</param> public void ConfigureServices(IServiceCollection services) { services.AddControllers() .AddNewtonsoftJson ( options => { options.SerializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver(); } ); } #endregion #region 구성하기 - Configure(app, environment) /// <summary> /// 구성하기 /// </summary> /// <param name="app">애플리케이션 빌더</param> /// <param name="environment">웹 호스트 환경</param> public void Configure(IApplicationBuilder app, IWebHostEnvironment environment) { if(environment.IsDevelopment()) { app.UseDeveloperExceptionPage(); } app.UseHttpsRedirection(); app.UseRouting(); app.UseAuthorization(); app.UseEndpoints ( endpoints => { endpoints.MapControllers(); } ); } #endregion } } |
※ JSON 문자열 변환시 카멜
■ Microsoft.AspNetCore.Mvc.NewtonsoftJson 누겟을 설치하는 방법을 보여준다. 1. Visual Studio를 실행한다. 2. [도구] / [NuGet 패키지 관리자] / [패키지 관리자 콘솔] 메뉴를 실행한다.
■ IViewComponentHelper 인터페이스의 InvokeAsync 메소드를 사용해 뷰 컴포넌트의 특정 뷰를 호출하는 방법을 보여준다. ▶ Models/FavoriteModel.cs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
namespace TestProject.Models { /// <summary> /// 즐겨찾기 모델 /// </summary> public class FavoriteModel { //////////////////////////////////////////////////////////////////////////////////////////////////// Property ////////////////////////////////////////////////////////////////////////////////////////// Public #region ID - ID /// <summary> /// ID /// </summary> public int ID { get; set; } #endregion #region 제목 - Title /// <summary> /// 제목 /// </summary> public string Title { get; set; } #endregion #region URL - URL /// <summary> /// URL /// </summary> public string URL { get; set; } #endregion } } |
▶ ViewComponents/FavoriteViewComponent.cs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
using Microsoft.AspNetCore.Mvc; using System.Collections.Generic; using TestProject.Models; namespace TestProject.ViewComponents { /// <summary> /// 즐겨찾기 뷰 컴포넌트 /// </summary> public class FavoriteViewComponent : ViewComponent { //////////////////////////////////////////////////////////////////////////////////////////////////// Method ////////////////////////////////////////////////////////////////////////////////////////// Public #region 호출하기 - Invoke() /// <summary> /// 호출하기 /// </summary> /// <returns>뷰 컴포넌트 결과</returns> public IViewComponentResult Invoke(string page) { List<FavoriteModel> list = new List<FavoriteModel>() { new FavoriteModel { ID = 1, Title = "ICODEBROKER", URL = "https://icodebroker.tistory.com/" }, new FavoriteModel { ID = 2, Title = "구글" , URL = "https://www.google.com/" }, new FavoriteModel { ID = 3, Title = "유튜브" , URL = "https://www.youtube.com/" }, new FavoriteModel { ID = 4, Title = "다음" , URL = "https://www.daum.net/" }, new FavoriteModel { ID = 5, Title = "네이버" , URL = "https://www.naver.com/" } }; return View(page, list); } #endregion } } |
▶ Views/Shared/Components/Favorite/Default.cshtml
1 2 3 4 5 6 7 8 9 10 11 |
@using TestProject.Models @model List<FavoriteModel> <p>디폴트 뷰</p> <ul> @foreach(FavoriteModel favorite in Model) { <li><a href="@favorite.URL" target="_blank">@favorite.Title</a></li> } </ul> |
■ ViewComponent 클래스를 사용해 데이터를 표시하는 뷰 컴포넌트를 만드는 방법을 보여준다. ▶ Models/TestModel.cs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
namespace TestProject.Models { /// <summary> /// 테스트 모델 /// </summary> public class TestModel { //////////////////////////////////////////////////////////////////////////////////////////////////// Property ////////////////////////////////////////////////////////////////////////////////////////// Public #region ID - ID /// <summary> /// ID /// </summary> public int ID { get; set; } #endregion #region 제목 - Title /// <summary> /// 제목 /// </summary> public string Title { get; set; } #endregion } } |
▶ ViewComponents/TestListViewComponent.cs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
using Microsoft.AspNetCore.Mvc; using System.Collections.Generic; using TestProject.Models; namespace TestProject.ViewComponents { /// <summary> /// 테스트 목록 뷰 컴포넌트 /// </summary> public class TestListViewComponent : ViewComponent { //////////////////////////////////////////////////////////////////////////////////////////////////// Method ////////////////////////////////////////////////////////////////////////////////////////// Public #region 호출하기 - Invoke() /// <summary> /// 호출하기 /// </summary> /// <returns>뷰 컴포넌트 결과</returns> public IViewComponentResult Invoke() { List<TestModel> list = new List<TestModel>() { new TestModel { ID = 1, Title = "ASP.NET Core" }, new TestModel { ID = 2, Title = "Bootstrap" }, new TestModel { ID = 3, Title = "C#" }, new TestModel { ID = 4, Title = "Dapper" }, new TestModel { ID = 5, Title = "Azure" }, new TestModel { ID = 6, Title = "jQuery" }, new TestModel { ID = 7, Title = "Angular" } }; return View(list); } #endregion } } |
▶ Views/Shared/TestList/Default.cshtml
1 2 3 4 5 6 7 8 9 |
@model List<TestProject.Models.TestModel> <ul> @foreach(TestModel test in Model) { <li>@test.Title</li> } </ul> |
▶ Controllers/TestController.cs
■ ViewComponent 클래스를 사용해 저작권을 표시하는 뷰 컴포넌트를 만드는 방법을 보여준다. ▶ ViewComponents/CopyrightViewComponent.cs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
using Microsoft.AspNetCore. Mvc; using System; namespace TestProject.ViewComponents { /// <summary> /// 저작권 뷰 컴포넌트 /// </summary> public class CopyrightViewComponent : ViewComponent { //////////////////////////////////////////////////////////////////////////////////////////////////// Method ////////////////////////////////////////////////////////////////////////////////////////// Public #region 호출하기 - Invoke() /// <summary> /// 호출하기 /// </summary> /// <returns>뷰 컴포넌트 결과</returns> public IViewComponentResult Invoke() { string viewName = "Default"; if(DateTime.Now.Second % 2 == 0) { viewName = "Alternate"; } return View(viewName); } #endregion } } |
▶ Views/Shared/Components/Copyright/Default.cshtml
1 2 3 4 5 |
<div class="text-center"> Copyright © @DateTime.Now.Year all right reserved. </div> |
▶ Views/Shared/Components/Copyright/Alternate.cshtml
1 2 3 4 5 |
<div class="text-center"> Copyright © @DateTime.Now.Year <em>Icodebroker</em> all right reserved. </div> |
▶ Controllers/TestController.cs
■ JsonConfigurationExtensions 클래스의 AddJsonFile 확장 메소드를 사용해 환경 설정 파일을 로드하는 방법을 보여준다. ▶ Settings/MainSettings.cs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
namespace TestProject.Settings { /// <summary> /// 메인 설정 /// </summary> public class MainSettings { //////////////////////////////////////////////////////////////////////////////////////////////////// Method ////////////////////////////////////////////////////////////////////////////////////////// Public #region 애플리케이션 ID - ApplicationID /// <summary> /// 애플리케이션 ID /// </summary> public int ApplicationID { get; set; } #endregion #region 애플리케이션명 - ApplicationName /// <summary> /// 애플리케이션명 /// </summary> public string ApplicationName { get; set; } #endregion } } |
▶ Settings/MainSettings.json
1 2 3 4 5 6 7 8 9 |
{ "MainSettings" : { "ApplicationID" : 100, "ApplicationName" : "TestApplication" } } |
※ 상기 파일의
■ LoggerExtensions 클래스의 LogInformation 확장 메소드를 사용해 로그를 기록하는 방법을 보여준다. ▶ Controllers/TestController.cs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 |
using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; using System; namespace TestProject.Controllers { /// <summary> /// 테스트 컨트롤러 /// </summary> public class TestController : Controller { //////////////////////////////////////////////////////////////////////////////////////////////////// Field ////////////////////////////////////////////////////////////////////////////////////////// Private #region Field /// <summary> /// 로그 기록기 /// </summary> private ILogger<TestController> logger; #endregion //////////////////////////////////////////////////////////////////////////////////////////////////// Constructor ////////////////////////////////////////////////////////////////////////////////////////// Public #region 생성자 - TestController(logger) /// <summary> /// 생성자 /// </summary> /// <param name="logger">로그 기록기</param> public TestController(ILogger<TestController> logger) { this.logger = logger; } #endregion //////////////////////////////////////////////////////////////////////////////////////////////////// Method ////////////////////////////////////////////////////////////////////////////////////////// Public #region 인덱스 페이지 처리하기 - Index() /// <summary> /// 인덱스 페이지 처리하기 /// </summary> /// <returns>액션 결과</returns> public IActionResult Index() { this.logger.LogInformation($"인덱스 페이지 실행 : {DateTime.Now:HH:mm:ss}"); return View(); } #endregion #region 소개 페이지 처리하기 - About() /// <summary> /// 소개 페이지 처리하기 /// </summary> /// <returns>액션 결과</returns> public IActionResult About() { this.logger.LogInformation($"소개 페이지 실행 : {DateTime.Now:HH:mm:ss}"); return View(); } #endregion } } |
▶ Views/Test/Index.cshtml
1 2 3 4 5 6 |
@{ Layout = null; } <p>LoggerExtensions 클래스 : LogInformation 확장 메소드를 사용해 로그 기록하기</p> <hr /> <p>인덱스 페이지 입니다.</p> |
▶ Views/Test/About.cshtml
1 2 3 4 5 6 |
@{ Layout = null; } <p>LoggerExtensions 클래스 : LogInformation 확장 메소드를 사용해 로그 기록하기</p> <hr /> <p>소개 페이지 입니다.</p> |
TestProject.zip
■ 파일을 업로드/다운로드하는 방법을 보여준다. ▶ Controllers/TestController.cs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 |
using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.Net.Http.Headers; using System.Collections.Generic; using System.IO; using System.Threading.Tasks; namespace TestProject.Controllers { /// <summary> /// 테스트 컨트롤러 /// </summary> public class TestController : Controller { //////////////////////////////////////////////////////////////////////////////////////////////////// Field ////////////////////////////////////////////////////////////////////////////////////////// Private #region Field /// <summary> /// 웹 호스트 환경 /// </summary> private IWebHostEnvironment environment; #endregion //////////////////////////////////////////////////////////////////////////////////////////////////// Constructor ////////////////////////////////////////////////////////////////////////////////////////// Public #region 생성자 - TestController(environment) /// <summary> /// 생성자 /// </summary> /// <param name="environment">웹 호스트 환경</param> public TestController(IWebHostEnvironment environment) { this.environment = environment; } #endregion //////////////////////////////////////////////////////////////////////////////////////////////////// Method ////////////////////////////////////////////////////////////////////////////////////////// Public #region 업로드 페이지 처리하기 - Upload() /// <summary> /// 업로드 페이지 처리하기 /// </summary> [HttpGet] public IActionResult Upload() { return View(); } #endregion #region 업로드 페이지 처리하기 - Upload(fileCollection) /// <summary> /// 업로드 페이지 처리하기 /// </summary> /// <param name="fileCollection">파일 컬렉션</param> /// <returns>액션 결과 태스크</returns> [HttpPost] public async Task<IActionResult> Upload(ICollection<IFormFile> fileCollection) { var uploadDirectoryPath = Path.Combine(this.environment.WebRootPath, "upload"); foreach(IFormFile formFile in fileCollection) { if(formFile.Length > 0) { string fileName = Path.GetFileName ( ContentDispositionHeaderValue.Parse(formFile.ContentDisposition).FileName.Value ); using(FileStream stream = new FileStream(Path.Combine(uploadDirectoryPath, fileName), FileMode.Create)) { await formFile.CopyToAsync(stream); } } } return View(); } #endregion #region 다운로드 페이지 처리하기 - Download(fileName) /// <summary> /// 다운로드 페이지 처리하기 /// </summary> public FileResult Download(string fileName = "Test.txt") { byte[] fileByteArray = System.IO.File.ReadAllBytes ( Path.Combine(this.environment.WebRootPath, "upload", fileName) ); return File(fileByteArray, "application/octet-stream", fileName); } #endregion } } |
▶ Views/Test/Upload.cshtml
1 2 3 4 5 6 7 8 9 10 11 12 13 |
@{ Layout = null; } <p>파일 업로드/다운로드하기</p> <hr /> <form asp-controller="Test" asp-action="Upload" method="post" enctype="multipart/form-data"> <p><input type="file" name="fileCollection" multiple /></p> <p><input type="submit" value="업로드" /></p> </form> |
※ wwwroot 폴더에서 upload 폴더를 생성한다. ※ 파일 다운로드 URL은 아래를
■ ServiceCollectionServiceExtensions 클래스의 AddSingleton 확장 메소드를 사용해 싱글톤 객체 의존성 주입을 만드는 방법을 보여준다. ▶ Services/IInfoService.cs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
namespace TestProject.Services { /// <summary> /// 정보 서비스 인터페이스 /// </summary> public interface IInfoService { //////////////////////////////////////////////////////////////////////////////////////////////////// Method #region URL 구하기 - GetURL() /// <summary> /// URL 구하기 /// </summary> /// <returns>URL</returns> string GetURL(); #endregion } } |
▶ Services/InfoService.cs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
namespace TestProject.Services { /// <summary> /// 정보 서비스 /// </summary> public class InfoService : IInfoService { //////////////////////////////////////////////////////////////////////////////////////////////////// Method ////////////////////////////////////////////////////////////////////////////////////////// Public #region URL 구하기 - GetURL() /// <summary> /// URL 구하기 /// </summary> /// <returns>URL</returns> public string GetURL() { return "https://icodebroker.tistory.com"; } #endregion } } |
▶ Controllers/TestController.cs