.Net 6.0 の Core WCFから、Dapper+Microsoft.Data.SqlClient NuGetパッケージを使い、SQL ServerデータベースへDBトランザクション処理を行うサンプルを作成しました。
ORマッピングの主流はDapperになって来てる。
ソースコードはGitHubで公開しています。
Youtube動画だと、DBトランザクション継続中に SQL Server Management Studio側からの接続に対してレスポンスが返って来ないのは、SQL Server側でロックが発生している為です。
ビジネスロジックやデータベース処理を、Repogitoryや Interfaceを駆使して実装すると、ソースコードが複雑になり保守性が下がります。
staticクラスで実装するとソースコードがシンプルになり保守性が高くなります。
staticクラスで実装したソースコードは、ユニットテストの実装も容易です。
Repogitoryや Interfaceを駆使して無駄な処理を大量に実装し、工数が爆発して開発が終わらなくなっているプロジェクトが多過ぎる。駆使するべきはstatic。
いまだに、C#のマルチスレッド処理が分からず「staticを使うのは不安だ」と言っている人がいて、その人の意見が採用され「static禁止」になると、開発工数が爆発します。
Core WCF サーバ側
ソースコード構成
.NET6.0で実装する Core WCF 開発手順 で作成したソースコードを元に、一部修正を加えています。
ソースコード変更内容を解説
/CoreWCFService1.csproj
Dapper/Microsoft.Data.SqlClient NuGetパッケージをインストール。
SQLSeverへ接続するライブラリは、System.Data.SqlClientではなく Microsoft.Data.SqlClientが推奨です。
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="Microsoft.Data.SqlClient" Version="5.1.0" /> </ItemGroup> </Project> |
/appsettings.Development.json
SQLServerへ接続するコネクションストリングを追加。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
{ "Logging": { "LogLevel": { "Default": "Information", "Microsoft.AspNetCore": "Warning" } }, "ConnectionStrings": { "SQLServerDB": "Data Source=localhost;Initial Catalog=TestDb;Integrated Security=True;Connect Timeout=30;Encrypt=False;TrustServerCertificate=False;ApplicationIntent=ReadWrite;MultiSubnetFailover=False" } } |
/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> /// SQLServer 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")["SQLServerDB"]; 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
・SQLServerから取得したデータに対応する、データモデルクラスを追加。
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 TableA { public long Id { get; set; } public string Code { get; set; } public string? ValueString { get; set; } public DateTime? ValueDate { get; set; } } } |
/IService.cs
・SQLServerデータベースに対して、DBトランザクション処理を実行する新しいインターフェース(InsertTableA)を追加。
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 InsertTableA(TableA tableA); [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 InsertTableA(TableA tableA) { SQL_TableA.Transaction1(tableA); } 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_TableA.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 82 |
using System.Data; using CoreWCFService1.Model; using CoreWCFService1.Shared; using Dapper; using Microsoft.Extensions.Logging; using Microsoft.Data.SqlClient; namespace CoreWCFService1.SQL { public class SQL_TableA { public static void Transaction1(TableA insertTableA) { using (var conn = new SqlConnection(SharedData.ConnectionString)) { conn.Open(); using (var tran = conn.BeginTransaction(IsolationLevel.ReadCommitted)) { try { var tableAList = Select(conn, tran); Insert(conn, tran, insertTableA); var tableAList2 = Select(conn, tran); tran.Commit(); } catch(Exception ex) { tran.Rollback(); throw; } } } } public static IEnumerable<TableA> Select(SqlConnection conn, SqlTransaction tran) { const string sql = " SELECT " + " Id, " + " Code, " + " ValueString, " + " ValueDate " + " FROM " + " dbo.TableA "; return conn.Query<TableA>(sql, null, tran); } public static void Insert(SqlConnection conn, SqlTransaction tran, TableA insertTableA) { const string sql = " INSERT INTO dbo.TableA( " + " Id, " + " Code, " + " ValueString, " + " ValueDate " + " ) VALUES ( " + " @id, " + " @code, " + " @valueString, " + " @valueDate " + " ); "; var sqlParam = new { id = insertTableA.Id, code = insertTableA.Code, valueString = insertTableA.ValueString, valueDate = insertTableA.ValueDate }; conn.Execute(sql, sqlParam, tran); } } } |
Core WCF クライアント側
ソースコード構成
.NET6.0で実装する Core WCF 開発手順 のソースコードを元に、一部修正を加えています。
ソースコード変更内容を解説
/Connected Services/ServiceReference1/ConnectedService.json
CoreWCFサーバ側で追加したインターフェースを、CoreWCFクライアント側へ反映したことに伴い自動更新された。必要なパッケージが増えている。
※CoreWCFサーバ側の変更を、CoreWCFクライアント側へ反映する手順は .NET6.0で実装する Core WCF 開発手順 > 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 |
{ "ExtendedData": { "inputs": [ "https://localhost:7086/Service.svc?singleWsdl" ], "collectionTypes": [ "System.Array", "System.Collections.Generic.Dictionary`2" ], "namespaceMappings": [ "*, ServiceReference1" ], "references": [ "Microsoft.Bcl.AsyncInterfaces, {Microsoft.Bcl.AsyncInterfaces, 5.0.0}", "Microsoft.Extensions.ObjectPool, {Microsoft.Extensions.ObjectPool, 5.0.10}", "Microsoft.IdentityModel.Logging, {Microsoft.IdentityModel.Logging, 6.8.0}", "Microsoft.IdentityModel.Protocols.WsTrust, {Microsoft.IdentityModel.Protocols.WsTrust, 6.8.0}", "Microsoft.IdentityModel.Tokens, {Microsoft.IdentityModel.Tokens, 6.8.0}", "Microsoft.IdentityModel.Tokens.Saml, {Microsoft.IdentityModel.Tokens.Saml, 6.8.0}", "Microsoft.IdentityModel.Xml, {Microsoft.IdentityModel.Xml, 6.8.0}", "System.Drawing.Common, {System.Drawing.Common, 5.0.0}", "System.IO, {System.IO, 4.3.0}", "System.Reflection.DispatchProxy, {System.Reflection.DispatchProxy, 4.7.1}", "System.Runtime, {System.Runtime, 4.3.0}", "System.Security.AccessControl, {System.Security.AccessControl, 5.0.0}", "System.Security.Cryptography.Cng, {System.Security.Cryptography.Cng, 5.0.0}", "System.Security.Cryptography.Xml, {System.Security.Cryptography.Xml, 5.0.0}", "System.Security.Permissions, {System.Security.Permissions, 5.0.0}", "System.Security.Principal.Windows, {System.Security.Principal.Windows, 5.0.0}", "System.ServiceModel, {System.ServiceModel.Primitives, 4.10.0}", "System.ServiceModel.Duplex, {System.ServiceModel.Duplex, 4.10.0}", "System.ServiceModel.Federation, {System.ServiceModel.Federation, 4.10.0}", "System.ServiceModel.Http, {System.ServiceModel.Http, 4.10.0}", "System.ServiceModel.NetTcp, {System.ServiceModel.NetTcp, 4.10.0}", "System.ServiceModel.Primitives, {System.ServiceModel.Primitives, 4.10.0}", "System.ServiceModel.Security, {System.ServiceModel.Security, 4.10.0}", "System.Text.Encoding, {System.Text.Encoding, 4.3.0}", "System.Threading.Tasks, {System.Threading.Tasks, 4.3.0}", "System.Windows.Extensions, {System.Windows.Extensions, 5.0.0}", "System.Xml.ReaderWriter, {System.Xml.ReaderWriter, 4.3.0}", "System.Xml.XmlDocument, {System.Xml.XmlDocument, 4.3.0}" ], "targetFramework": "net6.0", "typeReuseMode": "All" } } |
/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="TableA", Namespace="http://schemas.datacontract.org/2004/07/CoreWCFService1.Model")] public partial class TableA : object { private string CodeField; private long IdField; private System.Nullable<System.DateTime> ValueDateField; private string ValueStringField; [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> ValueDate { get { return this.ValueDateField; } set { this.ValueDateField = value; } } [System.Runtime.Serialization.DataMemberAttribute()] public string ValueString { get { return this.ValueStringField; } set { this.ValueStringField = 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/InsertTableA", ReplyAction="http://tempuri.org/IService/InsertTableAResponse")] System.Threading.Tasks.Task InsertTableAAsync(ServiceReference1.TableA tableA); [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 InsertTableAAsync(ServiceReference1.TableA tableA) { return base.Channel.InsertTableAAsync(tableA); } 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
ボタンクリック時の処理を、CoreWC