简体   繁体   中英

Method Inference of Type T

Question

How do I define an incoming Type T constraint that will allow me to call a static method on the class (of type T) to get the intended IndexModel object for passing to Mongo?

Background

I'm currently trying to write a Mongo Provider class that will allow me to ensure my particular database and collection are present before doing any operations with them, since there is a potential that the container or server it resides in could be destroyed and recreated at any time, and I'd prefer to have a safe way in code to ensure that the external dependency is there (instance is beyond my control, so I have to trust that something is there).

One of the things I'm trying to do, since I've managed to do what I stated above for Database and Collection instantiation, is to also generate indexes. My idea was to have a static method on the classes that would return their specific definition of an index model. This way, each class would be responsible for their own Mongo indexes, rather than some convoluted switch-case statement in my Provider based on the incoming type of T.

My first idea was to have an interface that shared this method, but Interfaces don't allow you to declare a static method. Similarly, I tried an Abstract Base-class and found that the static implementation would call the base class that defined the method, rather than any overrides in an inheritor.

Sample Code

public class MyClass
{
    public DateTime DateValue { get; set; }
    public int GroupId { get; set; }
    public string DataType { get; set; }

    public static IEnumerable<CreateIndexModel<MyClass>> GetIndexModel(IndexKeysDefinitionBuilder<MyClass> builder)
    {
        yield return new CreateIndexModel<MyClass>(
            builder.Combine(
                builder.Descending(entry => entry.DateValue), 
                builder.Ascending(entry => entry.GroupId), 
                builder.Ascending(entry => entry.DataType)
                )
            );
    }
}

Edit

I guess I should probably include a shell of my Mongo Provider class. See below:

Edit #2 due to questions about how this hasn't solved my problem, I'm updating the MongoProvider to have the problematic code. Note: Once this method is included, the class will no longer compile, since it isn't possible given what I've done thus far.

public class MongoProvider
{
    private readonly IMongoClient _client;

    private MongoPrivder(ILookup<string, string> lookup, IMongoClient client)
    {
        _client = client;

        foreach(var database in lookup)
            foreach(var collection in database)
                Initialize(database.Key, collection);
    }

    public MongoProvider(IConfiguration config) :this(config.GetMongoObjects(), config.GetMongoClient())
    {}

    public MongoProvider(IConfiguration config, IMongoClient client) : this(config.GetMongoObjects(), client)
    {}

    private void Initialize(string database, string collection)
    {
        var db = _client.GetDatabase(database);
        if (!db.ListCollectionNames().ToList().Any(name => name.Equals(collection)))
            db.CreateCollection(collection);
    }
// The Problem
    private void InitializeIndex<T>(string database, string collection)
    {
        IEnumerable<CreateIndexModel<T>> models;

        switch (T)
        {
            case MyClass:
                model = MyClass.GetIndexModel();
                break;
            default:
                break;
        }

        await _client.GetDatabase(database)
            .GetCollection<T>(collection)
            .Indexes
            .CreateManyAsync(models);
    }
}

Edit #3

As a stop-gap, I've gone ahead and done something terrible (not sure if it's going to work yet), and I'll supply the example so you can know my best solution thus far.

public static class Extensions
{
    #region Object Methods

    public static T TryCallMethod<T>(this object obj, string methodName, params object[] args) where T : class
    {
        var method = obj.GetType().GetMethod(methodName);

        if (method != null)
        {
            return method.Invoke(obj, args) as T;
        }

        return default;
    }

    #endregion
}

This allows me to do the following (inside of MongoProvider)

private async void InitializeIndex<T>(string database, string collection) where T : new()
{
    var models = new T().TryCallMethod<IEnumerable<CreateIndexModel<T>>>("GetIndexModel");

    await _client.GetDatabase(database)
        .GetCollection<T>(collection)
        .Indexes
        .CreateManyAsync(models);
}

Since it doesn't look like I'm going to get an answer to this, I figured I would provide my solution for future searches of this question. Basically, I added an extension method to the base object class, and used reflection to determine if the method I was looking for was there. From there, I returned a value of true or false , depending on if the method was found, and output the return value to a parameter, in the traditional TryGet pattern.

Note to Future Readers

I do not recommend this approach. This is just how I solved my problem for accessing a method on a type of T. Ideally, an instance method would be implemented, and a signature defined in a common Interface , but that wasn't going to work for my use case.

My Answer

public static class Extensions
{
    #region Object Methods

    public static bool TryCallMethod<T>(this object obj, string methodName, out T result, params object[] args) where T : class
    {
        result = null;
        var method = obj.GetType().GetMethod(methodName);

        if (method == null)            
            return false;

        result = method.Invoke(obj, args) as T;
        return true;
    }

    #endregion
}

My data class looks like this (obfuscated from actual usage)

[BsonDiscriminator("data")]
public class DataClass
{
    #region Private Fields

    private const string MongoCollectionName = "Data";

    #endregion

    #region Public Properties

    public string CollectionName => MongoCollectionName;
    [BsonId]
    public ObjectId Id { get; set; }
    [BsonElement("date_value")]
    public DateTime DateValue { get; set; }
    [BsonElement("group_id")]
    public int GroupId { get; set; }
    [BsonElement("data_type")]
    public string DataType { get; set; }
    [BsonElement("summary_count")]
    public long SummaryCount { get; set; }
    [BsonElement("flagged_count")]
    public long FlaggedCount { get; set;  }
    [BsonElement("error_count")]
    public long ErrorCount { get; set;  }

    #endregion

    #region Constructor

    public DataClass()
    {

    }

    public DataClass(int groupId, string dataType = null, long summaryCount = 0, long flaggedCount = 0, long errorCount = 0)
    {
        Id = ObjectId.GenerateNewId();
        DateValue = DateTime.UtcNow;
        GroupId = groupId;
        DocCount = summaryCount;
        DataType = dataType ?? "default_name";
        FlaggedCount = flaggedCount;
        ErrorCount = errorCount;
    }

    #endregion

    #region Public Methods

    public static IEnumerable<CreateIndexModel<AuditEntry>> GetIndexModel(IndexKeysDefinitionBuilder<AuditEntry> builder)
    {
        yield return new CreateIndexModel<AuditEntry>(
            builder.Combine(
                builder.Descending(entry => entry.DateValue), 
                builder.Ascending(entry => entry.GroupId), 
                builder.Ascending(entry => entry.DataType)
                )
            );
    }

    #endregion
}

I would then call the method in the following fashion, inside my MongoProvider class. The ellipses are present to identify that more code exists within the class.

public class MongoProvider : IMongoProvider
{
    #region Private Fields

    private readonly IMongoClient _client;

    #endregion


    #region Constructor

    ...

    #endregion

    #region Private Methods

    private void Initialize(string database, string collection)
    {
        var db = _client.GetDatabase(database);
        if (!db.ListCollectionNames().ToList().Any(name => name.Equals(collection)))
            db.CreateCollection(collection);
    }

    private async Task InitializeIndex<T>(string database, string collection) where T : new()
    {
        if(new T().TryCallMethod<IEnumerable<CreateIndexModel<T>>>("GetIndexModel", out var models, new IndexKeysDefinitionBuilder<T>()))
            await _client.GetDatabase(database)
                .GetCollection<T>(collection)
                .Indexes
                .CreateManyAsync(models);
    }

    private static void ValidateOptions<T>(ref FindOptions<T, T> options)
    {
        if(options != null)
            return;

        options = new FindOptions<T, T>
        {
            AllowPartialResults = null,
            BatchSize = null,
            Collation = null,
            Comment = "AspNetWebService",
            CursorType = CursorType.NonTailable,
            MaxAwaitTime = TimeSpan.FromSeconds(10),
            MaxTime = TimeSpan.FromSeconds(10),
            Modifiers = null,
            NoCursorTimeout = false,
            OplogReplay = null
        };
    }

    private static FilterDefinition<T> GetFilterDefinition<T>(Func<FilterDefinitionBuilder<T>, FilterDefinition<T>>[] builders)
    {
        if(builders.Length == 0)
            builders = new Func<FilterDefinitionBuilder<T>, FilterDefinition<T>>[] {b => b.Empty};

        return new FilterDefinitionBuilder<T>()
            .And(builders
                .Select(b => b(new FilterDefinitionBuilder<T>()))
            );
    }

    #endregion

    #region Public Methods

    public async Task<IReadOnlyCollection<T>> SelectManyAsync<T>(string database, string collection, FindOptions<T, T> options = null, params Func<FilterDefinitionBuilder<T>, FilterDefinition<T>>[] builders) where T : new()
    {
        ValidateOptions(ref options);
        await InitializeIndex<T>(database, collection);
        var filter = GetFilterDefinition(builders);

        var find = await _client.GetDatabase(database)
            .GetCollection<T>(collection)
            .FindAsync(filter, options);

        return await find.ToListAsync();
    }

    ...

    #endregion
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM