6
\$\begingroup\$

This code is for dispatching and handling certain events from GitHub (and also Bitbucket). Things like pushes, issues, etc. can be handled by this system, and strongly-typed objects will be returned.

I left the XML docs out of all these, as somewhere from 2/3 to 3/4 of the files were XML docs. (We all know how much I love my XML docs.)

The first file here is the IEventDispatcher which is what handles the event dispatching. This is a common interface between the GitHub and Bitbucket dispatchers. On GitHub: IEventDispatcher.

public interface IEventDispatcher
{
    T Deserialze<T>(string json);

    void Dispatch(string eventKey, string json);
}

This next file is the GitHub dispatcher. On GitHub: GitHub.EventDispatcher.

public class EventDispatcher : IEventDispatcher
{
    public void Dispatch(string eventKey, string json)
    {
        switch (eventKey)
        {
            case CommitCommentEvent.WebhookEventName:
                OnCommitCommentReceived(new CommitCommentEventArgs(Deserialze<CommitCommentEvent>(json)));
                break;
            case CreateEvent.WebhookEventName:
                OnCreateReceived(new CreateEventArgs(Deserialze<CreateEvent>(json)));
                break;
            case DeleteEvent.WebhookEventName:
                OnDeleteReceived(new DeleteEventArgs(Deserialze<DeleteEvent>(json)));
                break;
            case DeploymentEvent.WebhookEventName:
                OnDeploymentReceived(new DeploymentEventArgs(Deserialze<DeploymentEvent>(json)));
                break;
            case DeploymentStatusEvent.WebhookEventName:
                OnDeploymentStatusReceived(new DeploymentStatusEventArgs(Deserialze<DeploymentStatusEvent>(json)));
                break;
            case ForkEvent.WebhookEventName:
                OnForkReceived(new ForkEventArgs(Deserialze<ForkEvent>(json)));
                break;
            case GollumEvent.WebhookEventName:
                OnGollumReceived(new GollumEventArgs(Deserialze<GollumEvent>(json)));
                break;
            case IssueCommentEvent.WebhookEventName:
                OnIssueCommentReceived(new IssueCommentEventArgs(Deserialze<IssueCommentEvent>(json)));
                break;
            case IssuesEvent.WebhookEventName:
                OnIssuesReceived(new IssuesEventArgs(Deserialze<IssuesEvent>(json)));
                break;
            case MemberEvent.WebhookEventName:
                OnMemberReceived(new MemberEventArgs(Deserialze<MemberEvent>(json)));
                break;
            case MembershipEvent.WebhookEventName:
                OnMembershipReceived(new MembershipEventArgs(Deserialze<MembershipEvent>(json)));
                break;
            case PageBuildEvent.WebhookEventName:
                OnPageBuildReceived(new PageBuildEventArgs(Deserialze<PageBuildEvent>(json)));
                break;
            case PublicEvent.WebhookEventName:
                OnPublicReceived(new PublicEventArgs(Deserialze<PublicEvent>(json)));
                break;
            case PullRequestEvent.WebhookEventName:
                OnPullRequestReceived(new PullRequestEventArgs(Deserialze<PullRequestEvent>(json)));
                break;
            case PullRequestReviewCommentEvent.WebhookEventName:
                OnPullRequestReviewCommentReceived(new PullRequestReviewCommentEventArgs(Deserialze<PullRequestReviewCommentEvent>(json)));
                break;
            case PushEvent.WebhookEventName:
                OnPushReceived(new PushEventArgs(Deserialze<PushEvent>(json)));
                break;
            case ReleaseEvent.WebhookEventName:
                OnReleaseReceived(new ReleaseEventArgs(Deserialze<ReleaseEvent>(json)));
                break;
            case RepositoryEvent.WebhookEventName:
                OnRepositoryReceived(new RepositoryEventArgs(Deserialze<RepositoryEvent>(json)));
                break;
            case StatusEvent.WebhookEventName:
                OnStatusReceived(new StatusEventArgs(Deserialze<StatusEvent>(json)));
                break;
            case TeamAddEvent.WebhookEventName:
                OnTeamAddReceived(new TeamAddEventArgs(Deserialze<TeamAddEvent>(json)));
                break;
            case WatchEvent.WebhookEventName:
                OnWatchReceived(new WatchEventArgs(Deserialze<WatchEvent>(json)));
                break;
        }
    }

    public T Deserialze<T>(string json)
    {
        DataContractJsonSerializer serializer = new DataContractJsonSerializer(typeof(T));
        using (MemoryStream ms = new MemoryStream(Encoding.UTF8.GetBytes(json)))
        {
            ms.Position = 0;
            return (T)serializer.ReadObject(ms);
        }
    }

    protected void OnCommitCommentReceived(CommitCommentEventArgs e)
    {
        var del = CommitCommentEventReceived;
        del?.Invoke(this, e);
    }

    protected void OnCreateReceived(CreateEventArgs e)
    {
        var del = CreateEventReceived;
        del?.Invoke(this, e);
    }

    protected void OnDeleteReceived(DeleteEventArgs e)
    {
        var del = DeleteEventReceived;
        del?.Invoke(this, e);
    }

    protected void OnDeploymentReceived(DeploymentEventArgs e)
    {
        var del = DeploymentEventReceived;
        del?.Invoke(this, e);
    }

    protected void OnDeploymentStatusReceived(DeploymentStatusEventArgs e)
    {
        var del = DeploymentStatusEventReceived;
        del?.Invoke(this, e);
    }

    protected void OnForkReceived(ForkEventArgs e)
    {
        var del = ForkEventReceived;
        del?.Invoke(this, e);
    }

    protected void OnGollumReceived(GollumEventArgs e)
    {
        var del = GollumEventReceived;
        del?.Invoke(this, e);
    }

    protected void OnIssueCommentReceived(IssueCommentEventArgs e)
    {
        var del = IssueCommentEventReceived;
        del?.Invoke(this, e);
    }

    protected void OnIssuesReceived(IssuesEventArgs e)
    {
        var del = IssuesEventReceived;
        del?.Invoke(this, e);
    }

    protected void OnMemberReceived(MemberEventArgs e)
    {
        var del = MemberEventReceived;
        del?.Invoke(this, e);
    }

    protected void OnMembershipReceived(MembershipEventArgs e)
    {
        var del = MembershipEventReceived;
        del?.Invoke(this, e);
    }

    protected void OnPageBuildReceived(PageBuildEventArgs e)
    {
        var del = PageBuildEventReceived;
        del?.Invoke(this, e);
    }

    protected void OnPublicReceived(PublicEventArgs e)
    {
        var del = PublicEventReceived;
        del?.Invoke(this, e);
    }

    protected void OnPullRequestReceived(PullRequestEventArgs e)
    {
        var del = PullRequestEventReceived;
        del?.Invoke(this, e);
    }

    protected void OnPullRequestReviewCommentReceived(PullRequestReviewCommentEventArgs e)
    {
        var del = PullRequestReviewCommentEventReceived;
        del?.Invoke(this, e);
    }

    protected void OnPushReceived(PushEventArgs e)
    {
        var del = PushEventReceived;
        del?.Invoke(this, e);
    }

    protected void OnReleaseReceived(ReleaseEventArgs e)
    {
        var del = ReleaseEventReceived;
        del?.Invoke(this, e);
    }

    protected void OnRepositoryReceived(RepositoryEventArgs e)
    {
        var del = RepositoryEventReceived;
        del?.Invoke(this, e);
    }

    protected void OnStatusReceived(StatusEventArgs e)
    {
        var del = StatusEventReceived;
        del?.Invoke(this, e);
    }

    protected void OnTeamAddReceived(TeamAddEventArgs e)
    {
        var del = TeamAddEventReceived;
        del?.Invoke(this, e);
    }

    protected void OnWatchReceived(WatchEventArgs e)
    {
        var del = WatchEventReceived;
        del?.Invoke(this, e);
    }

    public event EventHandler<CommitCommentEventArgs> CommitCommentEventReceived;
    public event EventHandler<CreateEventArgs> CreateEventReceived;
    public event EventHandler<DeleteEventArgs> DeleteEventReceived;
    public event EventHandler<DeploymentEventArgs> DeploymentEventReceived;
    public event EventHandler<DeploymentStatusEventArgs> DeploymentStatusEventReceived;
    public event EventHandler<ForkEventArgs> ForkEventReceived;
    public event EventHandler<GollumEventArgs> GollumEventReceived;
    public event EventHandler<IssueCommentEventArgs> IssueCommentEventReceived;
    public event EventHandler<IssuesEventArgs> IssuesEventReceived;
    public event EventHandler<MemberEventArgs> MemberEventReceived;
    public event EventHandler<MembershipEventArgs> MembershipEventReceived;
    public event EventHandler<PageBuildEventArgs> PageBuildEventReceived;
    public event EventHandler<PublicEventArgs> PublicEventReceived;
    public event EventHandler<PullRequestEventArgs> PullRequestEventReceived;
    public event EventHandler<PullRequestReviewCommentEventArgs> PullRequestReviewCommentEventReceived;
    public event EventHandler<PushEventArgs> PushEventReceived;
    public event EventHandler<ReleaseEventArgs> ReleaseEventReceived;
    public event EventHandler<RepositoryEventArgs> RepositoryEventReceived;
    public event EventHandler<StatusEventArgs> StatusEventReceived;
    public event EventHandler<TeamAddEventArgs> TeamAddEventReceived;
    public event EventHandler<WatchEventArgs> WatchEventReceived;
}

Then the Bitbucket dispatcher. On GitHub: Bitbucket.EventDispatcher.

public class EventDispatcher : IEventDispatcher
{
    public void Dispatch(string eventKey, string json)
    {
        switch (eventKey)
        {
            case PushEvent.WebhookEventName:
                OnPushReceived(new PushEventArgs(Deserialze<PushEvent>(json)));
                break;
        }
    }

    public T Deserialze<T>(string json)
    {
        DataContractJsonSerializer serializer = new DataContractJsonSerializer(typeof(T));
        using (MemoryStream ms = new MemoryStream(Encoding.UTF8.GetBytes(json)))
        {
            ms.Position = 0;
            return (T)serializer.ReadObject(ms);
        }
    }

    protected void OnPushReceived(PushEventArgs e)
    {
        var del = PushReceived;
        del?.Invoke(this, e);
    }

    public event EventHandler<PushEventArgs> PushReceived;
}

I'm not going to put all the events or event args here, as I thing I would exceed the character limit (even if it is 65k), but they all share the same basics.

We'll use the PushEvent as an example, as this is one of the more popular/regular events.

Here we have the PushEventArgs. On GitHub: GitHub.Events.Args.PushEventArgs.

public class PushEventArgs : EventArgs
{
    public PushEvent Event { get; }

    public PushEventArgs(PushEvent e)
    {
        Event = e;
    }
}

Next, the PushEvent model. On GitHub: GitHub.Events.PushEvent.

[DataContract(Name = "root")]
public class PushEvent
{
    public const string WebhookEventName = "push";

    [DataMember(Name = "ref")]
    public string Ref { get; set; }

    [DataMember(Name = "before")]
    public string Before { get; set; }

    [DataMember(Name = "after")]
    public string After { get; set; }

    [DataMember(Name = "created")]
    public bool Created { get; set; }

    [DataMember(Name = "deleted")]
    public bool Deleted { get; set; }

    [DataMember(Name = "forced")]
    public bool Forced { get; set; }

    [DataMember(Name = "base_ref")]
    public string BaseRef { get; set; }

    [DataMember(Name = "compare")]
    public string Compare { get; set; }

    [DataMember(Name = "commits")]
    public List<Commit> Commits { get; set; }

    [DataMember(Name = "head_commit")]
    public Commit HeadCommit { get; set; }

    [DataMember(Name = "repository")]
    public Repository Repository { get; set; }

    [DataMember(Name = "pusher")]
    public Author Pusher { get; set; }

    [DataMember(Name = "sender")]
    public Sender Sender { get; set; }
}

The Bitbucket version of PushEventArgs. On GitHub: Bitbucket.Events.Args.PushEventArgs.

public class PushEventArgs
{
    public PushEvent Event { get; }

    public PushEventArgs(PushEvent e)
    {
        Event = e;
    }
}

And the PushEvent for Bitbucket. On GitHub: Bitbucket.Events.PushEvent.

[DataContract]
public class PushEvent
{
    public const string WebhookEventName = "repo:push";

    [DataMember(Name = "actor")]
    public Actor Actor { get; set; }

    [DataMember(Name = "push")]
    public PushData Push { get; set; }

    [DataMember(Name = "repository")]
    public Repository Repository { get; set; }
}

Do not that Repository and Actor are different classes between GitHub and Bitbucket.

I'll not bother posting any models, as they don't do anything except contain DataMember attributes and properties which those attributes apply to.

One of the things I thought about doing is making an EventArgs<T> class to handle all these event args. At the moment I have like 20+ files that all look like PushEventArgs does.


Example usage would look something like:

EventDispatcher gitHubEventDispatcher = new EventDispatcher();
gitHubEventDispatcher.PushEventReceived += gitHubEventDispatcher_PushEventReceived;
gitHubEventDispatcher.Dispatch(response.Headers["X-GitHub-Event"], responsePayload);

public void gitHubEventDispatcher_PushEventReceived(object sender, PushEventArgs e)
{
    using (SqlConnection sqlConnection = new SqlConnection(connectionString)
    {
        sqlConnection.Open();
        using (SqlCommand sqlCommand = new SqlCommand("INSERT INTO dbo.OutgoingMessages (ApiSource, EventKey, Message) VALUES ('GitHub', @EventKey, @Message)", sqlConnection)
        {
            command.Parameters.Add(new SqlParameter("@Message", SqlDbType.VarChar)).Value = $"Commit to {e.Event.Repository.FullName} at {e.Event.HeadCommit.Timestamp.ToString()} by {e.Event.HeadCommit.Author.Name}, message: \"[e.Event.Commits[e.Event.Commits.Count - 1].Message\".";
            command.Parameters.Add(new SqlParameter("@EventKey", SqlDbType.VarChar)).Value = e.Event.WebhookEventName;
            command.ExecuteNonQuery();
        }
    }
}
\$\endgroup\$
  • \$\begingroup\$ Can you post an example of how you would use IEventDispatcher? I think some more context about how this will be used could help. \$\endgroup\$ – Greg Burghardt Oct 1 '15 at 12:37
  • \$\begingroup\$ @GregBurghardt Added an example of usage. :) \$\endgroup\$ – Der Kommissar Oct 1 '15 at 13:03
  • \$\begingroup\$ Also, @GregBurghardt the IEventDispatcher is to require any EventDispatcer objects to implement the regular stuff that's needed with them. \$\endgroup\$ – Der Kommissar Oct 1 '15 at 13:35
4
\$\begingroup\$

Let us tackle this code top down.

public interface IEventDispatcher
{
    T Deserialze<T>(string json);

    void Dispatch(string eventKey, string json);
}  

This is nice and clean, but I have the feeling that the Deserialize<T>() method is somehow misplaced in that interface.
A dispatcher should just have only the Dispatch() method because that is his job. I don't say that an implementation of this interface shouldn't have a Deserialize<T>() method but that method doesn't need to be in the interface hence it should't be public.

public interface IEventDispatcher
{
    void Dispatch(string eventKey, string json);
}  

Now assume someday jason isn't anymore that hip or will be replaced by some better format, the best would be to add an interface ISerializer of which an implementation will be constructor injected into the EventDispatcher, something along this lines

public interface ISerializer
{
    T Deserialze<T>(string json);
}
public class JasonSerializer : ISerializer
{
    public T Deserialze<T>(string json)
    {
        DataContractJsonSerializer serializer = new DataContractJsonSerializer(typeof(T));
        using (MemoryStream ms = new MemoryStream(Encoding.UTF8.GetBytes(json)))
        {
            return (T)serializer.ReadObject(ms);
        }
    }
}  

by the way, setting the Position property to 0 isn't needed here, because after instantiation of the MemoryStream the Position is 0.


public void Dispatch(string eventKey, string json)

Assume the class would have a Dictionary<string, Action<string>> for each passed in eventKey like so

private readonly ISerializer serializer;
public EventDispatcher(ISerializer serializer)
{
    this.serializer = serializer;
    InitializeEventsDictionary();
}

private T Deserialze<T>(string json)
{
    return serializer.Deserialze<T>(json);
}

Private Dictionary<string, Action<string>> events = Dictionary<string, Action<string>>();
private void InitializeEventsDictionary()
{
    events.Add(CommitCommentEvent.WebhookEventName, (s) => { OnCommitCommentReceived(() => { return new CommitCommentEventArgs(Deserialze<CommitCommentEvent>(s)); }); });
    events.Add(CreateEvent.WebhookEventName, (s) => { OnCreateReceived(() => { return new CreateEventArgs(Deserialze<CreateEvent>(s)); }); });
    ....
    ....
}

and if we change the signatures of the OnXXX methods like so

protected void OnCommitCommentReceived(Func<CommitCommentEventArgs> e)
{
    var del = CommitCommentEventReceived;
    del?.Invoke(this, e.Invoke());
}

the Dispatch() method could look like this

public void Dispatch(string eventKey, string json)
{
    Action<string> action;
    if (events.TryGetValue(eventKey, out action))
    {
        action.Invoke(jason);
    }
}

If this dispatcher is used by any kind of plugin you should ensure that an eventhandler which throws an exception does not break the whole applictation. This can be done by wraping the del?.Invoke(this, e.Invoke()); in a try..catch block, but if one of the plugins throws, the whole thing would go in the catch block and therefor any following eventhandler wouldn't get the event.

A solution to this would be to use the invocationlist of the event and iterate over the eventhandlers wrapped in a try..catch like so

protected void OnCommitCommentReceived(Func<CommitCommentEventArgs> f)
{
    var del = CommitCommentEventReceived;
    if (del == null) {return;}

    CommitCommentEventArgs e = f.Invoke();
    foreach(EventHandler<CommitCommentEventArgs> handler in del.GetInvocationList())
    {
        try
        {
            handler(this, e);
        }
        catch(Exception ex)
        {
            // because we don't/can't know which exception is thrown lets catch them all
            // and do some logging here if we don't want to swallow them
        }
    }
}
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.