Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Returning a generic object without knowing the type?

I'm still fairly new to programming and have been tasked with creating a WebHook consumer that takes in a raw JSON string, parses the JSON into an object, which will be passed into a handler for processing. The JSON is coming in like this:

{
   "id":"1",
   "created_at":"2017-09-19T20:41:23.093Z",
   "type":"person.created",
   "object":{
      "id":"person1",
      "created_at":"2017-09-19T20:41:23.076Z",
      "updated_at":"2017-09-19T20:41:23.076Z",
      "firstname":"First",
      ...
   }
}

The inner object can be any object so I thought this would be a great opportunity to use generics and built my class as follows:

public class WebHookModel<T> where T : class, new()
{
    [JsonProperty(PropertyName = "id")]
    public string Id { get; set; }

    [JsonProperty(PropertyName = "created_at")]
    public DateTime CreatedAt { get; set; }

    [JsonProperty(PropertyName = "type")]
    public string Type { get; set; }

    [JsonProperty(PropertyName = "object")]
    public T Object { get; set; }

    [JsonIgnore]
    public string WebHookAction
    {
        get
        {
            return string.IsNullOrEmpty(Type) ? string.Empty : Type.Split('.').Last();
        }
    }
}

Then created the following interface:

public interface IWebHookModelFactory<T> where T : class, new()
{
   WebHookModel<T> GetWebHookModel(string type, string jsonPayload);
}

What I'm failing to understand is how am I supposed to implement the Factory class without knowing what the type is at compile time?

Playing around with the Model a bit, I changed it to an abstract class with an abstract T object so that it could be defined by a derived class.

public abstract class WebHookModel<T> where T : class, new()
{
    [JsonProperty(PropertyName = "id")]
    public string Id { get; set; }

    [JsonProperty(PropertyName = "created_at")]
    public DateTime CreatedAt { get; set; }

    [JsonProperty(PropertyName = "type")]
    public string Type { get; set; }

    [JsonProperty(PropertyName = "object")]
    public abstract T Object { get; set; }

    [JsonIgnore]
    public string WebHookAction
    {
        get
        {
            return string.IsNullOrEmpty(Type) ? string.Empty : Type.Split('.').Last();
        }
    }
}

public PersonWebHookModel : WebHookModel<Person>
{
    public override Person Object { get; set; }
}

But I still run into the same issue of trying to implement an interface in which I don't know the type at runtime. From what I've found online, this is an example of covariance, but I haven't found any articles that explain how to resolve this issue. Is it best to skip generics and create a massive case statement?

public interface IWebHookFactory<TModel, TJsonObject> 
    where TJsonObject : class, new()
    where TModel : WebHookModel<TJsonObject>
{
    TModel GetWebHookModel(string type, string jsonPayload);
}

I'm a bit partial to using the abstract class approach because it lets me define individual handlers based on which model I'm passing into my Service.

public interface IWebHookService<TModel, TJsonObject>
    where TJsonObject : class, new()
    where TModel : WebHookModel<TJsonObject>
{
    void CompleteAction(TModel webHookModel);
}

public abstract class BaseWebhookService<TModel, TJsonObject> : IWebHookService<TModel, TJsonObject>
        where TJsonObject : class, new()
        where TModel : WebHookModel<TJsonObject>
{
    public void CompleteAction(TModel webHookModel)
    {
        var self = this.GetType();
        var bitWise = System.Reflection.BindingFlags.IgnoreCase
                        | System.Reflection.BindingFlags.Instance
                        | System.Reflection.BindingFlags.NonPublic;

        var methodToCall = self.GetMethod(jsonObject.WebHookAction, bitWise);
        methodToCall.Invoke(this, new[] { jsonObject });
    }

    protected abstract void Created(TModel webHookObject);

    protected abstract void Updated(TModel webHookObject);

    protected abstract void Destroyed(TModel webHookObject);
}

public class PersonWebHookService : BaseWebHookService<PersonWebHookModel, Person>
{
    protected override void Created(PersonWebHookModel webHookModel)
    {
        throw new NotImplementedException();
    }

    protected override void Updated(PersonWebHookModel webHookModel)
    {
        throw new NotImplementedException();
    }

    protected override void Destroyed(PersonWebHookModel webHookModel)
    {
        throw new NotImplementedException();
    }
}
like image 332
bschreck Avatar asked Oct 29 '22 01:10

bschreck


1 Answers

Key points for the solution: 1. There needs to be some virtual call in there somewhere. 2. Somehow you need to map from your type tag in your JSON payload to your actual C# class.
IE, "person.created"," --> 'Person'.
If you control the serialization format, JSON.Net can inject its own type tag and do this for you. Assuming you can't go that route ... So you'll need something like a Dictionary to contain the mapping.

Assuming your definitions is like:

abstract class WebhookPayload // Note this base class is not generic! 
{
    // Common base properties here 

    public abstract void DoWork();
}

abstract class PersonPayload : WebhookPayload
{
    public override void DoWork()
    {
        // your derived impl here 
    }
}

And then you can deserialize like:

    static Dictionary<string, Type> _map = new Dictionary<string, Type>
    {
        { "person.created", typeof(PersonPayload)}
    }; // Add more entries here 

    public static WebhookPayload Deserialize(string json)
    {
        // 1. only parse once!
        var jobj = JObject.Parse(json); 

        // 2. get the c# type 
        var strType = jobj["type"].ToString(); 

        Type type;
        if (!_map.TryGetValue(strType, out type))
        {
            // Error! Unrecognized type
        }

        // 3. Now deserialize 
        var obj = (WebhookPayload) jobj.ToObject(type);
        return obj;
    }
like image 142
Mike S Avatar answered Nov 15 '22 05:11

Mike S