.Net 6.0 の Core WCFから、Dapper+Oracle.EntityFrameworkCore NuGetパッケージを使い、SQLServerデータベースへDBトランザクション処理を行うサンプルを作成しました。
ORマッピングの主流はDapperになって来てる。
ソースコードはGitHubで公開しています。
ビジネスロジックやデータベース処理を、Repogitoryや Interfaceを駆使して実装すると、ソースコードが複雑になり保守性が下がります。
staticクラスで実装するとソースコードがシンプルになり保守性が高くなります。
staticクラスで実装したソースコードは、ユニットテストの実装も容易です。
Repogitoryや Interfaceを駆使して無駄な処理を大量に実装し、工数が爆発して開発が終わらなくなっているプロジェクトが多過ぎる。駆使するべきはstatic。
いまだに、C#のマルチスレッド処理が分からず「staticを使うのは不安だ」と言っている人がいて、その人の意見が採用され「static禁止」になると、開発工数が爆発します。
Core WCF サーバ側
ソースコード構成
.NET6.0で実装する Core WCF 開発手順 で作成したソースコードを元に、一部修正を加えています。
ソースコード変更内容を解説
/CoreWCFService1.csproj
Dapper/Oracle.EntityFrameworkCore NuGetパッケージをインストール。
Oracle.EntityFrameworkCore は Oracle.ManagedDataAccess と違い、ORACLE Client を別途インストールしなくても、OracleConnectionを実行できるので便利。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
<Project Sdk="Microsoft.NET.Sdk.Web"> <PropertyGroup> <TargetFramework>net6.0</TargetFramework> <Nullable>enable</Nullable> <ImplicitUsings>true</ImplicitUsings> </PropertyGroup> <ItemGroup> <Using Include="CoreWCF" /> <Using Include="CoreWCF.Configuration" /> <Using Include="CoreWCF.Channels" /> <Using Include="CoreWCF.Description" /> <Using Include="System.Runtime.Serialization " /> <Using Include="CoreWCFService1" /> <Using Include="Microsoft.Extensions.DependencyInjection.Extensions" /> </ItemGroup> <ItemGroup> <PackageReference Include="CoreWCF.Primitives" Version="1.*" /> <PackageReference Include="CoreWCF.Http" Version="1.*" /> <PackageReference Include="Dapper" Version="2.0.123" /> <PackageReference Include="Oracle.EntityFrameworkCore" Version="7.21.9" /> </ItemGroup> </Project> |
/appsettings.Development.json
ORACLEへ接続するコネクションストリングを追加。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
{ "Logging": { "LogLevel": { "Default": "Information", "Microsoft.AspNetCore": "Warning" } }, "ConnectionStrings": { "OracleDB": "User Id = TESTUSER1; Password = testuser1password; Data Source = (DESCRIPTION = (ADDRESS = (PROTOCOL = tcp)(HOST = LOCALHOST)(PORT = 1521))(CONNECT_DATA = (SERVICE_NAME = ORCL1)));" } } |
/Shared/SharedData.cs
・staticメンバ変数で実装した、アプリ全体で使えるコネクションストリング変数を追加。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
namespace CoreWCFService1.Shared { public static class SharedData { /// <summary> /// ORACLE DB 接続文字列 /// </summary> public static string ConnectionString; } } |
/Program.cs
・アプリ起動時に環境設定ファイルを読み込み、アプリ全体で使えるコネクションストリング変数にセットする処理を追加。
環境設定ファイルの読み込み処理をアプリ起動時1回だけにし、アプリ起動後は staticメンバ変数を直接参照することで、処理コストを減らせる。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
using CoreWCFService1.Shared; var builder = WebApplication.CreateBuilder(); var configuration = builder.Configuration; SharedData.ConnectionString = configuration.GetSection("ConnectionStrings")["OracleDB"]; builder.Services.AddServiceModelServices(); builder.Services.AddServiceModelMetadata(); builder.Services.AddSingleton<IServiceBehavior, UseRequestHeadersForMetadataAddressBehavior>(); var app = builder.Build(); app.UseServiceModel(serviceBuilder => { serviceBuilder.AddService<Service>(); serviceBuilder.AddServiceEndpoint<Service, IService>(new BasicHttpBinding(BasicHttpSecurityMode.Transport), "/Service.svc"); var serviceMetadataBehavior = app.Services.GetRequiredService<ServiceMetadataBehavior>(); serviceMetadataBehavior.HttpsGetEnabled = true; }); app.Run(); |
/Model/TableModels.cs
・ORACLEから取得したデータに対応する、データモデルクラスを追加。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
using static System.Net.Mime.MediaTypeNames; namespace CoreWCFService1.Model { public class TABLE1 { public long ID; public string CODE; public string? VALUE_STRING; public DateTime? VALUE_DATE; } } |
/IService.cs
・ORACLEデータベースに対して、DBトランザクション処理を実行する新しいインターフェース(InsertTABLE1)を追加。
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 |
using CoreWCF; using CoreWCFService1.Model; using CoreWCFService1.SQL; using System; using System.Runtime.Serialization; namespace CoreWCFService1 { [ServiceContract] public interface IService { [OperationContract] string GetData(int value); [OperationContract] void InsertTABLE1(TABLE1 TABLE1); [OperationContract] CompositeType GetDataUsingDataContract(CompositeType composite); } public class Service : IService { public string GetData(int value) { var msg = string.Format("You entered: {0}", value); return msg; } public void InsertTABLE1(TABLE1 TABLE1) { SQL_TABLE1.Transaction1(TABLE1); } public CompositeType GetDataUsingDataContract(CompositeType composite) { if (composite == null) { throw new ArgumentNullException("composite"); } if (composite.BoolValue) { composite.StringValue += "Suffix"; } return composite; } } // Use a data contract as illustrated in the sample below to add composite types to service operations. [DataContract] public class CompositeType { bool boolValue = true; string stringValue = "Hello "; [DataMember] public bool BoolValue { get { return boolValue; } set { boolValue = value; } } [DataMember] public string StringValue { get { return stringValue; } set { stringValue = value; } } } } |
/SQL/SQL_TABLE1.cs
・DBトランザクション処理を行う Transaction1メソッドから、Insert SQLを発行する Insertメソッド、Select SQLを発行する Selectメソッドを呼び出しています。
・BeginTransactionでトランザクションを開始し、DBコネクション変数(conn)とトランザクション変数(tran)を各メソッドへ渡し、Dapperを通してSQLを発行することで、とてもシンプルで高速なDBトランザクション処理を実装しています。
・razorページには画面処理のみ、SQLフォルダ配下の staticクラスにはDB処理と、レイヤー分けをしています。
今回は簡単なロジックなので2レイヤーにしています。業務システムだとビジネスロジックレイヤーのソースコードをDataフォルダ配下に纏め、「画面レイヤー(Pagesフォルダ) => ビジネスロジックレイヤー(Dataフォルダ) => データベースレイヤー(Sqlフォルダ)」と繋げていくイメージです。
ビジネスロジックレイヤーを設ける場合、トランザクション処理の開始処理(BeginTransaction)はビジネスロジックレイヤーに実装します。
・データベース処理を行う、SQL文を伴うソースコードはSQLフォルダに纏まっていると分かり易い。
・Insert文のパラメータは匿名クラスにし、DapperにSQLとパラメータを渡して、Insert SQLを発行しています。
・データベースとC#側のデータ型が一致していれば、あとは Dapperがバインドしてくれる。
・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 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 |
using System.Data; using CoreWCFService1.Model; using CoreWCFService1.Shared; using Dapper; using Microsoft.Extensions.Logging; using Oracle.ManagedDataAccess.Client; namespace CoreWCFService1.SQL { public class SQL_TABLE1 { public static void Transaction1(TABLE1 insertTABLE1) { using (var conn = new OracleConnection(SharedData.ConnectionString)) { conn.Open(); using (var tran = conn.BeginTransaction(IsolationLevel.ReadCommitted)) { try { var table1List = Select(conn); Insert(conn, tran, insertTABLE1); var table1List2 = Select(conn); tran.Commit(); } catch { tran.Rollback(); throw; } } } } public static IEnumerable<TABLE1> Select(OracleConnection conn) { const string sql = " SELECT " + " id, " + " code, " + " value_string, " + " value_date " + " FROM " + " testuser1.table1 "; return conn.Query<TABLE1>(sql); } public static void Insert(OracleConnection conn, OracleTransaction tran, TABLE1 insertTABLE1) { const string sql = " INSERT INTO testuser1.table1( " + " id, " + " value_string, " + " value_date, " + " code " + " ) VALUES ( " + " :id, " + " :value_string, " + " :value_date, " + " :code " + " ) "; var sqlParam = new { id = insertTABLE1.ID, code = insertTABLE1.CODE, value_string = insertTABLE1.VALUE_STRING, value_date = insertTABLE1.VALUE_DATE }; conn.Execute(sql, sqlParam, tran); } } } |
Core WCF クライアント側
ソースコード構成
.NET6.0で実装する Core WCF 開発手順 のソースコードを元に、一部修正を加えています。
ソースコード変更内容を解説
/Connected Services/ServiceReference1/Reference.cs
CoreWCFサーバ側で追加したインターフェースを、CoreWCFクライアント側へ反映したことに伴い自動更新された。
追加したインターフェースのパラメータで使われる、データクラスの定義が増えている。
追加したインターフェースの定義が増えている。
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 |
//------------------------------------------------------------------------------ // <auto-generated> // このコードはツールによって生成されました。 // // このファイルへの変更は、正しくない動作の原因になる可能性があり、 // コードが再生成されると失われます。 // </auto-generated> //------------------------------------------------------------------------------ namespace ServiceReference1 { using System.Runtime.Serialization; [System.Diagnostics.DebuggerStepThroughAttribute()] [System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Tools.ServiceModel.Svcutil", "2.1.0")] [System.Runtime.Serialization.DataContractAttribute(Name="TABLE1", Namespace="http://schemas.datacontract.org/2004/07/CoreWCFService1.Model")] public partial class TABLE1 : object { private string CODEField; private long IDField; private System.Nullable<System.DateTime> VALUE_DATEField; private string VALUE_STRINGField; [System.Runtime.Serialization.DataMemberAttribute()] public string CODE { get { return this.CODEField; } set { this.CODEField = value; } } [System.Runtime.Serialization.DataMemberAttribute()] public long ID { get { return this.IDField; } set { this.IDField = value; } } [System.Runtime.Serialization.DataMemberAttribute()] public System.Nullable<System.DateTime> VALUE_DATE { get { return this.VALUE_DATEField; } set { this.VALUE_DATEField = value; } } [System.Runtime.Serialization.DataMemberAttribute()] public string VALUE_STRING { get { return this.VALUE_STRINGField; } set { this.VALUE_STRINGField = value; } } } [System.Diagnostics.DebuggerStepThroughAttribute()] [System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Tools.ServiceModel.Svcutil", "2.1.0")] [System.Runtime.Serialization.DataContractAttribute(Name="CompositeType", Namespace="http://schemas.datacontract.org/2004/07/CoreWCFService1")] public partial class CompositeType : object { private bool BoolValueField; private string StringValueField; [System.Runtime.Serialization.DataMemberAttribute()] public bool BoolValue { get { return this.BoolValueField; } set { this.BoolValueField = value; } } [System.Runtime.Serialization.DataMemberAttribute()] public string StringValue { get { return this.StringValueField; } set { this.StringValueField = value; } } } [System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Tools.ServiceModel.Svcutil", "2.1.0")] [System.ServiceModel.ServiceContractAttribute(ConfigurationName="ServiceReference1.IService")] public interface IService { [System.ServiceModel.OperationContractAttribute(Action="http://tempuri.org/IService/GetData", ReplyAction="http://tempuri.org/IService/GetDataResponse")] System.Threading.Tasks.Task<string> GetDataAsync(int value); [System.ServiceModel.OperationContractAttribute(Action="http://tempuri.org/IService/InsertTABLE1", ReplyAction="http://tempuri.org/IService/InsertTABLE1Response")] System.Threading.Tasks.Task InsertTABLE1Async(ServiceReference1.TABLE1 TABLE1); [System.ServiceModel.OperationContractAttribute(Action="http://tempuri.org/IService/GetDataUsingDataContract", ReplyAction="http://tempuri.org/IService/GetDataUsingDataContractResponse")] System.Threading.Tasks.Task<ServiceReference1.CompositeType> GetDataUsingDataContractAsync(ServiceReference1.CompositeType composite); } [System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Tools.ServiceModel.Svcutil", "2.1.0")] public interface IServiceChannel : ServiceReference1.IService, System.ServiceModel.IClientChannel { } [System.Diagnostics.DebuggerStepThroughAttribute()] [System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Tools.ServiceModel.Svcutil", "2.1.0")] public partial class ServiceClient : System.ServiceModel.ClientBase<ServiceReference1.IService>, ServiceReference1.IService { /// <summary> /// サービス エンドポイントを構成するには、この部分メソッドを実装してください。 /// </summary> /// <param name="serviceEndpoint">構成するエンドポイント</param> /// <param name="clientCredentials">クライアントの資格情報です</param> static partial void ConfigureEndpoint(System.ServiceModel.Description.ServiceEndpoint serviceEndpoint, System.ServiceModel.Description.ClientCredentials clientCredentials); public ServiceClient() : base(ServiceClient.GetDefaultBinding(), ServiceClient.GetDefaultEndpointAddress()) { this.Endpoint.Name = EndpointConfiguration.BasicHttpBinding_IService.ToString(); ConfigureEndpoint(this.Endpoint, this.ClientCredentials); } public ServiceClient(EndpointConfiguration endpointConfiguration) : base(ServiceClient.GetBindingForEndpoint(endpointConfiguration), ServiceClient.GetEndpointAddress(endpointConfiguration)) { this.Endpoint.Name = endpointConfiguration.ToString(); ConfigureEndpoint(this.Endpoint, this.ClientCredentials); } public ServiceClient(EndpointConfiguration endpointConfiguration, string remoteAddress) : base(ServiceClient.GetBindingForEndpoint(endpointConfiguration), new System.ServiceModel.EndpointAddress(remoteAddress)) { this.Endpoint.Name = endpointConfiguration.ToString(); ConfigureEndpoint(this.Endpoint, this.ClientCredentials); } public ServiceClient(EndpointConfiguration endpointConfiguration, System.ServiceModel.EndpointAddress remoteAddress) : base(ServiceClient.GetBindingForEndpoint(endpointConfiguration), remoteAddress) { this.Endpoint.Name = endpointConfiguration.ToString(); ConfigureEndpoint(this.Endpoint, this.ClientCredentials); } public ServiceClient(System.ServiceModel.Channels.Binding binding, System.ServiceModel.EndpointAddress remoteAddress) : base(binding, remoteAddress) { } public System.Threading.Tasks.Task<string> GetDataAsync(int value) { return base.Channel.GetDataAsync(value); } public System.Threading.Tasks.Task InsertTABLE1Async(ServiceReference1.TABLE1 TABLE1) { return base.Channel.InsertTABLE1Async(TABLE1); } public System.Threading.Tasks.Task<ServiceReference1.CompositeType> GetDataUsingDataContractAsync(ServiceReference1.CompositeType composite) { return base.Channel.GetDataUsingDataContractAsync(composite); } public virtual System.Threading.Tasks.Task OpenAsync() { return System.Threading.Tasks.Task.Factory.FromAsync(((System.ServiceModel.ICommunicationObject)(this)).BeginOpen(null, null), new System.Action<System.IAsyncResult>(((System.ServiceModel.ICommunicationObject)(this)).EndOpen)); } private static System.ServiceModel.Channels.Binding GetBindingForEndpoint(EndpointConfiguration endpointConfiguration) { if ((endpointConfiguration == EndpointConfiguration.BasicHttpBinding_IService)) { System.ServiceModel.BasicHttpBinding result = new System.ServiceModel.BasicHttpBinding(); result.MaxBufferSize = int.MaxValue; result.ReaderQuotas = System.Xml.XmlDictionaryReaderQuotas.Max; result.MaxReceivedMessageSize = int.MaxValue; result.AllowCookies = true; result.Security.Mode = System.ServiceModel.BasicHttpSecurityMode.Transport; return result; } throw new System.InvalidOperationException(string.Format("名前 \'{0}\' のエンドポイントは見つかりません。", endpointConfiguration)); } private static System.ServiceModel.EndpointAddress GetEndpointAddress(EndpointConfiguration endpointConfiguration) { if ((endpointConfiguration == EndpointConfiguration.BasicHttpBinding_IService)) { return new System.ServiceModel.EndpointAddress("https://localhost:7086/Service.svc"); } throw new System.InvalidOperationException(string.Format("名前 \'{0}\' のエンドポイントは見つかりません。", endpointConfiguration)); } private static System.ServiceModel.Channels.Binding GetDefaultBinding() { return ServiceClient.GetBindingForEndpoint(EndpointConfiguration.BasicHttpBinding_IService); } private static System.ServiceModel.EndpointAddress GetDefaultEndpointAddress() { return ServiceClient.GetEndpointAddress(EndpointConfiguration.BasicHttpBinding_IService); } public enum EndpointConfiguration { BasicHttpBinding_IService, } } } |
/Form1.cs
ボタンクリック時の処理を、CoreWCFサーバ側に定義されたデータクラスを元にInsert用のデータを作成し、CoreWCFサーバ側で追加したDBトランザクションを行うインターフェースへ渡す処理に変更。
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 WinFormsApp1 { public partial class Form1 : Form { public Form1() { InitializeComponent(); } private async void button1_Click(object sender, EventArgs e) { var insertTableA = new ServiceReference1.TABLE1() { ID = 1, CODE = "2", VALUE_STRING = "3", VALUE_DATE = DateTime.Now }; var client = new ServiceReference1.ServiceClient(); //デフォルトの1分で Timeoutだと DB処理中に Timeoutすることが多かったので 10分に変更。 client.Endpoint.Binding.SendTimeout = new TimeSpan(0, 10, 0); await client.InsertTABLE1Async(insertTableA); MessageBox.Show("完了"); } } } |
Select対象のDBテーブル構成
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 |
-------------------------------------------------------- -- DDL for Table TABLE1 -------------------------------------------------------- CREATE TABLE "TESTUSER1"."TABLE1" ( "ID" NUMBER, "CODE" VARCHAR2(20 BYTE), "VALUE_STRING" NVARCHAR2(20), "VALUE_DATE" DATE ) SEGMENT CREATION IMMEDIATE PCTFREE 10 PCTUSED 40 INITRANS 1 MAXTRANS 255 NOCOMPRESS LOGGING STORAGE(INITIAL 65536 NEXT 1048576 MINEXTENTS 1 MAXEXTENTS 2147483645 PCTINCREASE 0 FREELISTS 1 FREELIST GROUPS 1 BUFFER_POOL DEFAULT FLASH_CACHE DEFAULT CELL_FLASH_CACHE DEFAULT) TABLESPACE "TABLESPACE1" ; -------------------------------------------------------- -- DDL for Index TABLE1_PK -------------------------------------------------------- CREATE UNIQUE INDEX "TESTUSER1"."TABLE1_PK" ON "TESTUSER1"."TABLE1" ("ID") PCTFREE 10 INITRANS 2 MAXTRANS 255 COMPUTE STATISTICS STORAGE(INITIAL 65536 NEXT 1048576 MINEXTENTS 1 MAXEXTENTS 2147483645 PCTINCREASE 0 FREELISTS 1 FREELIST GROUPS 1 BUFFER_POOL DEFAULT FLASH_CACHE DEFAULT CELL_FLASH_CACHE DEFAULT) TABLESPACE "TABLESPACE1" ; -------------------------------------------------------- -- Constraints for Table TABLE1 -------------------------------------------------------- ALTER TABLE "TESTUSER1"."TABLE1" ADD CONSTRAINT "TABLE1_PK" PRIMARY KEY ("ID") USING INDEX PCTFREE 10 INITRANS 2 MAXTRANS 255 COMPUTE STATISTICS STORAGE(INITIAL 65536 NEXT 1048576 MINEXTENTS 1 MAXEXTENTS 2147483645 PCTINCREASE 0 FREELISTS 1 FREELIST GROUPS 1 BUFFER_POOL DEFAULT FLASH_CACHE DEFAULT CELL_FLASH_CACHE DEFAULT) TABLESPACE "TABLESPACE1" ENABLE; ALTER TABLE "TESTUSER1"."TABLE1" MODIFY ("ID" NOT NULL ENABLE); |