0

Use case: I need to create serial numbers that are accountable for, so serial number objects, once created must have a destination: either they are used (they get a code) or they are discarded.
For now I have an serial number generator which generates them depending on a a few production parameters, now I want to make sure no object is "lost".
Although I do record each serial number generated in a database as "generated", I want to make sure that whoever uses a serial number object correctly declares what was done with it and not just discards it.
I tried using destructors and disposable interfaces but none really worked.
Any ideas?

Its not at all about how and when the generated object can or could be discarded, but its about forcing the consumer to implement the correct logic when using this class, on all code paths, being: call one of the "use/discard" functions on the object.

this means: generate() => use() or generate() => discard() but not generate() => forget/ignore

EDIT: Its not about making sure that if armaggedon comes, the object is still treated correctly. Its just about making sure that no method in the same process generates an object and never uses it, or, that it correcly discards the object if not needed. And, since this is tagged C#, how to go along to implement it correctly in C#.

Oh, and yes, if one enables Code Analysis, CA2000 will be emited if not used correctly when implementing the IDisposable interface in the SerialNumber class. Thats an option of course.

Usage example:

        // this is a "correct use" case
        using(SerialNumber sn = SerialNumberGenerator.NewNumber())
        {
            if(part is null)
                SerialNumberGenerator.Discard(sn);
            else
                sn.AttachToPart();
            // could also be SerialNumberGenerator.Use(sn, part);
        }

        // now for a use case that I want to avoid:
        SerialNumber sn = SerialNumberGenerator.NewNumber();
        if(part is null)
            return;
11
  • Are the generator and the consumer part of the same running system? Or is the generator a service for other systems? Aug 27, 2020 at 10:51
  • Same running system, but most certainly not used by the same developer. Thats why I want to force the correct consumption, ie. throw an exception when the object was never consumed. Aug 27, 2020 at 10:57
  • 2
    A commented code example, ideally from the perspective of the user of the serial number generator, might be helpful.
    – Doc Brown
    Aug 27, 2020 at 11:25
  • 1
    If I got this right, a serial number can keep track if AttachToPart was called, and it may hold a reference to the SerialNumberGenerator which created it. So why not implement a Finalize method in SerialNumber which calls MySerialNumberGenerator.Discard(this) when AttachToPart was not called before the garbage collector tries to clean up the unused SerialNumber?
    – Doc Brown
    Aug 27, 2020 at 15:18
  • 1
    @Hefaistos68: that sounds more like a simple bug in your code. using will call the Dispose method, not the finalizer. The finalizer will only be called by the GC. Maybe you could provide a minimum working example of a code snippet where you expect Finalize to be called and post a related question on Stackoverflow?
    – Doc Brown
    Aug 27, 2020 at 15:42

5 Answers 5

2

You can never guarantee usage.

  • The easiest way to prove that point is that the consumer's machine can just physically explode, and it won't be doing much of anything after that (except smoldering, I guess).
  • Even if it's the same machine, the other process could be killed.
  • Even if it's the same process, the other class could have a bug which causes it to not do its subsequent work.
  • Even if no bugs are present, the consumer can just willfully discard or not use any information it chooses to.

What you can do is expect to see usage, and when not seen, take a certain action. Define what you expect to see as proof of usage, check for it in a given grace period, and if no usage is found, clean up the generated ID or mark it as free again.

If the generator and consumer are in the same runtime, then you could add a little spy inside the ID class which your generator tracks and observes its usage. But that again requires you to define your criteria for "being used" or not.

Make sure you document this behavior to your consumers, or you're going to get bug reports for what is essentially intended behavior.

whoever uses a serial number object correctly declares what was done with it

Taking your assumption to its logical conclusion, your idea is based on the notion that you can definitively force other code to be flawless. That's just not a reasonable standard by any means.

Not just because it crosses the boundaries of good practice encapsulation, but because you're not accounting for human error or even (physical or digital) events that no one could foresee.

Part of developing processes is accounting for all the possible outs, and it's generally naive to assume you can force a given out to be the only possible one.

5
  • 1
    No no, just want to make sure that whatever method asks for a object (serial number) to be generated, correctly does something with it. So, either discard it, or use it. But dont just forget it. Its not about taking into account external causes, like processes that die, computers that burn, or whatever may happen. Its about the logic of using the object. Aug 27, 2020 at 12:32
  • @Hefaistos68: You cannot dictate what a separate piece of logic will do. That crosses the boundary of encapsulation. Because when you do, then it's no longer a separate piece of code. Besides, even if your generator were to enforce that, you still can't guarantee that your own code that does so isn't bugged either.
    – Flater
    Aug 27, 2020 at 18:03
  • of course one can, when you check a parameter for null and throw an ArgumentNullException you do exactly that. Its simply verifying that a required condition is satisfied. In this case, that one of two methods is called after the object was created. Aug 28, 2020 at 8:01
  • @Hefaistos68: You're arguing whether you could. I'm arguing whether you should.
    – Flater
    Aug 28, 2020 at 10:44
  • When you have an underlying legal requirement then its not a question if you should. Even if its "only" a design decision on how an object should be used, any means of enforcing this decision is helpful. Otherwise you dont even need to make the design decision. Aug 28, 2020 at 10:54
0

It sounds like you're wanting compile time guarantees about exactly-one usage of an object. Linear type systems provide such a guarantee. (Affine type systems like Rust's allow you to guarantee "zero usages or one usage" for a value.) I don't know of anyone having embedded a linear logic in C#'s type system, or a Roslyn analyzer, but that's an avenue to explore.

1
  • Runtime, not compile time (although this would solve it too), and its not one usage per object, its at-least-one usage. I dont wont to end up with endless serials generated in the database that have no use or no part associated with them, cause every number has to be accounted for. Aug 27, 2020 at 14:48
0

Assuming you have a central database where your objects are stored you could issue a key for a possible new object to a client that requests it and keep it in your database as "issued". Once the client has the object fully populated (this may require interaction, it may take a couple of minutes up to a couple of days if your system is distributed and partly offline) it will post it to your database. At that moment you could check if the key provided with the object was indeed issued and has not expired yet. Now you can either adopt the new object or reject it. If you reject it because the client is too late, you will not use that key again and flag it as dead, which you could also do if the client does not submit an object in time.

[Edit]

After exchanging comments it seems you want to provide a class that has control over what its client does to it. You may be able to achieve this using events. If you have the client of your class create an instance and then subscribe to an event, you can have the class (your code) call the shots (by raising the event) and expect a certain response from the client.

The client indicates it wants a new serial number by calling a method HitMe() on your class (the generator). The generator would then create a new serial number object and pass it as an event argument to the client's event handler. When the handler returns, you can check if either the Discard method or the Attach method was called. If not, you can throw your exception.

8
  • This would be the solution (and is already working like this) for a distributed system. But we are talking same process here (mentioned in question). As I said, i already store each number as "generated" in the database, yet this doesnt help me avoiding that it stays like this, from a programming point of view. Aug 27, 2020 at 13:14
  • @Hefaistos68 I was presenting the hardest scenario. With one process it just gets easier because you do not need to persist anything and you can be sure all your clients are gone when the process ends and will never use that key again. Within one process you (can) know everything that goes on. I do not see a problem within a process. All parties involved can listen in real time to all keys being created, applied or discarded. Aug 27, 2020 at 15:03
  • the problem is that I need something to be done with each object, so it can be accounted for. Any serial number generated and not used or discarded, is a legal problem. So, if a method asks for a new serial number, the developer of that method is reponsible to do something with it, this I want to enforce. Either by throwing an exception when the object goes out of scope, or some other similar drastic action. Aug 27, 2020 at 15:13
  • @Hefaistos68 - the thing that needs to be done with the generated serial - is it always the same (e.g., a call to AttachToPart) or is it that different methods do different things? Also, does the condition for discarding the serial vary? Aug 27, 2020 at 17:09
  • @Hefaistos68 I updated my answer. Aug 27, 2020 at 17:40
0

See below for an updated (better) answer based on the suggestions of @user:275536

I had it all right from the beginning. IDisposable implemented, checked for a flag that indicated if the object was used or not, throwing an SerialNumberNotUsedException when not used. Point is: how exceptions are handled when thrown in the finalizer depends on the application policy. In my case, they where ignored, so the application can continue.

What pointed me in the right direction: https://stackoverflow.com/questions/20358401/throwing-exception-in-finalizer-to-enforce-dispose-calls

Solution: use Debug.Fail() instead of throwing the exception.

Conclusion: yes, we can enforce other devs to use our object the intended way. And this is how:

public class TestUsage : IDisposable
{
    private bool _wasUsed;
    private bool disposedValue;
    private int someProp;

    public TestUsage()
    {
    }

    ~TestUsage()
    {
        if(!_wasUsed) Debug.Fail("Object not used");
    }

    public int SomeProp
    {
        get
        {
            return someProp;
        }
        set
        {
            someProp = value;
            Console.WriteLine("Using");
            _wasUsed = true;
        }
    }

    protected virtual void Dispose(bool disposing)
    {
        if(!disposedValue)
        {
            if(disposing) { /* not doing anything here */   }
            disposedValue = true;
        }

        if(!_wasUsed)
        {
            Debug.Fail("Object was not used");
        }
    }
    public void Dispose()
    {
        Dispose(disposing: true);
        GC.SuppressFinalize(this);
    }
}

Now for the updated answer: As suggested by @user:275536, using an action when creating the SerialNumber allows for a cleaner and more efficient way of creating, using and checking the use of the object:

// Somewhere down the SerialNumberGenerator...
public void Generate(Action<SerialNumber> consumer)
{
  using(SerialNumber sn = GenerateNumberInternal())
  {
    consumer?.Invoke(sn);
    if(!sn.WasUsed) 
      throw new SerialNumberNotUsedException();
      // we could just discard the SN here, but when this happens the consumer actually did something wrong, so throw it up
  }
}

Inside the SerialNumber object, it tracks if any of its "use" functions was called, or it was discarded by the SerialNumberGenerator, through the WasUsed property.

11
  • Please no comments of type "exceptions in finalizers are a bad idea" and such. We all know that. And its not for the sake of the application but the developer using the object, this case is to screw the dev until he uses the object the intended way. Aug 28, 2020 at 9:33
  • Why do you need a Dispose method / IDisposable derivation? A finalizer should be pretty much enough for the described purpose.
    – Doc Brown
    Aug 28, 2020 at 12:52
  • True, well, the dispose of the real object does a bit more than only check. So, for the purpose of checking only, yes, a finalizer shoudl suffice. Aug 28, 2020 at 13:15
  • 3
    I think your experience with C++ holds you back here. It's great that you can utilize the lifetime of an object in C++, but it's sort of a crutch; so now what you're doing is you're trying to find a similar mechanism in a language that operates under different rules. Instead, design an API that suits the problem. See my comment on the accepted answer, the one about passing in a lambda. Aug 28, 2020 at 17:05
  • 1
    You can't tell when the finalizer or Dispose will kick in. The CLR will call those whenever it chooses to, which may be far into the shutdown procedure of the application. If exceptions would still work, which is questionable, they would not be handled by the application's main thread as already pointed out by others. By the way, it is not done to accept your own answer, even if it were a good one. Aug 29, 2020 at 20:38
-2

Could put a method inside the object - im asuming its a constructor object and call that method which will update the parameters/attributes etc in the object or execute an even...c# can do that

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

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