Tagged: SQL Server

A Quick Note on COALESCE and ISNULL

I’ve noticed couple of commentators on my blog post on Entity Framework and setting primary keys on views pointing out the difference between COALESCE and ISNULL. Apparently they were using COALESCE and expecting it to return non-nullable value, but instead, got the different result. I would like to clarify this a bit further.

In my example, I’ve been using ISNULL. It is a function which returns some replacement value in case an expression equals NULL. This basically means that (given the replacement value is not NULL) return value will always be non-nullable.

On the other hand, COALESCE returns the first expression which is not NULL. Only if all expressions are non-nullable, the result is considered non-nullable. Here’s the explanation from msdn site:

The NULLability of the result expression is different for ISNULL and COALESCE. The ISNULL return value is always considered NOT NULLable (assuming the return value is a non-nullable one) whereas COALESCE with non-null parameters is considered to be NULL. So the expressions ISNULL(NULL, 1) and COALESCE(NULL, 1) although equivalent have different nullability values.

To demonstrate this, here’s a quick example. I’ve created a view extracting a column (of bit type, nullable), using ISNULL, COALESCE and NULLIF (I’ve added the last one since it was used in my previously mentioned post).

CREATE VIEW test1
AS
SELECT ISNULL([Activated],1) AS Col1,
COALESCE([Activated],1) AS Col2,
NULLIF([Activated],1) AS Col3
FROM [User]

The resulting view:

test1

As expected, ISNULL returns non-nullable column, unlike COALESCE and NULLIF.

Also worth noting: ISNULL and NULLIF return type matches the column type, while COALESCE returns the data type of expression with the highest data type precedence.

Another note: I’m not familiar with Oracle, so can’t give an advice on which function to use instead of ISNULL. If you have an idea on that, feel free to comment this post.

Crop & Save (Part II)

This is the second (and the last) post discussing cropping images and saving them to the SQL Server database, using C# ASP.NET and jQuery library called Jcrop. In my previous post, I have created a simple database model that can be used for this purpose, so if you haven’t checked it out, please do. Also, I won’t get into details with Jcrop, since its website is very informative and contains a good manual, as well as demos.

I’ve made a simple demo which can be downloaded here, and I’ll quickly guide you through it, so it could be easily understood. The idea is the following: user clicks on ‘Choose file’ button (which opens FileUpload control), picks an image on their computer and clicks on ‘Upload’ button. At that point, the image is uploaded to the app server and saved to a folder in the file system(in this demo, it is TempImages folder). User can then see the uploaded picture and do the cropping, after which ‘Crop’ button should be clicked. Then, a new bitmap will be created, containing only the area of the original photo selected as the crop area. The bitmap will then be saved to the database as the array of bytes.

FIrst of all, here is the aspx page:

<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="Default.aspx.cs" Inherits="JcropDemo._Default" %>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<html xmlns="http://www.w3.org/1999/xhtml" >
<head runat="server">
    <title>Jcrop Demo</title>
    <script type="text/javascript" src="//ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"></script>
    <script type="text/javascript" src="//ajax.googleapis.com/ajax/libs/jqueryui/1.10.2/jquery-ui.min.js"></script>
    <script type="text/javascript" src="js/jquery.Jcrop.min.js"></script>
    <link rel="Stylesheet" type="text/css" href="css/jquery.Jcrop.min.css" />
    <link rel="Stylesheet" type="text/css" href="css/custom.css" />
    
    <script type="text/javascript">
        jQuery(document).ready(function() {
            
            $preview = $('#preview_pane'),
            $pcnt = $('#preview_pane .preview_container'),
            $pimg = $('#preview_pane .preview_container img'),

            xsize = $pcnt.width(),
            ysize = $pcnt.height();
            
            jQuery(window).load(function() {
                jQuery($('img[bid=EditedImage]')).Jcrop({
                    onChange: doPreviewSave,
                    onSelect: doPreviewSave,
                    boxWidth: 600,
                    boxHeight: 400,
                    aspectRatio: xsize / ysize
                }, function() {

                    // Use the API to get the real image size
                    var bounds = this.getBounds();
                    boundx = bounds[0];
                    boundy = bounds[1];
                    // Store the API in the jcrop_api variable
                    jcrop_api = $('img[bid=EditedImage]');          //this;            

                    // Move the preview into the jcrop container for css positioning
                    $preview.appendTo(jcrop_api.ui.holder);

                });
            });

            function doPreviewSave(c) {
                jQuery($('[id$=HIDDENX]')).val(c.x);
                jQuery($('[id$=HIDDENY]')).val(c.y);
                jQuery($('[id$=HIDDENW]')).val(c.w);
                jQuery($('[id$=HIDDENH]')).val(c.h);

                if (parseInt(c.w) > 0) {
                    var rx = xsize / c.w;
                    var ry = ysize / c.h;

                    $pimg.css({
                        width: Math.round(rx * boundx) + 'px',
                        height: Math.round(ry * boundy) + 'px',
                        marginLeft: '-' + Math.round(rx * c.x) + 'px',
                        marginTop: '-' + Math.round(ry * c.y) + 'px'
                    });
                }
            };
        });       
 
</script>
</head>
<body>
    <form id="form1" runat="server">
    <div>
        <asp:Panel ID="CurrentImagePanel" runat="server">
            <fieldset>
                <legend id="CurrentPhotoLegend" runat="server"></legend>
                    <p>
                    <asp:Image ID="UserImage" runat="server" CssClass="user_photo_large" />
                    <asp:FileUpload ID="ImageUpload" runat="server" />
                </p>
                <p>
                    <asp:Button ID="ImageUploadButton" runat="server" onclick="ImageUploadButton_Click" Text="Upload"/>
                </p>    
                <p>
                    <asp:Label ID="ImageUploadErrorLabel" runat="server"></asp:Label>
                </p>
            </fieldset>            
        </asp:Panel>
        <asp:Panel ID="EditedImagePanel" runat="server">   
            <fieldset>
                <legend><asp:Label ID="EditedPhotoLabel" runat="server"></asp:Label></legend>
                <asp:Image ID="EditedImage" runat="server" bid="EditedImage" />
                <div id="preview_pane">
                    <div class="preview_container">
                        <asp:Image ID="PreviewImage" runat="server" CssClass="jcrop-preview" />
                    </div>
                </div>          
                <p style="clear:both">
                    <asp:Button ID="CropImageButton" runat="server" OnClientClick="return checkCroppedImage();" onclick="CropImageButton_Click" CssClass="custom_button3" Text="Crop"/>
                    <asp:HiddenField ID="HIDDENX" runat="server" />
                    <asp:HiddenField ID="HIDDENY" runat="server" />
                    <asp:HiddenField ID="HIDDENW" runat="server" />
                    <asp:HiddenField ID="HIDDENH" runat="server" />
                </p>  
            </fieldset>              
        </asp:Panel>
    </div>
    </form>
</body>
</html>

So, we have to add references to jQuery and jQueryui, as well as to Jcrop js and css files. There are two panels, CurrentImagePanel, shown before the image has been selected, and EditedImagePanel, where the cropping will take place. Hidden fields HIDDENX, HIDDENY, HIDDENW and HIDDENH are used to store coordinates of the rectangle representing new (cropped) image. On window load, Jcrop’s default behavior is activated. When the selection is made, hidden fields are populated with its values (check doPreviewSave function).

Code behind:

using System;
using System.Linq;
using System.IO;
using System.Web;

namespace JcropDemo
{
    public partial class _Default : System.Web.UI.Page
    {
        private string[] _AllowedExtensions = { ".png", ".jpeg", ".jpg", ".gif" };
        private string _TempImageLocation = "TempImages\\";
        private string _ImagePath = HttpContext.Current.Request.PhysicalApplicationPath + "TempImages\\";
        private int _UserID = 1;

        protected void Page_Load(object sender, EventArgs e)
        {
            if (Session["WorkingImage"] != null)
            {
                EditedImagePanel.Visible = true;
                CurrentImagePanel.Visible = false;

                PreviewImage.ImageUrl = EditedImage.ImageUrl = _TempImageLocation + (string)Session["WorkingImage"];
            }
            else
            {
                EditedImagePanel.Visible = false;
                CurrentImagePanel.Visible = true;

                UserImage.ImageUrl = _TempImageLocation + "noimage.jpg";
            }
        }

        protected void ImageUploadButton_Click(object sender, EventArgs e)
        {
            string imageName = "";
            if (ImageUpload.HasFile)
            {
                string extension = System.IO.Path.GetExtension(ImageUpload.FileName).ToLower();
                if (_AllowedExtensions.Contains(extension))
                {
                    try
                    {
                        imageName = string.Format("{0}_{1}{2}", _UserID, DateTime.Now.ToFileTimeUtc(), extension);
                        ImageUpload.SaveAs(_ImagePath + imageName);

                        EditedImagePanel.Visible = true;
                        CurrentImagePanel.Visible = false;

                        PreviewImage.ImageUrl = EditedImage.ImageUrl = _TempImageLocation + imageName;

                        Session["WorkingImage"] = imageName;
                    }
                    catch (Exception ex)
                    {
                        ImageUploadErrorLabel.Text = "Image could not be uploaded. Details: " + ex.Message;
                    }
                }
                else
                {
                    ImageUploadErrorLabel.Text = "Wrong extension!!!";
                }
            }
        }

        protected void CropImageButton_Click(object sender, EventArgs e)
        {
            if (Session["WorkingImage"] == null)
                return;

            string imageName = Session["WorkingImage"].ToString();

            int w = Convert.ToInt32(Math.Floor(Convert.ToDouble(HIDDENW.Value)));
            int h = Convert.ToInt32(Math.Floor(Convert.ToDouble(HIDDENH.Value)));
            int x = Convert.ToInt32(Math.Floor(Convert.ToDouble(HIDDENX.Value)));
            int y = Convert.ToInt32(Math.Floor(Convert.ToDouble(HIDDENY.Value)));

            byte[] cropImage = Crop(_ImagePath + imageName, w, h, x, y);

            BLL.SaveUserProfilePhoto(_UserID, cropImage);

            File.Delete(_ImagePath + imageName);
            Session["WorkingImage"] = null;

            EditedImagePanel.Visible = false;
            CurrentImagePanel.Visible = true;
        }

        static byte[] Crop(string Img, int Width, int Height, int X, int Y)
        {
            try
            {
                using (System.Drawing.Image originalImage = System.Drawing.Image.FromFile(Img))
                {
                    using (System.Drawing.Bitmap bmp = new System.Drawing.Bitmap(Width, Height))
                    {
                        bmp.SetResolution(originalImage.HorizontalResolution, originalImage.VerticalResolution);
                        using (System.Drawing.Graphics Graphic = System.Drawing.Graphics.FromImage(bmp))
                        {
                            Graphic.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.AntiAlias;
                            Graphic.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.HighQualityBicubic;
                            Graphic.PixelOffsetMode = System.Drawing.Drawing2D.PixelOffsetMode.HighQuality;
                            Graphic.DrawImage(originalImage, new System.Drawing.Rectangle(0, 0, Width, Height), X, Y, Width, Height, System.Drawing.GraphicsUnit.Pixel);
                            MemoryStream ms = new MemoryStream();
                            bmp.Save(ms, originalImage.RawFormat);
                            return ms.GetBuffer();
                        }
                    }
                }
            }
            catch (Exception Ex)
            {
                throw (Ex);
            }
        }
    }
}

In order for this example to be as simple as possible, there is only one user that the image would be added to, hence the variable _UserID is set to 1. In the ImageUploadButton_Click event, the image is saved to a folder on the server. In CropImageButton_Click, the points of the cropped rectangle (represented in the hidden fields) are used to create array of bytes that is later saved to the database. On successful save, the original image is deleted from the file system.

I’ve put the business logic into separate file:

using System;
using System.Configuration;
using System.Linq;
using System.Web;

namespace JcropDemo
{
    public class BLL
    {
        public static Model Model
        {
            get
            {
                if (!HttpContext.Current.Items.Contains("Model"))
                    HttpContext.Current.Items.Add("Model", new Model());

                return (Model)HttpContext.Current.Items["Model"];
            }
        }

        public static void SaveUserProfilePhoto(int userID, byte[] photoContent)
        {
            User usr = Model.Entities.User.Where(x => x.UserId == userID).FirstOrDefault();
            if (usr != null)
            {
                Image img = new Image();
                img.ImageContent = photoContent;
                img.User.Add(usr);

                Model.Entities.AddToImage(img);
                Model.Entities.SaveChanges(true);
            }
        }
    }

    public class Model
    {
        private TestDbEntities _Entities = null;
        public TestDbEntities Entities
        {
            get
            {
                if (_Entities == null)
                {
                    if (ConfigurationManager.ConnectionStrings["TestDbEntities"] == null)
                    {
                        throw new NullReferenceException("Missing connection string!");
                    }
                    _Entities = new TestDbEntities(ConfigurationManager.ConnectionStrings["TestDbEntities"].ConnectionString);
                }
                return _Entities;
            }
        }
    }
}

Static method SaveUserProfilePhoto uses EF model to save the data to the SQL Server database.

So, once again: you can download the source code here, and remember to change your connection settings in Web.config!

Hope this example is useful, and I’ll be glad to get your feedback on it.

Crop & Save (Part I)

Hi all,

I’ve neglected the blog for some time because of the projects I’m currently involved in, but I’ll do my best to write more, I promise! :)

If you make code, you’ve probably at least once had the requirement to save an image in the system. For example, user can upload their profile photo so it could be displayed throughout your application. Let’s see how that could be done in C# ASP.NET, using SQL Server and Entity Framework. Also, we’ll add a cool feature of cropping photos, for which we’ll use jQuery plugin called JCrop. Since this is a fairly long example, I will divide it into couple of posts, first one discussing the database structure.

The database model for this use case is really a basic one. There are three tables: User – where the user information is stored, Image – the same for the photos/images to be uploaded, and UserImage – composite table, containing only primary keys for User and Image tables. Here is the database diagram:

DbModel

Note: User table structure is taken from a project I work on, so the fields in it reflect that app’s requirements. Some other applications may require different fields. In this example, the only field that will actually be used is UserId. I’ve added only one user to the table, having UserId set to 1. Have this in mind when going through this example!

User:

CREATE TABLE [dbo].[User](
       [UserId] [int] IDENTITY(1,1) NOT NULL,
       [Username] [nvarchar](50) NOT NULL,
       [Password] [varbinary](32) NULL,
       [FirstName] [nvarchar](50) NULL,
       [LastName] [nvarchar](50) NULL,
       [Activated] [bit] NULL,
       [Email] [nvarchar](100) NOT NULL,
       [Birthday] [datetime] NULL,
       [GenderId] [int] NULL,
       [DateCreated] [datetime] NULL,
       [DateUpdated] [datetime] NULL,
       [IsDeleted] [tinyint] NULL,
CONSTRAINT [PK_User] PRIMARY KEY CLUSTERED
(
       [UserId] ASC
)WITH (PAD_INDEX  = OFF, STATISTICS_NORECOMPUTE  = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS  = ON, ALLOW_PAGE_LOCKS  = ON) ON [PRIMARY]
) ON [PRIMARY]
GO

Image:

CREATE TABLE [dbo].[Image](
       [ImageId] [int] IDENTITY(1,1) NOT NULL,
       [ImageContent] [varbinary](max) NOT NULL,
CONSTRAINT [PK_Image] PRIMARY KEY CLUSTERED
(
       [ImageId] ASC
)WITH (PAD_INDEX  = OFF, STATISTICS_NORECOMPUTE  = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS  = ON, ALLOW_PAGE_LOCKS  = ON) ON [PRIMARY]
) ON [PRIMARY]
GO

UserImage:

CREATE TABLE [dbo].[UserImage](
       [UserId] [int] NOT NULL,
       [ImageId] [int] NOT NULL,
CONSTRAINT [PK_UserImage] PRIMARY KEY CLUSTERED
(
       [UserId] ASC,
       [ImageId] ASC
)WITH (PAD_INDEX  = OFF, STATISTICS_NORECOMPUTE  = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS  = ON, ALLOW_PAGE_LOCKS  = ON) ON [PRIMARY]
) ON [PRIMARY]
GO

ALTER TABLE [dbo].[UserImage]  WITH CHECK ADD  CONSTRAINT [FK_UserImage_Image] FOREIGN KEY([ImageId])
REFERENCES [dbo].[Image] ([ImageId])
GO

ALTER TABLE [dbo].[UserImage] CHECK CONSTRAINT [FK_UserImage_Image]
GO

ALTER TABLE [dbo].[UserImage]  WITH CHECK ADD  CONSTRAINT [FK_UserImage_User] FOREIGN KEY([UserId])
REFERENCES [dbo].[User] ([UserId])
GO

ALTER TABLE [dbo].[UserImage] CHECK CONSTRAINT [FK_UserImage_User]
GO

Here is the corresponding EF model:

EFModel

Note: If you’re wondering where the UserImage table has disappeared, remember that it is an aggregate table, so in EF model it won’t be represented as entity type, but as association!

Association

In order for this example to be simple, we’ll save only one user to the database and make sure its id is set to 1:

SET IDENTITY_INSERT [User] ON

INSERT INTO [User](UserId, Username, Email)
VALUES (1, 'test', 'test@example.com')

SET IDENTITY_INSERT [User] OFF

When the database is set, we can move on to programming a simple ASP.NET application for saving the images. You can check how it is done in this post.

Getting Shamsi Date From Gregorian Date

Shamsi calendar, also known as Iranian, Persian or Jalāli Calendar is a solar calendar which is still in use in Iran and Afghanistan. If you need to convert date from Gregorian to Jalali calendar and vice versa, here is a handy Iraniar Calendar Converter. On the other hand, if you need to convert the dates in your own application, check the SQL Server function in this article.

So, let’s start. The main function is getShamsiDate, which receives Gregorian date and returns Shamsi date. I won’t get into astronomical explanations, but you can check this link to get more insight.

CREATE FUNCTION [dbo].[getShamsiDate] (@Date smalldatetime)
RETURNS nvarchar(20)
AS
BEGIN
    


DECLARE @Year int,
		@Month int,
		@Day int, 
		@ToEid int, 
		@Elapsed int, 
		@counter int,
		@retVal nvarchar(20),
		@nDay nvarchar(2),
		@nMonth nvarchar(2)

DECLARE @LeapSumOfDays TABLE 
(
	ID int,
	Val int
)

DECLARE @NonLeapSumOfDays TABLE(
	ID int,
	Val int
)

INSERT INTO @LeapSumOfDays
SELECT ROW_NUMBER() OVER (ORDER BY CAST([value] AS int)) AS ID, CAST([value] AS int) AS Val
FROM dbo.SplitStringList ('0,31,62,93,124,155,186,216,246,276,306,336,366')


INSERT INTO @NonLeapSumOfDays
SELECT ROW_NUMBER() OVER (ORDER BY CAST([value] AS int)) AS ID, CAST([value] AS int) AS Val
FROM dbo.SplitStringList ('0,31,62,93,124,155,186,216,246,276,306,336,365')


SET @Year = YEAR(@Date) - 621

IF dbo.isLeapYearShamsi(@Year - 1) = 1 AND dbo.isLeapYear(YEAR(@Date)) = 1
	SET @ToEid = 80
ELSE
	SET @ToEid = 79

IF DATEPART(dy,@Date) <= @ToEid
BEGIN
	SET @Year = @Year-1
	SET @Elapsed = 286 + DATEPART(dy,@Date)
	IF dbo.isLeapYearShamsi(@Year) = 1 AND dbo.isLeapYear(YEAR(@Date)) = 0
		SET @Elapsed = @Elapsed + 1
END
ELSE
	SET @Elapsed = DATEPART(dy,@Date) - @ToEid
     

IF dbo.isLeapYearShamsi(@Year) = 1
BEGIN
	SELECT TOP 1 @counter = ID FROM @NonLeapSumOfDays WHERE val >= @Elapsed
	SET @Month = @counter - 1
	SELECT @Day = @Elapsed - val FROM @NonLeapSumOfDays WHERE ID = @counter - 1
END
ELSE
BEGIN
	SELECT TOP 1 @counter = ID FROM @LeapSumOfDays WHERE val >= @Elapsed
	SET @Month = @counter - 1
	SELECT @Day = @Elapsed - val FROM @LeapSumOfDays WHERE ID = @counter - 1
END


IF LEN(@Day) = 1 
	SET @nDay = '0' + CAST(@Day AS nvarchar(2))
ELSE
	SET @nDay = CAST(@Day AS nvarchar(2))

IF LEN(@Month) = 1 
	SET @nMonth = '0' + CAST(@Month AS nvarchar(2))
ELSE
	SET @nMonth =  CAST(@Month AS nvarchar(2))


SET @retVal = CAST(@Year AS nvarchar(4)) + '/' + @nMonth + '/' + @nDay   

RETURN @retVal

END

There are couple of helper functions used by the main function, and their code follows.

SplitStringList function gets comma separated list of strings and returns one column table, having given string values as table records. It is useful in cases you want to get some values into the table without writing INSERT INTO statement n times.

CREATE FUNCTION [dbo].[SplitStringList]
(
	@StrList nvarchar(4000)
)
RETURNS 
@ParsedList table
(
	value nvarchar(500) COLLATE Arabic_CI_AS
)
AS
BEGIN
	DECLARE @Value nvarchar(500), @Pos int

	SET @StrList = LTRIM(RTRIM(@StrList))+ ','
	SET @Pos = CHARINDEX(',', @StrList, 1)

	IF REPLACE(@StrList, ',', '') <> ''
	BEGIN
		WHILE @Pos > 0
		BEGIN
			SET @Value = LTRIM(RTRIM(LEFT(@StrList, @Pos - 1)))
			IF @Value <> ''
			BEGIN
				INSERT INTO @ParsedList (value) 
				VALUES (CAST(@Value AS nvarchar)) --Use Appropriate conversion
			END
			SET @StrList = RIGHT(@StrList, LEN(@StrList) - @Pos)
			SET @Pos = CHARINDEX(',', @StrList, 1)

		END
	END	
	RETURN
END

Function isLeapYear checks if given year is (Gregorian)leap year. The code is really simple – it is checked if February 29th exists in the given year.

CREATE FUNCTION [dbo].[isLeapYear] (@Year SMALLINT)
RETURNS BIT
AS
BEGIN
    DECLARE @leapDate SMALLDATETIME
    DECLARE @checkDay TINYINT
 
    SET @leapDate = CONVERT(VARCHAR(4), @Year) + '0228'
    SET @checkDay = DATEPART(d, DATEADD(d, 1, @leapDate))
    IF (@checkDay = 29)
        RETURN 1

    RETURN 0  
END

isLeapYearShamsi checks if the given year is Shamsi leap year. Once more, you can check the algorithm, if you want to find more.

CREATE FUNCTION [dbo].[isLeapYearShamsi]( @Year int )
RETURNS bit
AS

BEGIN 

	DECLARE @B int,
			@isLeap bit

	SET @B = @Year % 33

	IF @B IN (1, 5, 9, 13, 17, 22, 26, 30)
		SET @isLeap = 1
	ELSE
		SET @isLeap = 0
	
	RETURN @isLeap

END

So, to get Shamsi representation of, for example, today’s date, call the function:

SELECT dbo.getShamsiDate('20130430')

And the result is:

1392/02/10

Hope this helps.

Entity Framework and Setting Primary Keys on Views

When using Entity Framework, it’s important to set primary keys on table and view objects in SQL Server. If you skip this step, EF will define composite primary key made of all non-nullable columns belonging to the object. That way you can end up with some strange results and spend hours finding out what went wrong (trust me, I’ve been there). So, first of all: set primary keys in the database!

OK, with tables, that’s just it – set the primary keys and you’re good to go. Now, what about views? In case of views having primary keys defined in underlying tables, that should also be enough. But what to do if there are simply no columns to use as PK? Well, this can be achieved by using ROW_NUMBER() function. Here is the actual example from one of my projects:

CREATE VIEW [dbo].[vw_Action]
AS

SELECT *, ROW_NUMBER() OVER(ORDER BY ActionDate DESC) AS RowID
FROM
(
   SELECT User1ID AS UserID, NULL AS FriendID, Object1ID, Object2ID, ActionType, ActionDate
   FROM vw_ActionBase
   WHERE ActionType = 1
   GROUP BY User1ID, Object1ID, Object2ID, ActionType, ActionDate

   UNION

   SELECT User1ID AS UserID, NULL AS FriendID, Object1ID, Object2ID, ActionType, ActionDate
   FROM vw_ActionBase
   WHERE ActionType = 2
   GROUP BY User1ID, Object1ID, Object2ID, ActionType, ActionDate

   UNION

   SELECT User1ID AS UserID, NULL AS FriendID, Object1ID, Object2ID, ActionType, ActionDate
   FROM vw_ActionBase
   WHERE ActionType = 3
   GROUP BY User1ID, Object1ID, Object2ID, ActionType, Object1ID, ActionDate

)x

Now, here’s something that made me confused: I’ve expected RowID column to be defined as NOT NULL. I thought that if there was a row, it should have a number. But I was wrong, as you can see:

vw_Action

So, the column I wanted to use as primary key (RowID) was nullable, and the other (ActionType) that shouldn’t be primary key was in fact the only PK candidate for Entity Framework. As expected, when I performed Update From Database option, the result in edmx file was:

<EntityType Name="vw_Action">
    <Key>
        <PropertyRef Name="ActionType" />
    </Key>
    <Property Name="UserID" Type="int" />
    <Property Name="FriendID" Type="int" />
    <Property Name="Object1ID" Type="int" />
    <Property Name="Object2ID" Type="int" />
    <Property Name="ActionType" Type="int" Nullable="false" />
    <Property Name="ActionDate" Type="datetime" />
    <Property Name="RowID" Type="bigint" />
</EntityType>

If you get tempted to set keys manually in EF, don’t! You will run into errors similar to these:

EFModel.Model.msl(266,10) : error 3002: Problem in Mapping Fragment starting at line 266: Potential runtime violation of table vw_Action’s keys (vw_Action.ActionType): Columns (vw_Action.ActionType) are mapped to EntitySet vw_Action’s properties (vw_Action.ActionType) on the conceptual side but they do not form the EntitySet’s key properties (vw_Action.RowID).\r\n\r\nEFModel.Model.msl(266,10) : error 3003: Problem in Mapping Fragment starting at line 266: All the key properties (vw_Action.RowID) of the EntitySet vw_Action must be mapped to all the key properties (vw_Action.ActionType) of table vw_Action.\r\n

Also, changes will be lost on next update from database.

So, what’s the solution?

The solution is to explicitly define columns you want for PK in SQL server as non-nullable using ISNULL function. Similary, if there is an actual non-nullable column that you want excluded from the PK, use NULLIF, like this:

CREATE VIEW [dbo].[vw_Action]
AS

SELECT UserID, FriendID, Object1ID, Object2ID, NULLIF(ActionType, -1) AS ActionType,
ActionDate, ISNULL(ROW_NUMBER() OVER(ORDER BY ActionDate DESC), -1) AS RowID
FROM
(
    ...
)x

What will be the result of this code? SQL Server will understand that RowID could never be NULL – it will be set to -1 in case of null value (although this will never happen in my case anyway). On the other hand, ActionType column, that can actually never be NULL since it has values 1, 2 and 3, will be interpreted as nullable.

Here is how it all looks in the end:

vw_Action_1

After updating model from the database, the right column will be set as entity primary key in edmx file.

——— EDIT ———
Related post: A Quick Note on COALESCE and ISNULL