2

A Quick Note

I know the question title is a little odd but I haven't had much caffeine this morning so I'm having trouble thinking clearly right now. If you have any suggestions for a better title, please comment below and I'll update.


The Problem

I've built a custom logging solution that allows us (my organization) to log events within our applications. The solution requires an event type when logging an event and I'd like to add support for application specific event types.

We currently have a single enum that contains all of our event types, and I believe this is not the correct way to approach it since adding specific event types related to each application will increase the number of values available in the enum by a lot. For example, with support added for only two applications, we're already up to 22 different event types.

My Current Thoughts

I've thought about creating a single class that contains multiple enum declarations for the different event types, but this will not work since I can't create a single method that will handle all the different enums, and I do not want to create multiple methods for each different enum since that will be a maintenance nightmare.

// Simple example.
public bool LogEvent(EventType type) { /* ... */ }

A possible solution would be to create a collection of classes with several const declarations like so:

public sealed class EventTypes {
    public class EventType {
        public string Name { get; private set; }
        public EventType(string name) { Name = name; }
    }
    public class Validation : EventType {
        public const int GeneralValidation = 100;
        public const int DataValidation = 101;
        public const int UserValidation = 102;
        private Validation() : base("Validation") { }
    }
    public class Application1 : EventType {
        public const int Event1 = 1000;
        public const int Event2 = 1001;
        private Application1() : base("Application1") { }
    }
}

However, I'm not so sure that this is an approach that would follow best practices in an object oriented environment and feels kind of "hacky". I would also want to prevent the classes from being instantiated outside of the EventTypes container class (would abstract still allow the private constructor to run?), and EventTypes should not be inheritable, so sealed could apply there, but I'm not sure on that either.

Requirements

The solution I'm looking for will allow me to have a single method for logging events as I currently have. An example implementation when calling the LogEvent method would look like this:

LogEvent(EventTypes.ApplicationName.EventTypeName);
LogEvent(EventTypes.Validation.DataValidation);

The main goal is to enforce consistency in our data so that we ensure no records are lost in queries and views. The primary thing I want to avoid is having developers enter an application name manually because this can lead to inconsistency, for example:

LogEvent("Application Name", "Event Name");
LogEvent("App Name", "Evt Name");

Sure, I can use constants to prevent this, but that would require the developers to use them, and requires code reviews to ensure they are following the standard. The ideal solution would cause compilation failure if an invalid type was supplied.

The Question

This probably isn't the best phrasing; however, what options are available to replace a single enum with a solution that supports multiple types and follows best practices in an object oriented environment?

  • Seems to me that you can declare a base class or an IEvent interface, and have each application provide its own events. LogEvent could then take an IEvent and call a method on it to do the actual logging (you could pass the a generic "log writer" to the event itself). "I would also want to prevent the classes from being instantiated outside of the EventTypes container class" - why (why is that important)? Let each application provide its own set of events. – Filip Milovanović Nov 13 '19 at 15:07
  • This question seems to be built on encapsulation violations in multiple spots (logging library needing to know all of its consumers, enum needing to account for a global ID across applications, ...). It seems you are not properly distinguishing between what's reusable and what is not. This may require a change of approach rather than trying to do what you are trying to do now, rendering this question effectively an XY problem – Flater Nov 15 '19 at 9:59
2

We have a repository for logging that has methods like below that can log any Type. It has similar methods for Search() and GetByID() for querying the log. With this method you don't have to specify anywhere what Types are supported other than they implement interface ICommand which in our case only has one property CreatedByID. The object passed in is serialized as json and then deserialized in methods like Search().

    public long Log<T>(T command) where T: ICommand {
        var type = GetTypeName<T>();
        var payload = JsonConvert.SerializeObject(command, Formatting.None);
        var entity = new Command { CreatedByID = command.CreatedByID, Payload = payload, Type = type };
        context.Commands.AddObject(entity);
        context.SaveChanges();
        return entity.ID;
    }

    private string GetTypeName<T>() {
        return typeof(T).FullName;
    }
| improve this answer | |
  • I'm just curious as to why you're using the where keyword in the method declaration. I understand what it does with general use, but I've never seen it used this way. If the type T does not implement ICommand, does it prevent the application from compiling, return null, or something else entirely? – Candleshark Nov 13 '19 at 15:16
  • 1
    We wanted every 'command' passed to the method to have property CreatedByID and that is what ICommand enforces. If you don't have a similar requirement to force all classes that are logged to have common property(s) then you would not need that part. In our version it will not compile if the T does not implement ICommand. – Scott Mildenberger Nov 13 '19 at 15:19
1

I think what you might be looking for is the typesafe enum pattern. This was a popular approach in Java prior to the introduction of the Java enum keyword. The java implementation of enum builds on this pattern and adds some features that can only be done at the language level. My understanding is that there is no corresponding feature in C# so the typesafe enum pattern could make sense here.

Here's an example based on your initial code:

public sealed class EventTypes {
    public class EventType {
        public readonly int Value { get; }
        protected EventType(int value) { Value = value; }
    }
    public sealed class Validation : EventType {
        public static readonly Validation GeneralValidation = new Validation(100);
        public static readonly Validation DataValidation = new Validation(101);
        public static readonly Validation UserValidation = new Validation(102);
        private Validation(int value) : base(value) { }
    }
    public sealed class Application1 : EventType {
        public static readonly Application1 Event1 = new Application1(1000);
        public static readonly Application1 Event2 = new Application1(1001);
        private Application1(int value) : base(value) { }
    }
}

If anything is off about the syntax here, let me know.

Here's an article that talks about this approach in C#: https://www.infoworld.com/article/3198453/how-to-implement-a-type-safe-enum-pattern-in-c.html

| improve this answer | |
0

Allow any enum (using the System.Enum base type) and use the name of both the enum and the enum value.

public bool LogEvent(Enum eventType)
{
    Console.WriteLine
    (   
          "An event was logged. Application: {0}, Category: {1} EventType: {2}",
          eventType.GetType().Assembly.FullName,
          eventType.GetType().Name,
          eventType.ToString()
    );
    //etc....
}

This way, developers are free to define whatever enum they need for their events, and your LogEvent method can figure out where the enum came from and what it means.

| improve this answer | |

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.