Sicos1977/MSGReader: C# Outlook MSG file reader without the need for Outlook (github.com)
Msgreader는 이메일 파일인 .msg와 .eml확장자 파일을 읽을 수 있도록하는 라이브러리입니다.
원본 github주소는 위 그림을 클릭하면 이동하실 수 있습니다.
이메일 파일에 담긴 이메일 정보(보낸이, 받는이, 보낸시각, 받은시각, cc, bcc, 메일제목, 메일내용, 첨부파일 등)의 정보를 읽을 수 있도록합니다.
상세한 예외처리는 제외하고 용법을 보겠습니다.
먼저, 메일정보를 쉽게 주고받을 수 있도록 프로퍼티를 충분히 가진 클래스를 선언하겠습니다.
class MailFile {
public string FileDirectory { get; set; } // 파일 경로
public string FileName { get; set; } // 파일명
public string FilePath { get; set; } // 파일경로 + 파일명
public string Originator { get; set; } // 보낸사람
public string Addressee { get; set; } // 받는사람
public string CC { get; set; } // CC
public string BCC { get; set; } // BCC
public DateTime SentDate { get; set; } // 발신 시간
public DateTime ReceivedDate { get; set; } // 수신 시간
public MailFile(string fileDirectory, string fileName)
{
FileDirectory = fileDirectory;
FileName = fileName;
FilePath = Path.Combine(fileDirectory, fileName);
// 그냥 string으로 더하지 않고 Path.Combine으로 쓴이유를 모른다면 독자는 꼭 찾아봐야한다!
}
}
예제이기 때문에 msg인지 eml파일인지 여부를 비교할 대상은 그냥 선언하겠습니다.
(여기서는 확장자로만 구분합니다.)
// 메일 파일을 읽기전에 있어야할 것들을 기입한 부분입니다.
// msg인지 eml인지 비교할 확장자,
// MailFile 클래스의 선언
// 생성된 첨부파일을 다운받을 경로 등
string msgExtention = ".MSG";
string emlExtention = ".EML";
string mailFileDirectory = @"파일경로, \가 보통들어가서 @를 붙임";
string mailFileName = @"파일명";
var mailFile = new MailFile(mailFileDirectory, mailFileName);
string downloadDirectory = @"첨부파일이있다면 다운받을 경로이다.";
다음은 메일파일의 정보가 있는지 여부를 받아서 받아오는 것이다.
FileInfo mailFileInfo = new FileInfo(mailFile.FilePath);
if (fileInfo.Exists && fileInfo.Length > 0)
// fileInfo.Exists는 파일이 존재하는지 여부를 리턴
// fileInfo.Length는 파일의 크기를 byte단위로 리턴
{
if (mailFileInfo.Extension.ToUpper().Equals(msgExtention))
// 파일 경로 비교 ToUpper()를 해도되고 StringComparison.OrdinalIgnoreCase 를 사용해도된다.
{
using var msgFile = new MsgReader.Outlook.Storage.Message(mailFilePath);
// 메일 파일의 경로에 따라 여기서는 확장자값이 .MSG이면 msg인스턴스(Storage.Message)를 생성한다.
// 아래는 읽으면 감이 올 것이다. 각 정보를 받아오는 것이고
// 여기에 매칭되도록 초반에 MailFile을 선언했다.
// 실제 사용시에는 MailFile에 첨부파일 존재여부도 넣어주면 좋다.
mailFile.Originator = msg.Sender?.DisplayName; // 보낸이
mailFile.Addressee = msg.GetEmailRecipients(RecipientType.To, false, false); // 받는이
mailFile.CC = msg.GetEmailRecipients(RecipientType.Cc, false, false); // CC
mailFile.BCC = msg.GetEmailRecipients(RecipientType.Bcc, false, false); // BCC
mailFile.SentDate = msg.SentOn ?? DateTime.MinValue; // 보낸시각
mailFile.ReceivedDate = msg.ReceivedOn ?? DateTime.MinValue; // 받는시각
// 첨부파일은 아래와 같은방법으로 가져올 수 있다.
// #### 첨부파일류들은 data[]로 파일데이터를 들고있기때문에 아주 무겁다. ####
// ## 그러나 IDisposable을 구현하고 있어서 dispose()나 using을 사용할 수 있다.
// ## 해당부분은 여기에서는 포함하고 있지 않아서 참고하여 개발시 이 부분을 반드시 확인하기바란다.
// ## 아니면 메모리가 탈탈 털리는 현상을 볼 지도...
// ## 또한 파일데이터를 들고있기때문에 너무 용량이 큰 친구들은 이것으로 첨부파일 뽑으려면
// ## 메모리가 아주커야할 것이다... 파일 용량에 꼭 제한을 두자.
// ## 도입부에 FileInfo 선언했는데 거기서 처리할 수 있다.
var attachmentsObject = msgFile.Attachments;
attachmentsObject.ForEach(attachment =>
{
try
{
if (!Directory.Exists(downloadDirectory)){ // 첨부파일을 다운받을 경로가 없으면 만든다.
Directory.CreateDirectory(downloadDirectory);
}
if (attachment != null && attachment is Storage.Attachment) // 일반적인 첨부파일이면
{
var attachmentCast = attachment as Storage.Attachment ?? throw new Exception($"형변환했는데 null이면 던질 예외");
string attachmentPath = Path.Combine(downloadDirectory, attachmentCast.FileName); // 내려받을 첨부파일 path
File.WriteAllBytes(attachmentPath, attachmentCast.Data); // 다운로드
}
else if (attachment != null && attachment is Storage.Message) // 앞에 using var msgFile = new MsgReader.Outlook.Storage.Message(mailFilePath); 부분을 보면 눈치 챘을 수도 있지만 이는 첨부파일이 MSG파일인 경우이다.
{
var attachmentCast = attachment as Storage.Message ?? throw new Exception($"형변환했는데 null이면 던질 예외");
string attachmentPath = Path.Combine(downloadDirectory, attachmentCast.FileName);
attachmentCast.Save(attachmentPath); // Storage.Message는 Save함수를 사용하여 파일을 내려받아야 한다.
}
else
{
// 둘다 아닌경우에 취할 조치를 하면 된다. 예외처리를 해도 좋다.
}
}
catch (Exception ex)
{
Log.Error(ex.ToString());
}
});
}
else if (mailFileInfo.Extension.ToUpper().Equals(emlExtention))
{
var emlFile = MsgReader.Mime.Message.Load(mailFileInfo); // eml 파일을 읽는 방법이다.
if (emlFile.Headers != null) // eml 파일의 메일 파일정보 불러오기는 복잡하다... msgReader 공식 문서를 참조하면 이렇게 해야한다.
{
if (emlFile.Headers.From != null && !string.IsNullOrEmpty(eml.Headers.From.DisplayName))
{ mailFile.Originator = emlFile.Headers.From.DisplayName.ToString().Replace("\"", string.Empty); }
else if (eml.Headers.From != null && !string.IsNullOrEmpty(eml.Headers.From.Address))
{ mailFile.Originator = emlFile.Headers.From.Address.ToString().Replace("\"", string.Empty); }
else { mailFile.Originator = ""; }
emlFile.Headers.To.ForEach(to => mailInfo.Addressee += "," + to.ToString());
if (mailFile.Addressee == null) { mailFile.Addressee = ""; }
else { mailFile.Addressee = mailFile.Addressee.Substring(1).Replace("\"", string.Empty); }
emlFile.Headers.Cc.ForEach(cc => mailFile.CC += "," + cc.ToString());
if (mailFile.CC == null) { mailFile.CC = ""; }
else { mailFile.CC = mailFile.CC.Substring(1).Replace("\"", string.Empty); }
emlFile.Headers.Bcc.ForEach(bcc => mailFile.BCC += "," + bcc.ToString());
if (mailFile.BCC == null) { mailFile.BCC = ""; }
else { mailFile.BCC = mailInfo.BCC.Substring(1).Replace("\"", string.Empty); }
if (emlFile.Headers.DateSent != null) { mailFile.SentDate = emlFile.Headers.DateSent; }
else { mailFile.SentDate = DateTime.MinValue; }
if (emlFile.Headers.Received != null && emlFile.Headers.Received.Count > 0)
{
mailFile.ReceivedDate = emlFile.Headers.Received.Last().Date;
}
else
{
mailFile.ReceivedDate = DateTime.MinValue;
}
} // 여기까지가 메일정보 읽기
ObservableCollection<MessagePart> attachments = emlFile.Attachments;
var attachmentsCount = attachments.Count;
foreach (var attachment in attachments) // eml파일은 정보읽기는 복잡하지만 첨부파일 다운로드는 간단하다.
{
try
{
if (!Directory.Exists(downloadDirectory))
Directory.CreateDirectory(downloadDirectory);
if (attachment.IsAttachment)
attachment.Save(new FileInfo(Path.Combine(downloadDirectory, attachment.FileName)));
}
catch (Exception ex)
{
Log.Error(ex.ToString());
}
}
}
}
생각보다 코드가 길기 때문에 설명도 코드블럭내에 같이 적어놨습니다. 특히 ####, ## 붙은 부분을 반드시 읽어서 참고시에 메모리 등 관리문제를 최대한 방지 하시기 바랍니다.
class Example
{
public List<Fruit> Fruits;
public string Str;
public int Number = 0;
public int NumberDefault0 = 0;
public bool IsOk;
public string? NullableStr;
public int? NullableInteger;
}
class Fruit
{
public string Name;
public int price;
}
그리고 아래는 실행할 소스 입니다.
using Newtonsoft.Json;
Example example = new()
{
Str = "문자열",
Number = 10,
NumberDefault0 = 0,
IsOk = true,
NullableStr = null,
NullableInteger = null
};
var exampleJson0 = JsonConvert.SerializeObject(example);
var exampleJson1 = JsonConvert.SerializeObject(example, Formatting.None);
var exampleJson2 = JsonConvert.SerializeObject(example, Formatting.Indented);
var exampleJson3 = JsonConvert.SerializeObject(example, Formatting.None, new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore });
var exampleJson4 = JsonConvert.SerializeObject(example, Formatting.None, new JsonSerializerSettings { DefaultValueHandling = DefaultValueHandling.Ignore });
var exampleJson5 = JsonConvert.SerializeObject(example, Formatting.Indented, new JsonSerializerSettings { DefaultValueHandling = DefaultValueHandling.Ignore });
var exampleJson6 = JsonConvert.SerializeObject(example, Formatting.Indented, new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore });
Console.WriteLine($"exampleJson0: {exampleJson0}");
Console.WriteLine($"exampleJson1: {exampleJson1}");
Console.WriteLine($"exampleJson2: {exampleJson2}");
Console.WriteLine($"exampleJson3: {exampleJson3}");
Console.WriteLine($"exampleJson4: {exampleJson4}");
Console.WriteLine($"exampleJson5: {exampleJson5}");
Console.WriteLine($"exampleJson5: {exampleJson6}");
인스턴스를 json형태로 변환했는데 json형태의 문자열을 클래스의 인스턴스로 변환하는 것도 있어야겠죠?
우선 이해를 돕기 위한 코드를 작성하겠습니다.
class Example
{
public List<Fruit> Fruits;
public string Str;
public int Number = 0;
public int NumberDefault0 = 0;
public bool IsOk;
public string? NullableStr;
public int? NullableInteger;
public override string? ToString() => JsonConvert.SerializeObject(this);
public string? ToStringJson() => JsonConvert.SerializeObject(this, new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore});
public string? ToString2() =>
"Fruits=" + (Fruits == null ? "null" : Fruits.ToString()) +
"\n&Number=" + Number +
"\n&NumberDefault0=" + NumberDefault0 +
"\n&IsOk=" + IsOk +
"\n&NullableStr=" + (NullableStr == null ? "null" : NullableStr.ToString()) +
"\n&NullableInteger=" + (NullableInteger == null ? "null" : NullableInteger.ToString());
}
class Fruit
{
public string Name;
public int price;
}
Example 클래스를 출력 가능하게 바꾸는 클래스를 기본 ToString()을 override 한것 외에도 두개를 더 만들었고
다음은 실행 코드 입니다.
Example example = new()
{
Str = "문자열",
Number = 10,
NumberDefault0 = 0,
IsOk = true,
NullableStr = null,
NullableInteger = null
};
// 1.------------------------------------------------
var exampleJson0 = JsonConvert.SerializeObject(example);
Console.WriteLine($"example: {example}");
Console.WriteLine($"example.ToStringJson(): {example.ToStringJson()}");
Console.WriteLine($"example.ToString2(): {example.ToString2()}");
// 2.------------------------------------------------
var ex0 = JsonConvert.DeserializeObject(exampleJson0);
Console.WriteLine($"{ex0.GetType().Name}: {ex0}");
//Console.WriteLine($"ex0.ToStringJson(): {ex0.ToStringJson()}");
//Console.WriteLine($"ex0.ToString2(): {ex0.ToString2()}");
// 3.------------------------------------------------
var ex1 = JsonConvert.DeserializeObject<Example>(exampleJson0);
Console.WriteLine($"{ex1.GetType().Name}: {ex1}");
Console.WriteLine($"ex1.ToStringJson(): {ex1.ToStringJson()}");
Console.WriteLine($"ex1.ToString2(): {ex1.ToString2()}");
if (app.Environment.IsDevelopment())
{
// 여기 부분
var configuration = new ConfigurationBuilder()
.AddJsonFile("appsettings.Development.json", optional: false, reloadOnChange: true)
.Build();
}
이렇게 입력한 후 로그를 남길 컨트롤러로 돌아갑니다.
이후 부터는 사실 이미 설치되어있는 ILogger를 이용하면 되는데요. 그래도 혹시 처음 접하시는분들을 위해 방법을 남깁니다.
[Route("[controller]")]
public class HomeController : ControllerBase
{
private readonly IConfiguration _configuration;
private readonly ILogger<HomeController> _logger;
public HomeController(ILogger<HomeController> logger, IConfiguration configuration)
{
_logger = logger;
_configuration = configuration;
}
}
이와 같이 private readonly + 생성자의 변수를 입력하면 ILogger에 대한 의존성 주입이 완료되며,
ILogger로 작성한 로그가 Serilog 라이브러리에서 파일이나 콘솔에 남길 수 있게 됩니다.
각각 메소드에
_logger.LogVerbose();
_logger.LogDebug();
_logger.LogInformation();
_logger.LogWarning();
_logger.LogFatal();
_logger.LogError();
등과 같이 사용하실 수 있습니다. 괄호 내에는 string 변수를 입력하면 되며, 서식 문자열 등은 앞에 Serilog 게시글을 확인하시기 바랍니다.
다음은 main() 또는 program.cs에 작성해야하는 부분이다. using Serilog; 하나면 Serilog.Sinks~류 들을 별도로 설치했다고해서 using도 또 써줘야하는건 아니다.
아래 소스는 위에서 만든 클래스를 선언하여 logFIlePath를 담을뿐 실질적으로Log.Logger 부분만 봐도 무방하다.
using Serilog;
LogHandler logHandler = new();
Log.Logger = new LoggerConfiguration()
//.WriteTo.Console() // Serilog.Sinks.Console를 설치한 경우 사용가능
.WriteTo.File( // Serilog.Sinks.File를 설치한 경우 사용가능
logHandler.logFilePath, // 로그를 지정할 파일 경로
rollingInterval: RollingInterval.Day,
retainedFileCountLimit: null,
fileSizeLimitBytes: 50 * 1024 * 1024,
rollOnFileSizeLimit: true,
outputTemplate: "{Timestamp:yyyy-MM-dd HH:mm:ss.fff} [{Level}] {Message}{NewLine}{Exception}")
.MinimumLevel.Information() // 로그 최저레벨을 설정한다.
// .MinimumLevel.Is(Serilog.Events.LogEventLevel.Information) // 바로 위 .MinimumLevel.Information()를 제거하고 이 방법을 사용하면 설정 등으로 가져와서 최저레벨을 지정할 수도 있다.
.CreateLogger();
WriteTo.Console() 소스에 주석으로 적혀있기 때문에 패스하고
WriteTo.File()에는 여러가지 변수가 올 수 있는데
1. rollingInterval은 새로 파일을 만드는 기간 또는 주기를 말하는 것이다. 위 소스에는 RollingInterval.Day이기 때문에 일이 바뀔때마다 새로운 날짜의 파일이 생성된다.
2. retainedFileCountLimit은 로그파일 갯수 상한을 정하는 것이다.
필자의 경우는 서버 관리자가 직접 지울만큼 쌓이기 전까지는 보관하는 것을 선호하기 때문에 null을 입력했다. 그러나 하드용량이 매우적은 컴퓨터나 서버에서 사용한다면 제한을 두는 것이 좋다.
3. fileSizeLimitBytes는 로그파일의 최대크기를 정해놓는 것으로 byte단위로 입력하면 된다. 필자의 짧은 경험상으로는 50mb가 넘어가면 키는데 좀 딜레이가 생기는 것 같아서 저렇게 작성했다.
4. rollOnFileSizeLimit: 은 3번에서 지정한 로그파일의 용량 한계가 넘어가면 어떻게 할지를 결정하는 것이다. true면 다음 파일을 생성하고 false면 로그파일의 용량이 한계에 다다르면 더이상 파일을 작성하지 않는다. 따라서 근방의 로그가 작성되지 않길 바랄리가 없기 떄문에 true를 해야만 한다.
5. outputTemplate: 은 찍히는 로그의 모양을 나타낸 것이다. 미세한 시간차이를 확인하고 싶은게 아니라면 fff를 빼도 좋다. 시간 그 부분 제외하고는 딱히 수정해본적이 없기 때문에 필요하다면 Template작성방식을 검색해보아야 할 것이다.
그리고
.MinimumLevel. 은 찍히는 최저로그레벨을 정하는 것이다. 필자가 기억하기로 Verbose, Debug, Information, Waring, Error, Fatal 정도가 있는데 우측으로 갈수록 높은 레벨이다. 따라서 .MinimumLevel.Information()으로 작성하면 Information, Waring, Error, Fatal 는 찍히고 Verbose, Debug는 무시된다. ( .MinimumLevel.Information() - Information()자리에 Verbose()부터 Fatal()까지 모두 올 수 있다)
.MimimumLevel은 Is()라는 함수도 갖고있는데 변수로 Serilog.Events.LogEventLevel 안에있는 프로퍼티들을 사용할 수 있다. 위의 .MinimumLevel.과 동일한 기능이다. 그러나 Is()가 매력적인 것은 개발시 미리 지정이 아니라 실행시 어떤 로직에 따라서 로그 최저레벨을 정할 수 있다는 점이다.
예를 들면 .MinimumLevel.Is(WhatIWant()) 로 두고 public WhatIWant()의 리턴값으로 Serilog.Events.LogEventLevel.Warning 등을 사용하여 결정할 수있다.
ASP.NET CORE 7.0 버전의 MVC 모델에 sql server를 사용하여 만든 토이프로젝트 내에 테이블과 프로시저입니다.
각 테이블은 ENTITY FRAMEWORK CORE의 모델을 이용해 프로그램이 생성해준 테이블입니다.
CREATE TABLE [dbo].[Worker] (
[Id] INT IDENTITY (1, 1) NOT NULL,
[CompanyId] INT NOT NULL,
[Name] NVARCHAR (20) NOT NULL,
[Email] NVARCHAR (20) NOT NULL,
[Phone] NVARCHAR (12) NOT NULL,
[IsDelete] NVARCHAR (1) DEFAULT ('N') NOT NULL,
CONSTRAINT [PK_Worker] PRIMARY KEY CLUSTERED ([Id] ASC),
CONSTRAINT [FK_Worker_Company_CompanyId] FOREIGN KEY ([CompanyId]) REFERENCES [dbo].[Company] ([Id]) ON DELETE CASCADE
);
CREATE TABLE [dbo].[User] (
[Id] INT IDENTITY (1, 1) NOT NULL,
[LoginId] NVARCHAR (20) NOT NULL,
[Password] NVARCHAR (20) NOT NULL,
CONSTRAINT [PK_User] PRIMARY KEY CLUSTERED ([Id] ASC)
);
CREATE TABLE [dbo].[Company] (
[Id] INT IDENTITY (1, 1) NOT NULL,
[Name] NVARCHAR (20) NOT NULL,
[Address] NVARCHAR (50) NOT NULL,
[Contact] NVARCHAR (12) NOT NULL,
[IsDelete] NVARCHAR (1) DEFAULT ('N') NOT NULL,
CONSTRAINT [PK_Company] PRIMARY KEY CLUSTERED ([Id] ASC)
);
CREATE TABLE [dbo].[ChangeHistory] (
[Id] INT IDENTITY (1, 1) NOT NULL,
[UserId] INT NOT NULL,
[LoginId] NVARCHAR (MAX) NOT NULL,
[CompanyId] INT NULL,
[CompanyName] NVARCHAR (MAX) NULL,
[WorkerId] INT NULL,
[WorkerName] NVARCHAR (MAX) NULL,
[ActIP] NVARCHAR (MAX) NOT NULL,
[Act] NVARCHAR (MAX) NOT NULL,
[ActDate] DATETIME2 (7) DEFAULT (sysdatetime()) NOT NULL,
CONSTRAINT [PK_ChangeHistory] PRIMARY KEY CLUSTERED ([Id] ASC)
);
다음은 sql server내에서 사용한 sql 파일을 그대로 가져왔습니다. 한 번 설명한 내용은 그 밑에선 생략하겠습니다!
drop procedure if exists CreateCompany
-- drop proceduce 는 말그대로 프로시저를 db에서 drop 시킨다(삭제한다) 이며
-- if exists를 붙이므로서 존재하면 삭제하는 기능입니다.
-- 보통 spl 파일 전체를 실행 시키므로 프로시저나 테이블생성문 앞에 붙여 사용합니다.
go -- go는 앞문장과 뒷문장을 구분지어주는 역할을 합니다.
-- CreateCompany 이라는 stored procedure를 생성합니다.
-- 변수는 @Name ~ @Act 까지 총 9개를 받아서 처리하며
-- 이 프로시저를 실행시키면
-- Company 테이블에는 @Name ~ @Address 변수를 이용하여 한개의 레코드를 insert하고
-- ChangeHistory 테이블에는 나머지 6개의 변수를 이용하여 한개의 레코드를 insert 합니다.
create procedure CreateCompany
@Name NVARCHAR (20),
@Contact NVARCHAR (12),
@Address NVARCHAR (50),
@UserId int,
@LoginId nvarchar(MAX),
@CompanyId int ,
@CompanyName nvarchar(MAX),
@ActIP nvarchar(MAX),
@Act nvarchar(1)
AS
BEGIN
insert into Company (Name, Contact, Address)
values (@Name, @Contact, @Address)
insert into ChangeHistory (UserId, LoginId, CompanyId, CompanyName, ActIP, Act)
values (@UserId, @LoginId, @CompanyId, @CompanyName, @ActIP, @Act)
END
go
-- declare는 프로시저 내에서 변수를 선언하는 문법으로 해석자체가 '선언하다'입니다.
-- @CompanyName을 선언하여
-- 바로 아래 문장인
-- select @CompanyName =Name from Company where Id = @id;
-- Company 테이블에서 Id가 @id인 레코드(Id는 Primary key 이므로 단일 레코드가 셀렉됩니다.)
-- 중 Name 컬럼의 값을 @CompanyName이라는 변수에 집어 넣는다.
-- @DorR은 Delete or Restore의 줄임말로
-- Y인 경우 Delete를(IsDelete = 'Y'),
-- N인 경우 Restore를(IsDelete = 'N')합니다.
-- 회사를 삭제하는데 회사에 worker가 있으면 worker들도 모두 IsDelete를 Y로 만드는 프로시저입니다.
-- 복구시에는 회사만 복구됩니다.
drop procedure if exists DorRCompany
go
create procedure DorRCompany
@id int,
@DorR nvarchar(1),
@UserId int,
@LoginId nvarchar(MAX),
@ActIP nvarchar(MAX),
@Act nvarchar(1)
as
begin
declare @CompanyName NVARCHAR(MAX);
select @CompanyName =Name from Company where Id = @id;
update Company set IsDelete = @DorR where Id = @id;
insert into ChangeHistory (UserId, LoginId, CompanyId, CompanyName, ActIP, Act)
values (@UserId, @LoginId, @id, @CompanyName, @ActIP, @Act)
declare @WorkerCountInCompany int;
select @WorkerCountInCompany=count(CompanyId) from Worker group by CompanyId having CompanyId = @id
if @WorkerCountInCompany > 0 and @DorR = 'Y'
begin
update Worker set IsDelete = 'Y' where CompanyId = @id
end
end
go
drop procedure if exists CreateWorker
go
create procedure CreateWorker
@CompanyId int,
@Name nvarchar(MAX),
@Email nvarchar(MAX),
@Phone nvarchar(MAX),
@UserId int,
@LoginId nvarchar(MAX),
@ActIP nvarchar(MAX)
AS
begin
declare @WorkerId int;
insert into Worker (CompanyId, Name, Email, Phone)
values (@CompanyId, @Name, @Email, @Phone);
select @WorkerId =Max(Id) from Worker;
insert into ChangeHistory (UserId, LoginId, WorkerId, WorkerName, ActIP, Act)
values (@UserId, @LoginId, @WorkerId, @Name, @ActIP, 'C')
end
go
drop procedure if exists DorRWorker
go
create procedure DorRWorker
@id int,
@DorR nvarchar(1),
@UserId int,
@LoginId nvarchar(MAX),
@ActIP nvarchar(MAX),
@Act nvarchar(1)
as
begin
declare @WorkerName NVARCHAR(MAX);
select @WorkerName =Name from Worker where Id = @id;
update Worker set IsDelete = @DorR where Id = @id;
insert into ChangeHistory (UserId, LoginId, WorkerId, WorkerName, ActIP, Act)
values (@UserId, @LoginId, @id, @WorkerName, @ActIP, @Act)
end
다음은 실행방법 예제입니다.
본문 제일 상단에서 설명한 실행환경에서 사용했고 Controller 중 메소드 하나를 통째로 가져왔습니다.
var 선언할변수명 = new SqlParameter(쿼리에서쓰이는변수명, 값) 으로 선언한 값을
여기서 Database는 정말로 데이터 베이스를 가리키는 말로 보통의 Context뒤에 오는 테이블명과는 다릅니다.
프로시저 실행 쿼리문은 하단 예제에 query를 참고하시면 됩니다.
SqlParameter()로 선언된 변수들은 변수명.SqlValue로 해당변수의 값을 뽑아내서 사용하는 것이 가능합니다.
참고 ) SqlParameter() 대신 일반 변수에 담아 $"{변수명}"으로 사용하는 것이 불가능 하지않으나 동적인 부분에서는 안정성이 떨어진다는 경고 메시지가 나옵니다. 처음 접하시는분들은 이 방법으로도 한번 해보시고 메세지를 직접 보고 어떨때 예상과 결과값이 다르게 나오는 것인지 확인해보는 것도 좋을 것 같습니다.
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Create([Bind("Id,CompanyId,Name,Email,Phone, Company")] Worker worker)
{
int userId = HttpContext.Session.GetInt32("userId") ?? 0;
string? userLoginId = HttpContext.Session.GetString("userLoginId");
if (userId != 0 && userLoginId != null)
{
var p1 = new SqlParameter("@CompanyId", worker.CompanyId);
var p2 = new SqlParameter("@Name", worker.Name);
var p3 = new SqlParameter("@Email", worker.Email);
var p4 = new SqlParameter("@Phone", worker.Phone);
var p5 = new SqlParameter("@UserId", userId);
var p6 = new SqlParameter("@LoginId", userLoginId);
var p7 = new SqlParameter("@ActIP", HttpContext.Connection.RemoteIpAddress?.ToString() ?? string.Empty);
string query = @"EXEC CreateWorker @CompanyId, @Name, @Email, @Phone, @UserId, @LoginId, @ActIP";
await _context.Database.ExecuteSqlRawAsync(query, p1, p2, p3, p4, p5, p6, p7);
_logger.Log(LogLevel.Information,
$"delete company => UserId = {p5.SqlValue}, UserLoginId = {p6.SqlValue}, " +
$"target = Worker, CompanyId ={p1.SqlValue}, WorkerName = {p2.SqlValue} IP = {p7.SqlValue}, time = {DateTime.Now}");
await _context.SaveChangesAsync();
return RedirectToAction(nameof(Index));
}
ViewData["CompanyId"] = new SelectList(_context.Company, "Id", "Address", worker.CompanyId);
return View(worker);
}