I have to import a hundreds records to database from Excel.
Each record has to be verified:
Against duplicate
Has to has foreign key in another table
I’m wondering how should I do this with the highest performance. I know that I shouldn’t use db.SaveChanges(); after each record so after verification - I’m adding each record to temporary list (var recordsToAdd), and I’m saving that list after all.
Please check my code below, is this good approach to do this?
using (var db = new DbEntities())
{
var recordsToAdd = new List<User>();
for (var row = 2; row <= lastRow; row++)
{
var newRecord = new User
{
Id = Int32.Parse(worksheet.Cells[idColumn + row].Value.ToNullSafeString()),
FirstName = worksheet.Cells[firstNameColumn + row].Value.ToNullSafeString(),
LastName = worksheet.Cells[lastNameColumn + row].Value.ToNullSafeString(),
SerialNumber = worksheet.Cells[serialNumber + row].Value.ToNullSafeString()
};
bool exists = db.User.Any(u => u.Id == newRecord.Id) || recordsToAdd.Any(u => u.Id == newRecord.Id);
if (!exists)
{
bool isSerialNumberExist = db.SerialNumbers.Any(u => u.SerialNumber == newRecord.SerialNumber);
if (isSerialNumberExist)
{
recordsToAdd.Add(newRecord);
}
else
{
resultMessages.Add(string.Format("SerialNumber doesn't exist"));
}
}
else
{
resultMessages.Add(string.Format("Record already exist"));
}
}
db.User.AddRange(recordsToAdd);
db.SaveChanges();
}
First of all let's separate the code into two parts. First part is creating a list of valid User records to be inserted. Second part is inserting those records to the database (last two lines of your code).
Assuming you are using EntityFramework as your ORM, second part may be optimized by bulk inserting the records. It has many existing solutions that can be easily found. (example)
There are some suggestions concerning the first part.
Load user ids in a HashSet or Dictionary. These data structures are optimized for searching. var userDbIds = new HashSet<int>(db.User.Select(x => x.Id));. You will quickly check if id exists without making a request to DB.
Do the same for serialNumber. var serialNumbers = new HashSet<string>(db.SerialNumber.Select(x => x.SerialNumber)); assuming that type of SerialNumber property is string.
Change the type of your recordToAdd variable to be Dictionary<int, User> for the same reason.
In the check would look like this:
bool exists = userDbIds.Contains(newRecord.Id) || recordsToAdd.ContainsKey(newRecord.Id);
if (!exists)
{
bool isSerialNumberExist = serialNumbers.Contains(newRecord.SerialNumber);
if (isSerialNumberExist)
{
recordsToAdd[newRecord.Id] = newRecord;
}
else
{
resultMessages.Add(string.Format("SerialNumber doesn't exist"));
}
}
else
{
resultMessages.Add(string.Format("Record already exist"));
}
One way to improve the performance is to minimize the db calls and linear searches by using a fast lookup data structures for performing the verification - HashSet<string> for Id and Dictionary<string, bool> for SerialNumber:
using (var db = new DbEntities())
{
var recordsToAdd = new List<User>();
var userIdSet = new HashSet<string>();
var serialNumberExistsInfo = new Dictionary<string, bool>();
for (var row = 2; row <= lastRow; row++)
{
var newRecord = new User
{
Id = Int32.Parse(worksheet.Cells[idColumn + row].Value.ToNullSafeString()),
FirstName = worksheet.Cells[firstNameColumn + row].Value.ToNullSafeString(),
LastName = worksheet.Cells[lastNameColumn + row].Value.ToNullSafeString(),
SerialNumber = worksheet.Cells[serialNumber + row].Value.ToNullSafeString()
};
bool exists = !userIdSet.Add(newRecord.Id) || db.User.Any(u => u.Id == newRecord.Id);
if (!exists)
{
bool isSerialNumberExist;
if (!serialNumberExistsInfo.TryGetValue(newRecord.SerialNumber, out isSerialNumberExist))
serialNumberExistsInfo.Add(newRecord.SerialNumber, isSerialNumberExist =
db.SerialNumbers.Any(u => u.SerialNumber == newRecord.SerialNumber));
if (isSerialNumberExist)
{
recordsToAdd.Add(newRecord);
}
else
{
resultMessages.Add(string.Format("SerialNumber doesn't exist"));
}
}
else
{
resultMessages.Add(string.Format("Record already exist"));
}
}
db.User.AddRange(recordsToAdd);
db.SaveChanges();
}
It would be most efficient to use a Table-Valued Parameter instead of LINQ. That way you can handle this in a set-based approach that is a single connection, single stored procedure execution, and single transaction. The basic setup is shown in the example code I provided in the following answer (here on S.O.):
How can I insert 10 million records in the shortest time possible?
The stored procedure can handle both validations:
don't insert duplicate records
make sure that SerialNumber exists
The User-Defined Table Type (UDTT) would be something like:
CREATE TYPE dbo.UserList AS TABLE
(
Id INT NOT NULL,
FirstName NVARCHAR(50) NOT NULL,
LastName NVARCHAR(50) NULL,
SerialNumber VARCHAR(50) NOT NULL
);
-- Uncomment the following if you get a permissions error:
-- GRANT EXECUTE ON TYPE::[dbo].[UserList] TO [ImportUser];
GO
The stored procedure (executed via SqlCommand.ExecuteNonQuery) would look something like:
CREATE PROCEDURE dbo.ImportUsers
(
#NewUserList dbo.UserList READONLY
)
AS
SET NOCOUNT ON;
INSERT INTO dbo.User (Id, FirstName, LastName, SerialNumber)
SELECT tmp.Id, tmp.FirstName, tmp.LastName, tmp.SerialNumber
FROM #NewUserList tmp
WHERE NOT EXISTS (SELECT *
FROM dbo.User usr
WHERE usr.Id = tmp.[Id])
AND EXISTS (SELECT *
FROM dbo.SerialNumbers sn
WHERE sn.SerialNumber = tmp.[SerialNumber]);
The stored procedure above simply ignores the invalid records. If you need notification of the "errors", you can use the following definition (executed via SqlCommand.ExecuteReader):
CREATE PROCEDURE dbo.ImportUsers
(
#NewUserList dbo.UserList READONLY
)
AS
SET NOCOUNT ON;
CREATE TABLE #TempUsers
(
Id INT NOT NULL,
FirstName NVARCHAR(50) NOT NULL,
LastName NVARCHAR(50) NULL,
SerialNumber VARCHAR(50) NOT NULL,
UserExists BIT NOT NULL DEFAULT (0),
InvalidSerialNumber BIT NOT NULL DEFAULT (0)
);
INSERT INTO #TempUsers (Id, FirstName, LastName, SerialNumber)
SELECT tmp.Id, tmp.FirstName, tmp.LastName, tmp.SerialNumber
FROM #NewUserList tmp;
-- Mark existing records
UPDATE tmp
SET tmp.UserExists = 1
FROM #TempUsers tmp
WHERE EXISTS (SELECT *
FROM dbo.User usr
WHERE usr.Id = tmp.[Id]);
-- Mark invalid SerialNumber records
UPDATE tmp
SET tmp.InvalidSerialNumber = 1
FROM #TempUsers tmp
WHERE tmp.UserExists = 0 -- no need to check already invalid records
AND NOT EXISTS (SELECT *
FROM dbo.SerialNumbers sn
WHERE sn.SerialNumber = tmp.[SerialNumber]);
-- Insert remaining valid records
INSERT INTO dbo.User (Id, FirstName, LastName, SerialNumber)
SELECT tmp.Id, tmp.FirstName, tmp.LastName, tmp.SerialNumber
FROM #TempUsers tmp
WHERE tmp.UserExists = 0
AND tmp.InvalidSerialNumber = 0;
-- return temp table to caller as it contains validation info
SELECT tmp.Id, tmp.FirstName, tmp.LastName, tmp.SerialNumber,
tmp.UserExists, tmp.InvalidSerialNumber
FROM #TempUsers tmp
-- optionally only return records that had a validation error
-- WHERE tmp.UserExists = 1
-- OR tmp.InvalidSerialNumber = 1;
When this version of the stored procedure completes, cycle through SqlDataReader.Read() to get the validation info.
Related
I have two objects: SettlementRole and SettlementType (see below for sqlite schema). There is a many to many relationship between the two, where each SettlmentRole has a collection of SettlementTypes, each with a weight specific to that pair, but each SettlementType may be in a number of SettlementRoles, each with different weights.
I'm not using an ORM for various reasons, and ORMs are not in scope here.
Question: Is there a cleaner, simpler way of doing the same process than the following? This is mostly a learning project and there are a bunch of other similar (but not identical) entity pairs/groups in the project, so I'd like to get one as clean as possible so I can follow the same process for the more complex ones.
The function to get all SettlementRoles looks like
public async Task<IList<SettlementRole>> GetAllRolesAsync()
{
using var connection = new SqliteConnection(dbName);
connection.Open();
var command = connection.CreateCommand();
command.CommandText = #"
SELECT sr.id, sr.name, sr.description, t.id, t.name, t.description, t.minSize, t.maxSize, map.weight
FROM settlementRole sr
JOIN settlementTypeRoleWeight map
ON map.roleId = sr.id
JOIN settlementType t
ON t.id = map.typeId
;";
var rawRoles = await command.ExecuteReaderAsync();
var roles = new List<SettlementRole>();
var map = new Dictionary<int, SettlementRole>();
while (rawRoles.Read())
{
var id = rawRoles.GetInt32(0);
var exists = map.TryGetValue(id, out var role);
if (!exists)
{
role = new SettlementRole() { Id = id, Name = rawRoles.GetString(1), Description = rawRoles.GetString(2) };
map.Add(id, role);
}
role!.AddType(new SettlementType()
{
Name = rawRoles.GetString(3),
Description = rawRoles.GetString(4),
MinSize = rawRoles.GetInt32(5),
MaxSize = rawRoles.GetInt32(6),
}, rawRoles.GetDouble(7));
}
return new List<SettlementRole>(map.Values);
Where settlementRole.AddType(SettlementType type, double weight) handles the internal weighting process of adding a type to the (default empty) collection.
and the schema for the three (including the join table) looks like
CREATE TABLE settlementRole(
id INTEGER NOT NULL PRIMARY KEY ASC AUTOINCREMENT,
name TEXT NOT NULL,
description TEXT NOT NULL DEFAULT ""
) ;
CREATE TABLE settlementType(
id INTEGER NOT NULL PRIMARY KEY ASC AUTOINCREMENT,
name TEXT NOT NULL,
description TEXT NOT NULL,
minSize INTEGER NOT NULL DEFAULT 1,
maxSize INTEGER,
professions TEXT NOT NULL DEFAULT ""
);
CREATE TABLE settlementTypeRoleWeight(
roleId INTEGER NOT NULL,
typeId INTEGER NOT NULL,
weight INTEGER NOT NULL DEFAULT 1,
PRIMARY KEY (roleId, typeId)
);
And the entities themselves (right now) are simple data objects (they'll have behavior, but that's out of scope here).
I've created a database trigger on my table that updates a field in the table after an insert. When doing an insert using EF I'll get the ID and the number. On the database I've created this code:
create table Things (
ID int primary key identity not null,
Number nvarchar(20)
);
create trigger UpdateThingsNumberTrigger on Things
after insert
as
begin
declare #month nvarchar(2);
select #month = cast(month(getdate()) as nvarchar(2));
declare #code nvarchar(15);
select #code = cast(year(getdate()) as nvarchar(4)) +
'.' +
replicate('0', 2 - len(#month)) +
#month +
'.';
declare #max nvarchar(20);
select #max = t.ID
from Things t
where ID like #code + '%';
with CTE_UPD as
(
select
replicate('0',
4 -
len(cast(coalesce(cast(right(#max, 4) as int), 0) + row_number() over (order by ins.ID) as nvarchar(4)))) +
cast(coalesce(cast(right(#max, 4) as int), 0) + row_number() over (order by ins.ID) as nvarchar(4)) as NextNo,
ID
from Things ins
)
update Things
set Number = #code + NextNo
from Things t inner join CTE_UPD ins on ins.ID = t.ID;
end
Note: For the logical flaw inside the trigger, I'll refer to Create an incremental number with year and month without updating the entire table using a trigger on Database Administrators SE.
This part of my code works fine, ignoring the logical flaw inside the trigger… The problem I'll try to solve in this question, is when I insert a thing in my table from Entity Framework (database first). There is my code and the output:
using (Database db = new Database())
{
Thing thing = new Thing(); // --> just an empty constructor.
db.Entry(thing).State = EntityState.Added;
await db.SaveChangesAsync();
Console.WriteLine($"ID = {thing.ID}");
Console.WriteLine($"Number = {thing.Number}");
}
// Output:
// ID = 1
// Number =
In the background EF is doing this code on the server when calling SaveChangesAsync():
INSERT [dbo].[Things]([Number])
VALUES (NULL)
SELECT [ID]
FROM [dbo].[Things]
WHERE ##ROWCOUNT > 0 AND [ID] = scope_identity()
Now can EF update the ID in the C# object. But how could I get the number without using code below before closing the using block?
Thing recentlyInsertedThing = await db.Things.FindAsync(thing.ID);
I've found it to get the ID and the Number without writing a 2nd select statement. This is my code:
using (Database db = new Database())
{
Thing thing = new Thing();
string sql = #"insert into Things()
values ();
select ID, Number
from Things
where ##rowcount > 0 and ID = scope_identity();";
KeyMapper recentlyRecevedKeys = await db
.Database
.SqlQuery<KeyMapper>(sql)
.FirstAsync();
thing.ID = recentlyRecevedKeys.ID;
thing.Number = recentlyRecevedKeys.Number;
}
// Nested class
private class KeyMapper
{
public int ID { get; set; }
public string Number { get; set; }
}
Stored procedure works and deletes what I want but I still get this error after deleting:
The data reader is incompatible with the specified 'AMSIdentity.Models.RemoveRoleFromUserViewModel'. A member of the type, 'RoleId', does not have a corresponding column in the data reader with the same name.
I need to run the code without this error in the above
This code using ASP.NET MVC 5 and EF6 code first approach; I tried to use this code but always throws this error after delete.
This is the action method that I use
public ActionResult RemoveRoleFromUserConfirmed(string UserName, string RoleId)
{
if (UserName == null && RoleId == null)
{
return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
}
SqlParameter param1 = new SqlParameter("#RoleId", RoleId);
SqlParameter param2= new SqlParameter("#UserName", UserName);
var remove = Identitydb.Database.SqlQuery<RemoveRoleFromUserViewModel>("admin.sp_RemoveUserFromRole #RoleId, #UserName",
((ICloneable)param1).Clone(),
((ICloneable)param2).Clone()).ToArray().ToList().FirstOrDefault();
if (remove == null)
{
return HttpNotFound();
}
return RedirectToAction("Roles");
}
This is the view model that I use :
public class RemoveRoleFromUserViewModel
{
[Key]
[DisplayName("Role Id")]
public string RoleId { get; set; }
[DisplayName("Username")]
public string UserName { get; set; }
}
This is the stored procedure code:
ALTER PROCEDURE [Admin].[sp_RemoveUserFromRole]
#RoleId NVARCHAR(50),
#UserName NVARCHAR(50)
AS
BEGIN
DELETE FROM AspNetUserRoles
WHERE UserId = (SELECT Id
FROM AspNetUsers
WHERE UserName = #UserName)
AND RoleId = #RoleId
END
I expect that this code will delete role from the specific user.
When you perform a DELETE in the stored procedure, you need to "audit" what got deleted. Then perform a SELECT on that audit-table.
You are taking advantage of the OUTPUT feature of sql server.
see:
https://learn.microsoft.com/en-us/sql/t-sql/statements/delete-transact-sql?view=sql-server-2017
and/or
https://www.sqlservercentral.com/articles/the-output-clause-for-insert-and-delete-statements
Below is a generic example of the TSQL you need.
DROP TABLE IF EXISTS [dbo].[Patient]
GO
CREATE TABLE [dbo].[Patient]
(
[PatientKey] BIGINT NOT NULL IDENTITY(1, 1),
[PatientUniqueIdentifier] VARCHAR(256) NOT NULL,
[CreateDate] DATETIMEOFFSET NOT NULL,
CONSTRAINT [UC_Patient_PatientUniqueIdentifier] UNIQUE (PatientUniqueIdentifier)
)
/* now insert 3 sets of rows, with different create-dates */
INSERT INTO dbo.Patient (PatientUniqueIdentifier, [CreateDate]) SELECT TOP 10 NEWID() , '01/01/2001' from sys.objects
INSERT INTO dbo.Patient (PatientUniqueIdentifier, [CreateDate]) SELECT TOP 10 NEWID() , '02/02/2002' from sys.objects
INSERT INTO dbo.Patient (PatientUniqueIdentifier, [CreateDate]) SELECT TOP 10 NEWID() , '03/03/2003' from sys.objects
SELECT 'SeedDataResult' as Lable1, * FROM dbo.Patient
/* everything above is just setting up the example */
/* below would be the "guts"/implementation of your stored procedure */
DECLARE #PatientAffectedRowsCountUsingAtAtRowCount BIGINT
DECLARE #PatientAffectedRowsCountUsingCountOfOutputTable BIGINT
DECLARE #PatientCrudActivityAuditTable TABLE ( [PatientUniqueIdentifier] VARCHAR(256), DatabaseKey BIGINT , MyCrudLabelForKicks VARCHAR(16));
/* now delete a subset of all the patient rows , your delete will be whatever logic you implement */
DELETE FROM [dbo].[Patient]
OUTPUT deleted.PatientUniqueIdentifier , deleted.PatientKey , 'mydeletelabel'
INTO #PatientCrudActivityAuditTable ([PatientUniqueIdentifier] ,DatabaseKey , MyCrudLabelForKicks )
WHERE
CreateDate = '02/02/2002'
/*you don't need this update statement, but i'm showing the audit table can be used with delete and update and insert (update here) */
/*
UPDATE [dbo].[Patient]
SET CreateDate = '03/03/2003'
OUTPUT inserted.PatientUniqueIdentifier , inserted.PatientKey, 'myupdatelabel'
INTO #PatientCrudActivityAuditTable ([PatientUniqueIdentifier] ,DatabaseKey , MyCrudLabelForKicks)
FROM [dbo].[Patient] realTable
WHERE CreateDate != '03/03/2003'
*/
/* optionally, capture how many rows were deleted using ##ROWCOUNT */
SELECT #PatientAffectedRowsCountUsingAtAtRowCount = ##ROWCOUNT
/* or, capture how many rows were deleted using a simple count on the audit-table */
SELECT #PatientAffectedRowsCountUsingCountOfOutputTable = COUNT(*) FROM #PatientCrudActivityAuditTable
SELECT 'ResultSetOneForKicks' as Label1, 'Rows that I Deleted' as MyLabel_YouCanRemoveThisColumn, DatabaseKey , PatientUniqueIdentifier FROM #PatientCrudActivityAuditTable
/* if so inclined, you can also send back the delete-COUNTS to the caller. You'll have to code your IDataReader (ORM, whatever) to handle the multiple return result-sets */
/* most people will put the "counts" as the first result-set, and the rows themselves as the second result-set ... i have them in the opposite in this example */
SELECT 'ResultSetTwoForKicks' as Label1, #PatientAffectedRowsCountUsingAtAtRowCount as '#PatientAffectedRowsCountUsingAtAtRowCountCoolAliasName' , #PatientAffectedRowsCountUsingAtAtRowCount as '#PatientAffectedRowsCountUsingAtAtRowCountCoolAliasName'
In my example, you would write the dotNet serialize code...(whatever flavor you use, raw IDataReader, ORM tool, whatever) against the PatientKey and PatientUniqueIdentifier columns coming back from the #PatientSurrogateKeyAudit table.
Hi All,
I got the answer from #Jeroen Mostert, The solution is to use the
(ExecuteSqlCommand) rather than (SqlQuery) because I will never return
data, I only execute the stored procedure with two parameters.
This is the answer
SqlParameter param1 = new SqlParameter("#RoleId", RoleId);
SqlParameter param2= new SqlParameter("#UserName", UserName);
//I change this line from SqlQuery to ExecuteSqlCommand
var remove = Identitydb.Database.ExecuteSqlCommand("admin.sp_RemoveUserFromRole #RoleId, #UserName", param1, param2);
Thank you very much #Jeroen Mostert.
Regards,
Ali Mosaad
Software Developer
I'm very sorry for the title, can't figure out a better one. Any amendment is greatly appreciated!
Says i have these tables :
Header(id int identity(1,1), Date datetime, ...)
Detail(id int identity(1,1), HeaderID int, MaterialID nvarchar(24), Quantity float, AccountID nvarchar(20), Amount float)
Ledger(HeaderID, AccountID nvarchar(20), Amount float)
InventoryTransactionDetail(HeaderID, MaterialID nvarchar(24), Quantity float)
Here is how this work :
Header contains general information of a voucher
Detail contains voucher's detail records
We will then analyze Detail's record and produce data for Ledger and Inventory
For example :
Insert into Header(Date, ...) values (getdate(), ...)
Assuming the newly created header ID is 1
Insert into Detail(HeaderID, MaterialID, Quantity, AccountID, Amount)
values(1, 'MAT1', 50, '1561', 500000)
After analyzing, we have Ledger and InventoryTransactionDetail's records:
Insert into Ledger(HeaderID, AccountID, Amount)
values(1, '1561', 500000)
Insert into InventoryTransactionDetail(HeaderID, MaterialID, Quantity)
values(1, 'MAT1', 50)
So if there is any changes was made on the voucher, i will :
Update the header and the detail
Delete from Ledger and Inventory, and insert new analyzed records into them
Sound simple, right? I can achieve this using T-sql in just a glance, but doing this using EF is giving me a nightmare, i keep getting errors i can't figure out why and how to fix it!
So what i want to ask is, am i doing this the right way?
By the way, this is one of the errors i'm getting :
A referential integrity constraint violation occurred: The property
values that define the referential constraints are not consistent
between principal and dependent objects in the relationship.
Line of error (the line before the last : Attach(VH) ) :
using (var context2 = new GModel())
{
List<Ledger> AJToCheck = (from a in context2.Ledger where a.VHID == VH.ID select a).ToList();
foreach (Ledger DetailToCheck in AJToCheck)
{
context2.DeleteObject(DetailToCheck);
}
List<ITDetail> ITToCheck = (from a in context2.ITDetail where a.VHID == VH.ID select a).ToList();
foreach (ITDetail DetailToCheck in ITToCheck)
{
context2.DeleteObject(DetailToCheck);
}
context2.SaveChanges();
}
using (var context = new GModel())
{
if (VH.ID == 0 || VH.ID == null)
{
VH.State = State.Added;
context.VoucherHeader.AddObject(VH);
}
else
{
VH.State = State.Modified;
int counterID = -1;
foreach (var voucherDetail in VH.VoucherDetail)
{
if (voucherDetail.ID == 0)
{
counterID--;
voucherDetail.State = State.Added;
voucherDetail.ID = counterID;
}
else voucherDetail.State = State.Modified;
}
counterID = -1;
foreach (var Ledger in VH.Ledger)
{
counterID--;
Ledger.State = State.Added;
Ledger.ID = counterID;
}
counterID = -1;
foreach (var itDetail in VH.ITDetail)
{
counterID--;
itDetail.State = State.Added;
itDetail.ID = counterID;
}
context.VoucherHeader.Attach(VH);
context.ObjectStateManager.ChangeObjectState(VH, StateHelpers.GetEquivalentEntityState(VH.State));
This appears to be much more complex than it needs to.
This error generally occurs when the entity in the many part of a one to many relationship is being attached with a foreign key different from the parent object.
In your case the SQL examples you show, you insert into the dependent tables and only set the hearderID, but in all your EF examples you assign negative numbers to the ID properties of all your navigation properties?
To repeat what you have with SQL you would be better off just going straight to the attach of your VH rather than attempting to manipulate the navigation properties.
When VH is attached, any of the navigation properties in the graph that didn't exist (IDs == 0) will be automatically added (inserted). Your marking both the VH and its navigation properties as modified is unnecessary, and likely contributes to your problem.
Try this instead:
if (VH.ID == 0 || VH.ID == null)
{
VH.State = State.Added;
context.VoucherHeader.AddObject(VH);
}
else
{
VH.State = State.Modified;
context.VoucherHeader.Attach(VH);
context.ObjectStateManager.ChangeObjectState(VH, StateHelpers.GetEquivalentEntityState(VH.State));
}
Also take a look at http://msdn.microsoft.com/en-us/magazine/dn166926.aspx for that version of EF
enter code hereIn my SQL Server database I have my address information for the subNumber (e.g. Unit 802) and the streetNumber (e.g. 242 Elizabeth Street) stored separately.
I need to display these as one (i.e. 802/242 Elizabeth Street) if the subNumber contains a value, otherwise just return the streetNumber if it does not.
I've been working toward a solution using IF ELSE and a foreach loop after accessing the data through LINQ - but I'm stuck after the point where I have completed the loop. I would also be happy to do this with a SELECT Stored Procedure in SQL - open to suggestions!
DataClassesDataContext dc = new DataClassesDataContext();
var recent = from p in dc.Properties
orderby p.modtime descending
where p.status == "Current"
select new
{
rsub = (p.subNumber).ToString(),
rnumber = (p.streetNumber).ToString(),
rstreet = p.street,
rsuburb = p.suburb,
rurl = p.propertyURL,
};
foreach (var home in recent)
{
if (string.IsNullOrEmpty(home.rsub))
{
string rnum = home.rnumber;
}
else
{
string rnum = home.rsub + "/" + home.rnumber;
}
}
recentrepeater.DataSource = recent;
recentrepeater.DataBind();
Yahia gave the best option in c# - this is the SQL solution I have finally ended up with:
ALTER PROCEDURE GetPropertyShort
AS
SELECT TOP 5 ISNULL(convert(varchar(5), subNumber) + '/' + convert(varchar(5), streetNumber), convert(varchar(5), streetNumber)) as Number, street, suburb, propertyURL, modtime
FROM Property
ORDER BY modtime DESC
try
var recent = from p in dc.Properties
orderby p.modtime descending
where p.status == "Current"
select new
{
rsub = (p.subNumber).ToString(),
rnumber = (p.streetNumber).ToString(),
rnum = string.IsNullOrEmpty((p.subNumber).ToString()) ? (p.streetNumber).ToString() : (p.subNumber).ToString() + "/" + (p.streetNumber).ToString(),
rstreet = p.street,
rsuburb = p.suburb,
rurl = p.propertyURL,
};
Just a ternary conditional aka ?: operator in the "select" should do:
select new
{
house = p.subNumber != null
? p.subNumber + "/" + p.streetNumber
: p.streetNumber;
...
};
This makes the assumption street number is always there (or it might result in "xxx/" or null). It also assumes that sub is null (not just empty) if truly not present.
If it starts to get "too complicated", consider the following (which has slightly different rules than above, those are left to be figured out):
select new
{
house = PrettyHouseNumber(p.subNumber, p.streetNumber),
...
};
string PrettyHouseNumber(string sub, string street) {
// ?: could also be used here as well, but since invoking the method
// can be used as an expression itself, breaking it up like this also
// allows the use of other constructs
if (!string.IsNullOrEmpty(sub)) {
return sub + "/" + street;
} else {
return "" + street; // NULL will go to "", if it can even ever come up
}
}
Which should show how any expression, including a method call, can be used there -- pass it some data and get some data back :) While there are limits with expression trees and which ones can be efficiently turned into SQL, since this is just processing data already returned then there is nothing to worry about here.
Happy coding.
On the table level you could create a computed column; these are not stored - the value is 'created' when the column is queried.
CREATE TABLE [Customer]
(
[subNumber] NVARCHAR(256),
[streetNumber] NVARCHAR(256),
[fullAddress] AS (CASE
WHEN [subNumber] IS NULL THEN [streetNumber]
ELSE [subNumber] + N' ' + [streetNumber]
END)
);
GO
Or you can add it the table:
ALTER TABLE [Customer]
ADD COLUMN [fullAddress]
AS (CASE
WHEN [subNumber] IS NULL THEN streetNumber
ELSE [subNumber] + N' ' + streetNumber
END);
GO
Now the value will be directly-accessible from your EF model.