What you are asking is technically possible with text templating (an example is after the fold). However, as has been pointed out, good text templating isn't easy and usually requires just as much maintenance as your application code.
Instead, take a look and determine whether your Enum is really a constrained list of all possible states. If you are storing your statuses in a database table, I would say that it is not. Being in a table makes it technically possible that these values will change (new ones can be inserted, old ones deleted or even the names changed such that they no longer reflect their original meaning). Only user convention will prevent this, and it won't be immediately apparent in your application code.
You might be better off rethinking your database design. The next statement is tricky for me to explain, but I prefer not to infer entity status based on a child relationship. Instead I prefer to make code decisions based individual column value of a given data row.
For example, rather than two tables, as per:
CREATE TABLE status_codes (
key INT PRIMARY KEY,
value VARCHAR(32)
);
CREATE TABLE entity (
id INT PRIMARY KEY,
name VARCHAR(32),
status INT
CONSTRAINT fk_status FOREIGN KEY status REFERENCES status_codes(id)
);
INSERT INTO status_codes
VALUES (20, 'Open')
,(21, 'Received')
,(22, 'Delivered')
,(23, 'Cancelled');
I would prefer:
CREATE TABLE entity (
id INT,
name VARCHAR(32),
is_received BIT NOT NULL,
is_delivered BIT NOT NULL,
is_cancelled BIT NOT NULL,
is_closed BIT NOT NULL
);
For me, I find it easier to code the logic when there are no magic numbers.
This way you are no longer doing things like:
/* SQL */
SELECT * FROM entity WHERE status = 20;
/* C# */
if (entity.Status == StatusCodes.Open) { /* do something */ }
if (entity.Status == 20) { /* do something */ }
and instead doing:
/* SQL */
SELECT * FROM entity WHERE is_closed = 0;
/* C# */
if (!entity.IsClosed) { /* do something */ }
You can also employ database constraints for process (e.g. ensuring an item cannot be marked received before it is delivered, and cannot be cancelled if it has already been delivered).
I've also changed 'is open' semantics to 'is closed', but that is just a personal preference to make doing something (i.e. closing) be more significant than not done anything (i.e. is open).
I also note that sometimes you do need 'soft', user-maintainable states for things. However, I would suggest that these should be for display purposes only and that you shouldn't write 'hard' code around these 'soft' states.
(If your application is intended to be a highly-customisable, off-the-shelf product, you might look at making soft states available to a scripting or rules engine, so the users who define the states can also define the business rules around them, but this is well outside of scope.)
With all that said, if you really, really feel that you need to do what you are asking, one possible approach is below...
Firstly, you need a canonical source for your enum values. Either your C# Enum is correct, or the SQL definition is correct.
Once this is decided, I would probably use some form of text templating, like T4 or a custom script file/compiled exe to generate the one that isn't the canonical source.
C# code is canonical
Reflect your Enum using Enum.GetValues(typeof(StatusCode))
and Enum.GetName(typeof(StatusCode), value)
, then use this to generate a CHECK
constraint in your target table. Store the base int
type in your database table (e.g. ALTER TABLE my_table ADD status_code INT
).
// (untested, pseudo-ish code -- WATCH FOR INJECTION!)
StringBuilder sql = new StringBuilder();
sql.Append(@"ALTER TABLE [my_table] ADD CONSTRAINT chk_status_code CHECK (status_code IN (");
bool first = true;
foreach (var v in Enum.GetValues(typeof(StatusCode)) {
if (!first) sql.Append(", ");
sql.Append(v);
first = false;
}
sql.Append("));");
// write sql to file, or run against the development database
This would get you very close to a SQL statement that could be run on build / install. Note that this is not intended to be run during the normal operation of your database.
If you need the functionality, you may also want to generate an inline table function to map number to name, e.g.:
// (untested, pseudo-ish code -- WATCH FOR INJECTION!)
StringBuilder sql = new StringBuilder();
sql.AppendLine(@"IF OBJECT_ID('dbo.tf_status_codes') IS NULL EXECUTE('
CREATE FUNCTION dbo.tf_status_codes RETURNS TABLE AS RETURN (
SELECT ''not yet built'' AS err
)
')");
sql.AppendLine(@"ALTER FUNCTION dbo.tf_status_codes RETURNS TABLE AS RETURN (")
.AppendLine(@" SELECT value, name FROM (VALUES ")
bool first = true;
foreach (var v in Enum.GetValues(typeof(StatusCode)) {
if (!first) sql.AppendLine(",");
sql.Append(@" ({0}, '{1}')",
v,
Enum.GetName(typeof(StatusCode), v));
first = false;
}
sql.AppendLine(@" ) e(value, name);")
.AppendLine(@")";
// write sql to file, or run against the development database
Run the build tool as a Post-Build event, so the code is updated before the constraint / table.
SQL table is canonical
This one is more straight-forward, although I would make sure that the table source cannot change during the lifetime of the production iteration (i.e. only defined at deployment time). For this, I would define my enum values as an inline table function, rather than a table:
CREATE FUNCTION dbo.status_codes
RETURNS TABLE
AS RETURN (
SELECT value, name
FROM (VALUES (20, 'Open')
,(21, 'Received')
,(22, 'Delivered')
,(23, 'Cancelled')) AS v(value, name)
)
Then, in my build tool, connect to the database, retrieve the values and generate the enum:
// untested, pseudo, assumes an existing database connection routine
IDataReader reader = DB.GetReader("SELECT value, name FROM dbo.status_codes()");
StringBuilder code = new StringBuilder();
code.AppendLine("namespace MyApp {")
.AppendLine(" public enum StatusCodes : int {");
bool first = true
while (reader.Read()) {
if (!first) code.AppendLine(",");
code.Append(" {0} = {1}", reader["name"], reader["value"]);
first = false;
}
code.AppendLine(" }")
.AppendLine("}");
// ...write the code to the Enum class file, and exit with 0 code
Run the build tool as a Pre-Build event (so the code is generated before it is built).
(As I said, the code above is untested and no attempt has been made to secure it for injection. Use at your own risk and test thoroughly)