C#でDbGeographyに定義したPolygonの巻きを補正する

こんにちは。beaglesoftの真鍋です。

SQL ServerでGeographyのポリゴンを扱う場合には右巻/左巻で世界が一変するわけで、結構重要です。普通はSQL Serverで以下のクエリを発行すればよしなに行ってくれます。

-- geomがポリゴンを保存しているGeeography型の列

-- 巻きが逆の場合に補正します
update `テーブル名` set [geom] = [geom].MakeValid().ReorientObject() where [geom].MakeValid().EnvelopeAngle() > 90

-- 有効なインスタンスに補正します
update `テーブル名` set [geom] = [geom].MakeValid() where [geom].STIsValid() = 0

ただ、EntityFrameworkを使っているとこの処理だけSQLを実行するのも手間です。そこで、DbGeography型で対応できないかと調べたのですが、現在のところDbGeometory型に変換してからでないと処理ができないことがわかりました。

うーんと思っていたところ、どうやら以下の方法で同じことができることがわかりました。

/// <summary>
/// 経度 緯度,経度 緯度...の文字列からポリゴンを生成します。
/// </summary>
/// <param name="polygonText">経度 緯度,経度 緯度...の文字列</param>
/// <returns>DbGeoGraphyのインスタンス</returns>
public static DbGeography ConvertToPolygon(string polygonText)
{
    if (string.IsNullOrEmpty(polygonText))
    {
        throw new ArgumentNullException();
    }

    var sqlGeography = SqlGeography.STGeomFromText(new SqlChars($"POLYGON(({polygonText}))"), 4326).MakeValid();
    if (sqlGeography.EnvelopeAngle() > 90)
    {
        sqlGeography = sqlGeography.ReorientObject();
    }

    return DbSpatialServices.Default.GeographyFromProviderValue(sqlGeography);
}

行っていることはSQLと同じですので省略しますが、これは便利です。SqlGeographyはMicrosoft.SqlServer.Types 名前空間に定義されているクラスで、DbGeographyよりも多くの処理が定義されているようです。もともとWKTを介してデータの変換は DbGeometory⇔DbGeography と行えるのですが、こっちはより便利ですね。

参考にしたサイト

How to correct Polygon Ring Orientation using C# Entity Framework 5 DbGeography Spatial Data - Stack Overflow

DbGeography Reverse Polygon Point Order (Ring Orientation) With Entity Framework 5, WKTString and MSSQL 2008 | Tales of a White Robe

SqlServerSpatial110.dllが見つかりません

こんにちは。beaglesoftの真鍋です。

SQL ServerからGeography型のデータを取得するときに、「SqlServerSpatial110.dllが見つかりません」というエラーが出ることがあります。最初は何のことかさっぱりわからなかったのですが、いろいろと調べたところDbGeoGraphy型が内部でSqlServerSpatial110.dllを利用しているため、あらかじめプログラムの実行時にはロードしておく必要が有るけれども、そのロードを行っていないと発生するエラーのようです。

対応方法としては次の通り対応することで利用可能になりました。

  1. NuGet Packageをインストールする。
  2. アプリケーションごとにロード方法を設定する。
  3. App.configまたはweb.configにバージョンを追加する。

NuGet Packageをインストールする

NuGet Packageのインストールは以下のとおり実行可能です。

Install-Package Microsoft.SqlServer.Types

NuGet Gallery | Microsoft.SqlServer.Types (Spatial) 11.0.2

アプリケーションごとにロード方法を設定する

NuGetのインストール時に表示されるReadmeに記述が表示されますが、以下の設定を行う必要があります。

ASP.NETアプリケーション

Global.asaxApplication_Startメソッドで以下の設定を記述する必要があります。

SqlServerTypes.Utilities.LoadNativeAssemblies(Server.MapPath("~/bin"));

Windowsアプリケーション

Windowsアプリケーションではアプリケーションの起動時に以下の設定を記述する必要があります。(厳密には、SqlServerSpatial110.dllが実行される前までに読み込まれていれば良いと記述があります。)

SqlServerTypes.Utilities.LoadNativeAssemblies(AppDomain.CurrentDomain.BaseDirectory);

ユニットテスト

ユニットでストでは、ユニットテストクラスにも上記のNuGet Packageをインストールする必要があります。また、SqlServerSpatial110.dllを利用した処理を実行するテストクラスでは以下のとおり予め読み込み処理を実行する必要があります。

[ClassInitialize]
public static void ClassInitialize(TestContext context)
{
    SqlServerTypes.Utilities.LoadNativeAssemblies(AppDomain.CurrentDomain.BaseDirectory);
}

App.configまたはweb.configにバージョンを追加する

App.configまたはweb.configに以下の通りバージョンを指定します。

<runtime>
    <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
      <dependentAssembly>
        <assemblyIdentity name="Microsoft.SqlServer.Types" publicKeyToken="89845dcd8080cc91" culture="neutral" />
        <bindingRedirect oldVersion="10.0.0.0" newVersion="11.0.0.0" />
      </dependentAssembly>
     ...

参考にしたサイト

.net - SqlServerSpatial110.dll failed to copy on Windows Azure deployment - Stack Overflow .net - SqlServerSpatial110.dll failed to copy on Windows Azure deployment - Stack Overflow Microsoft.SqlServer.Types NuGet Package (Spatial on Azure) - ADO.NET Blog - Site Home - MSDN Blogs

IValidatableObjectのValidateメソッドへDIを行う

このエントリーはASP.NET MVC5に対応しています。ASP.NET MVC Core2に対応した記事はこちらになります。

blog.beaglesoft.net

こんにちは。beaglesoftの真鍋です。

ASP.NET MVC5でモデルの検証を行うときにIValidatableObjectを実装してValidateで検証処理を行うことがあると思います。たとえば、対象のモデルの複数項目について検証を行う場合などが多いのではないでしょうか。

プログラミングMicrosoft ASP.NET MVC 第3版ASP.NET MVC 5 対応版 (マイクロソフト公式解説書)でも、IValidatableObjectインターフェースということで説明があります。

今回はこの検証でRepositoryやServiceオブジェクトなどを利用する方法を試してみました。

IValidatableObjectをそのまま利用する

IValidatableObjectを実装したクラスでは、Validateメソッドでの処理を行います。この検証処理でたとえばRepositoryやRepositoryを利用するServiceを利用したい場合、DIを利用しない場合には以下のようなコードとなります。

public class CompanyViewModel : IValidatableObject
{
    private static readonly Logger Logger = LogManager.GetCurrentClassLogger();

    public long Id { get; set; }

    /// <summary>
    ///     会社名
    /// </summary>
    [StringLength(50)]
    [Display(Name = "会社名")]
    public string Name { get; set; }
    ...
    public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
    {
        using (var dataContext = new DataContext())
        {
            var fooRepository = new FooRepository(dataContext);
            var fooService = new FooService(fooRepository);
        ...
        }
    }
}

これはこれで正しく動作しますが、テストがとても難しくなります。Validateメソッドの内容にも寄るのでしょうが、検証ロジックは基本的にテストを行いたい対象です。そのため、テスタビリティが低いことはできるだけさけたいですよね。

一般的にテストを行うときにはMoqを利用してInterfaceに定義されたメソッドを置き換えていきます。そうすることで、テストの関心事項である処理フローに集中できるからですね。

対応した方法

それではどのように対応したかです。目的はIValidatableObject.ValidateでRepositoryやServiceのインスタンスを受け取ることです。簡単に言うと、Validateではnew FooRepository()とかしないと言うことです。

次に手順です。わかりづらいですが、とりあえず試してみると理解できると思います。

  1. IServiceProviderとIDependencyResolverを実装したServiceProviderを実装する。
  2. ValidatableObjectAdapterを実装したValidatableObjectAdapterを作成して、1で作成したServiceProviderをコンストラクタで設定する。
  3. UnityなどDIコンテナでDataAnnotationsModelValidatorProviderに2で作成したValidatableObjectAdapterを登録します。

IServiceProviderとIDependencyResolverを実装したServiceProviderを実装する

IServiceProviderIServiceProvider インターフェイス (System).aspx)で以下の通り説明されています。

サービス オブジェクト、つまり、他のオブジェクトにカスタム サポートを提供するオブジェクトを取得するための機構を定義します。

いまいちわかりづらいですが、他のオブジェクトに何かしらのオブジェクトを提供するために利用するものです。なぜ唐突にIServiceProviderが登場するかというと、IValidatableObjectに関連します。

IValidatableObjectValidateでは引数としてValidationContextを受け取ります。このValidationContextには GetServiceが定義されており、ここから今回取得したいServiceやRepositoryなどを取得するのですが、このGetServiceで取得できる値はValidationContextのコンストラクタで指定したIServiceProviderとなります。

このため、ValidationContextIServiceProviderを実装したクラスを渡したいわけです。

IServiceProviderを実装したCustomServiceProviderクラスは次の通りとなります。

public class CustomServiceProvider: IDependencyResolver, IServiceProvider
{

    private readonly IFooService _fooService;

    public CustomServiceProvider(IFooService fooService)
    {
        _fooService = fooService;
    }

    public object GetService(Type serviceType)
    {
       return _fooService;
    }

    public IEnumerable<object> GetServices(Type serviceType)
    {
        return Enumerable.Empty<object>();
    }
}

ValidatableObjectAdapterを実装したValidatableObjectAdapterを作成して、1で作成したServiceProviderをコンストラクタで設定する

次に先ほど定義したCustomServiceProviderを受け取りValidationContextを生成するValidatableObjectAdapterを作成します。これはValidationContextを生成するための手順と考えればいいと思います。(ちょっと自信がないですが…)

public class CustomValidatableObjectAdapter : ValidatableObjectAdapter
{
    private readonly IServiceProvider _serviceProvider;
        
        // note:コンストラクタでserviceProviderを設定してインスタンス変数に保持する
    public CustomValidatableObjectAdapter(IServiceProvider serviceProvider, ModelMetadata metadata, ControllerContext context)
        : base(metadata, context)
    {
        this._serviceProvider = serviceProvider;
    }

    public override IEnumerable<ModelValidationResult> Validate(object container)
    {
        var model = Metadata.Model;
        if (model == null)
        {
            return Enumerable.Empty<ModelValidationResult>();
        }

        var validatable = model as IValidatableObject;
        if (validatable == null)
        {
            throw new InvalidOperationException();
        }
        
            // note:ここでValidationContextを生成してコンストラクタで受け取った_serviceProviderを設定する。
        var validationContext = new ValidationContext(validatable, _serviceProvider, null);
        return ConvertResults(validatable.Validate(validationContext));
    }

    private IEnumerable<ModelValidationResult> ConvertResults(IEnumerable<ValidationResult> results)
    {
        foreach (var result in results)
        {
            if (result != ValidationResult.Success)
            {
                if (result.MemberNames == null || !result.MemberNames.Any())
                {
                    yield return new ModelValidationResult { Message = result.ErrorMessage };
                }
                else
                {
                    foreach (var memberName in result.MemberNames)
                    {
                        yield return new ModelValidationResult { Message = result.ErrorMessage, MemberName = memberName };
                    }
                }
            }
        }
    }
}

UnityなどDIコンテナでDataAnnotationsModelValidatorProviderに2で作成したValidatableObjectAdapterを登録します

最後にここまで作成したクラスを利用してインスタンスを生成します。今回はDIコンテナとしてUnityを利用します。

以下修正しました

当初はUnityConfig.csに設定を行っていましたが、Microsoft.Practices.Unity.ResolutionFailedExceptionが発生するためGlobal.asax.csに下記の通り設定するように修正しました。

public class MvcApplication : HttpApplication
{
    protected void Application_Start()
    {
        Utilities.LoadNativeAssemblies(Server.MapPath("~/bin"));

        AreaRegistration.RegisterAllAreas();
        FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
        RouteConfig.RegisterRoutes(RouteTable.Routes);
        BundleConfig.RegisterBundles(BundleTable.Bundles);

        var container = UnityConfig.GetConfiguredContainer();

        var fooService = container.Resolve<IFooService>();
        
        // note:RegisterViewModelのIsValidで使用するCustomValidatorを設定
        var serviceProvider = new CustomServiceProvider(fooService);
            
            // note:ここにはIValidatableObjectを実装したクラスを指定する
        DataAnnotationsModelValidatorProvider.RegisterValidatableObjectAdapterFactory(typeof(CompanyViewModel),
            (metadata, context) => new CustomValidatableObjectAdapter(serviceProvider, metadata, context));
    }
}

修正前の内容

修正前の内容はこちらの通りとなります。この場合、

Microsoft.Practices.Unity.ResolutionFailedException はユーザー コードによってハンドルされませんでした。

がスローされるようになりました。原因はちょっとわかっていません。

/// <summary>
///     Specifies the Unity configuration for the main container.
/// </summary>
public class UnityConfig
{
    private static readonly Logger Logger = LogManager.GetCurrentClassLogger();

    /// <summary>
    ///     Registers the type mappings with the Unity container.
    /// </summary>
    /// <remarks>
    ///     There is no need to register concrete types such as controllers or
    ///     API controllers (unless you want to change the defaults), as Unity
    ///     allows resolving a concrete type even if it was not previously
    ///     registered.
    /// </remarks>
    /// <param name="container">The unity container to configure.</param>
    public static void RegisterTypes(IUnityContainer container)
    {
        // NOTE: To load from web.config uncomment the line below. Make sure to add a Microsoft.Practices.Unity.Configuration to the using statements.
        // container.LoadConfiguration();

        container.RegisterType<DataContext>(new PerRequestLifetimeManager(),
            new InjectionFactory(_ =>
            {
                var basicContext = new DataContext();
                basicContext.Database.Log = s =>
                {
                    if (Logger.IsDebugEnabled)
                    {
                        Logger.Debug("SQL:{0}", s);
                    }
                };
                return basicContext;
            }));

        ...
        container.RegisterType<IFooRepository, FooRepository>();
            container.RegisterType<IFooService, FooService>();

        // note:RegisterViewModelのIsValidで使用するCustomValidatorを設定
        var serviceProvider = new AddressServiceProvider(container.Resolve<IFooService>());
            
            // note:ここにはIValidatableObjectを実装したクラスを指定する
        DataAnnotationsModelValidatorProvider.RegisterValidatableObjectAdapterFactory(typeof(CompanyViewModel),
            (metadata, context) => new CustomValidatableObjectAdapter(serviceProvider, metadata, context));
    }
        ...
}

IValidatableObjectを実装したValidateクラスでDIされたServiceを利用する

ここまでで準備は整いましたので、あとはDIされたServiceを利用します。

public class CompanyViewModel : IValidatableObject
{
    private static readonly Logger Logger = LogManager.GetCurrentClassLogger();

    public long Id { get; set; }

    /// <summary>
    ///     会社名
    /// </summary>
    [StringLength(50)]
    [Display(Name = "会社名")]
    public string Name { get; set; }
    ...
    public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
    {
        // using (var dataContext = new DataContext())
        // {
        //     var fooRepository = new FooRepository(dataContext);
        //     var fooService = new FooService(fooRepository);
        //  ...
        // }

            var fooServices = validationContext.GetService(typeof(IFooService)) as IFooService;
            ...

    }
}

コードもだいぶんすっきりしますし、テストも通常のメソッドと同様に引数のValidationContext経由でMoqを定義することで容易に行うことができます。

それにしても、この辺の情報へ皆さんどうやってアクセスされているのでしょうか?以下のように参考にさせていただいたサイトはいくつかありましたが、切り口を見つけること自体が大変だと感じます。

参考にしたサイト

今回の処理とは少し違うのですが、IServiceProvider経由でInjectしたいクラスを渡すこと、それにその周辺の技術的な説明がとても助かりました。ありがとうございます。

ValidationAttribute の検証メソッド内で、外部コンポーネントを利用する - miso_soup3 Blog

RegisterDefaultValidatableObjectAdapterFactoryを利用するところまではこのgistを参考にさせていただきました。 Adding IServiceProvider to ValidationContext in ASP.NET MVC

RegisterValidatableObjectAdapterFactoryの利用方法についてここを見ればわかりました。ずーっとIValidatableObjectを実装したinterfaceを作成して上手くいかないと悩んでいました。 Validate

参考にした書籍

プログラミングASP.NET MVC 第3版 ASP.NET MVC 5対応版

プログラミングASP.NET MVC 第3版 ASP.NET MVC 5対応版